1
0
Fork 0
forked from wry/wry

Merge pull request #291 from mahkoh/jorth/session-restore

portal: implement session restoration
This commit is contained in:
mahkoh 2024-10-11 17:02:12 +02:00 committed by GitHub
commit 06d7fff905
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
39 changed files with 987 additions and 380 deletions

6
Cargo.lock generated
View file

@ -586,6 +586,7 @@ dependencies = [
"repc",
"rustc-demangle",
"serde",
"serde_json",
"shaderc",
"smallvec",
"thiserror",
@ -1067,12 +1068,13 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.114"
version = "1.0.128"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0"
checksum = "6ff5456707a1de34e7e37f2a6fd3d3f808c318259cbd01ab6377795054b483d8"
dependencies = [
"indexmap",
"itoa",
"memchr",
"ryu",
"serde",
]

View file

@ -56,6 +56,7 @@ ash = "0.38.0"
gpu-alloc = "0.6.0"
gpu-alloc-ash = "0.7.0"
serde = { version = "1.0.196", features = ["derive"] }
serde_json = "1.0.128"
enum-map = "2.7.3"
png = "0.17.13"
rustc-demangle = { version = "0.1.24", optional = true }

View file

@ -7,6 +7,8 @@
- Allow X windows to scale themselves.
- Implement ext-image-capture-source-v1.
- Implement ext-image-copy-capture-v1.
- Implement screencast session restoration.
- Fix screen sharing in zoom.
# 1.6.0 (2024-09-25)

View file

@ -271,6 +271,7 @@ fn start_compositor2(
cpu_worker,
ui_drag_enabled: Cell::new(true),
ui_drag_threshold_squared: Cell::new(10),
toplevels: Default::default(),
});
state.tracker.register(ClientId::from_raw(0));
create_dummy_output(&state);

View file

@ -15,8 +15,8 @@ use {
wire_ei::{
ei_device::{
ClientFrame, ClientStartEmulating, ClientStopEmulating, Destroyed, DeviceType,
Done, EiDeviceRequestHandler, Interface, Paused, Region, Release, Resumed,
ServerFrame, ServerStartEmulating,
Done, EiDeviceRequestHandler, Interface, Paused, Region, RegionMappingId, Release,
Resumed, ServerFrame, ServerStartEmulating,
},
EiDeviceId,
},
@ -100,6 +100,13 @@ impl EiDevice {
});
}
pub fn send_region_mapping_id(&self, mapping_id: &str) {
self.client.event(RegionMappingId {
self_id: self.id,
mapping_id,
});
}
#[expect(dead_code)]
pub fn send_paused(&self, serial: u32) {
self.client.event(Paused {

View file

@ -346,6 +346,7 @@ impl EiSeat {
apply!(EI_CAP_KEYBOARD, create_keyboard);
apply!(EI_CAP_TOUCHSCREEN, create_touchscreen);
for output in self.client.state.root.outputs.lock().values() {
device.send_region_mapping_id(&output.global.connector.name);
device.send_region(
output.node_absolute_position(),
output.global.persistent.scale.get(),

View file

@ -23,17 +23,18 @@ use {
leaks::Tracker,
object::{Object, Version},
screenshoter::take_screenshot,
utils::errorfmt::ErrorFmt,
utils::{errorfmt::ErrorFmt, toplevel_identifier::ToplevelIdentifier},
wire::{jay_compositor::*, JayCompositorId, JayScreenshotId},
},
bstr::ByteSlice,
log::Level,
std::{cell::Cell, ops::Deref, rc::Rc},
std::{cell::Cell, ops::Deref, rc::Rc, str::FromStr},
thiserror::Error,
};
pub const CREATE_EI_SESSION_SINCE: Version = Version(5);
pub const SCREENSHOT_SPLITUP_SINCE: Version = Version(6);
pub const GET_TOPLEVEL_SINCE: Version = Version(12);
pub struct JayCompositorGlobal {
name: GlobalName,
@ -71,7 +72,7 @@ impl Global for JayCompositorGlobal {
}
fn version(&self) -> u32 {
11
12
}
fn required_caps(&self) -> ClientCaps {
@ -364,12 +365,7 @@ impl JayCompositorRequestHandler for JayCompositor {
fn select_toplevel(&self, req: SelectToplevel, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let seat = self.client.lookup(req.seat)?;
let obj = Rc::new(JaySelectToplevel {
id: req.id,
client: self.client.clone(),
tracker: Default::default(),
destroyed: Cell::new(false),
});
let obj = JaySelectToplevel::new(&self.client, req.id, self.version);
track!(self.client, obj);
self.client.add_client_obj(&obj)?;
let selector = JayToplevelSelector {
@ -422,6 +418,26 @@ impl JayCompositorRequestHandler for JayCompositor {
self.client.add_client_obj(&obj)?;
Ok(())
}
fn get_toplevel(&self, req: GetToplevel<'_>, _slf: &Rc<Self>) -> Result<(), Self::Error> {
let obj = JaySelectToplevel::new(&self.client, req.id, self.version);
track!(self.client, obj);
self.client.add_client_obj(&obj)?;
let tl = match ToplevelIdentifier::from_str(req.toplevel_id) {
Ok(id) => self
.client
.state
.toplevels
.get(&id)
.and_then(|w| w.upgrade()),
Err(e) => {
log::error!("Could not parse toplevel id: {}", ErrorFmt(e));
None
}
};
obj.done(tl);
Ok(())
}
}
object_base! {

View file

@ -1,7 +1,10 @@
use {
crate::{
client::{Client, ClientError},
ifs::{jay_toplevel::JayToplevel, wl_seat::ToplevelSelector},
ifs::{
jay_toplevel::{JayToplevel, ID_SINCE},
wl_seat::ToplevelSelector,
},
leaks::Tracker,
object::{Object, Version},
tree::ToplevelNode,
@ -17,6 +20,7 @@ pub struct JaySelectToplevel {
pub client: Rc<Client>,
pub tracker: Tracker<Self>,
pub destroyed: Cell<bool>,
pub version: Version,
}
pub struct JayToplevelSelector {
@ -35,38 +39,62 @@ impl Drop for JayToplevelSelector {
if self.jst.destroyed.get() {
return;
}
let id = match self.tl.take() {
None => JayToplevelId::NONE,
self.jst.done(self.tl.take());
}
}
impl JaySelectToplevel {
pub fn done(&self, tl: Option<Rc<dyn ToplevelNode>>) {
let jtl = match tl {
None => None,
Some(toplevel) => {
let id = match self.jst.client.new_id() {
let id = match self.client.new_id() {
Ok(id) => id,
Err(e) => {
self.jst.client.error(e);
self.client.error(e);
return;
}
};
let jtl = Rc::new(JayToplevel {
id,
client: self.jst.client.clone(),
client: self.client.clone(),
tracker: Default::default(),
toplevel,
destroyed: Cell::new(false),
version: self.version,
});
track!(self.jst.client, jtl);
self.jst.client.add_server_obj(&jtl);
track!(self.client, jtl);
self.client.add_server_obj(&jtl);
jtl.toplevel
.tl_data()
.jay_toplevels
.set((jtl.client.id, jtl.id), jtl.clone());
jtl.id
Some(jtl)
}
};
self.jst.send_done(id);
let _ = self.jst.client.remove_obj(&*self.jst);
match jtl {
None => self.send_done(JayToplevelId::NONE),
Some(jtl) => {
self.send_done(jtl.id);
if jtl.version >= ID_SINCE {
jtl.send_id();
jtl.send_done();
}
}
}
let _ = self.client.remove_obj(self);
}
pub fn new(client: &Rc<Client>, id: JaySelectToplevelId, version: Version) -> Rc<Self> {
Rc::new(JaySelectToplevel {
id,
client: client.clone(),
tracker: Default::default(),
destroyed: Cell::new(false),
version,
})
}
}
impl JaySelectToplevel {
fn send_done(&self, id: JayToplevelId) {
self.client.event(Done {
self_id: self.id,

View file

@ -10,12 +10,15 @@ use {
thiserror::Error,
};
pub const ID_SINCE: Version = Version(12);
pub struct JayToplevel {
pub id: JayToplevelId,
pub client: Rc<Client>,
pub tracker: Tracker<Self>,
pub toplevel: Rc<dyn ToplevelNode>,
pub destroyed: Cell<bool>,
pub version: Version,
}
impl JayToplevel {
@ -35,6 +38,18 @@ impl JayToplevel {
fn send_destroyed(&self) {
self.client.event(Destroyed { self_id: self.id });
}
pub fn send_id(&self) {
let s = self.toplevel.tl_data().identifier.get().to_string();
self.client.event(Id {
self_id: self.id,
id: &s,
})
}
pub fn send_done(&self) {
self.client.event(Done { self_id: self.id })
}
}
impl JayToplevelRequestHandler for JayToplevel {

View file

@ -1227,7 +1227,7 @@ impl UiDragUsecase for TileDragUsecase {
return;
};
let detach = || {
let placeholder = Rc::new(PlaceholderNode::new_empty(&seat.state));
let placeholder = Rc::new_cyclic(|weak| PlaceholderNode::new_empty(&seat.state, weak));
src_parent
.clone()
.cnode_replace_child(src.tl_as_node(), placeholder.clone());

View file

@ -205,18 +205,21 @@ impl Xwindow {
if xsurface.xwindow.is_some() {
return Err(XWindowError::AlreadyAttached);
}
let tld = ToplevelData::new(
&data.state,
data.info.title.borrow_mut().clone().unwrap_or_default(),
Some(surface.client.clone()),
);
tld.pos.set(surface.extents.get());
let slf = Rc::new(Self {
id: data.state.node_ids.next(),
data: data.clone(),
display_link: Default::default(),
toplevel_data: tld,
x: xsurface,
let slf = Rc::new_cyclic(|weak| {
let tld = ToplevelData::new(
&data.state,
data.info.title.borrow_mut().clone().unwrap_or_default(),
Some(surface.client.clone()),
weak,
);
tld.pos.set(surface.extents.get());
Self {
id: data.state.node_ids.next(),
data: data.clone(),
display_link: Default::default(),
toplevel_data: tld,
x: xsurface,
}
});
slf.x.xwindow.set(Some(slf.clone()));
slf.x.surface.set_toplevel(Some(slf.clone()));
@ -344,12 +347,7 @@ impl Node for Xwindow {
}
let rect = self.x.surface.buffer_abs_pos.get();
if x < rect.width() && y < rect.height() {
tree.push(FoundNode {
node: self.x.surface.clone(),
x,
y,
});
return FindTreeResult::AcceptsInput;
return self.x.surface.find_tree_at_(x, y, tree);
}
FindTreeResult::Other
}

View file

@ -347,7 +347,7 @@ impl XdgSurfaceRequestHandler for XdgSurface {
);
return Err(XdgSurfaceError::AlreadyConstructed);
}
let toplevel = Rc::new(XdgToplevel::new(req.id, slf));
let toplevel = Rc::new_cyclic(|weak| XdgToplevel::new(req.id, slf, weak));
track!(self.surface.client, toplevel);
self.surface.client.add_client_obj(&toplevel)?;
self.ext.set(Some(toplevel.clone()));

View file

@ -38,7 +38,7 @@ use {
cell::{Cell, RefCell},
fmt::{Debug, Formatter},
mem,
rc::Rc,
rc::{Rc, Weak},
},
thiserror::Error,
};
@ -115,7 +115,7 @@ impl Debug for XdgToplevel {
}
impl XdgToplevel {
pub fn new(id: XdgToplevelId, surface: &Rc<XdgSurface>) -> Self {
pub fn new(id: XdgToplevelId, surface: &Rc<XdgSurface>, slf: &Weak<Self>) -> Self {
let mut states = AHashSet::new();
states.insert(STATE_TILED_LEFT);
states.insert(STATE_TILED_RIGHT);
@ -141,6 +141,7 @@ impl XdgToplevel {
state,
String::new(),
Some(surface.surface.client.clone()),
slf,
),
drag: Default::default(),
is_mapped: Cell::new(false),

View file

@ -740,20 +740,28 @@ macro_rules! ei_object_base {
macro_rules! logical_to_client_wire_scale {
($client:expr, $($field:expr),+ $(,)?) => {
if let Some(scale) = $client.wire_scale.get() {
$(
$field = $field * scale;
)+
#[expect(clippy::allow_attributes)]
{
#[allow(clippy::assign_op_pattern)]
if let Some(scale) = $client.wire_scale.get() {
$(
$field = $field * scale;
)+
}
}
};
}
macro_rules! client_wire_scale_to_logical {
($client:expr, $($field:expr),+ $(,)?) => {
if let Some(scale) = $client.wire_scale.get() {
$(
$field = $field / scale;
)+
#[expect(clippy::allow_attributes)]
{
#[allow(clippy::assign_op_pattern)]
if let Some(scale) = $client.wire_scale.get() {
$(
$field = $field / scale;
)+
}
}
};
}

View file

@ -2,6 +2,7 @@ mod ptl_display;
mod ptl_remote_desktop;
mod ptl_render_ctx;
mod ptl_screencast;
mod ptl_session;
mod ptl_text;
mod ptr_gui;
@ -16,12 +17,13 @@ use {
forker::ForkerError,
io_uring::IoUring,
logger::Logger,
pipewire::pw_con::{PwConHolder, PwConOwner},
pipewire::pw_con::{PwCon, PwConHolder, PwConOwner},
portal::{
ptl_display::{watch_displays, PortalDisplay, PortalDisplayId},
ptl_remote_desktop::{add_remote_desktop_dbus_members, RemoteDesktopSession},
ptl_remote_desktop::add_remote_desktop_dbus_members,
ptl_render_ctx::PortalRenderCtx,
ptl_screencast::{add_screencast_dbus_members, ScreencastSession},
ptl_screencast::add_screencast_dbus_members,
ptl_session::PortalSession,
},
utils::{
clone3::{fork_with_pidfd, Forked},
@ -200,11 +202,11 @@ async fn run_async(
wheel,
displays: Default::default(),
dbus,
screencasts: Default::default(),
remote_desktop_sessions: Default::default(),
sessions: Default::default(),
next_id: NumCell::new(1),
render_ctxs: Default::default(),
dma_buf_ids: Default::default(),
pw_con: pw_con.as_ref().map(|c| c.con.clone()),
});
if let Some(pw_con) = &pw_con {
pw_con.con.owner.set(Some(state.clone()));
@ -295,11 +297,11 @@ struct PortalState {
wheel: Rc<Wheel>,
displays: CopyHashMap<PortalDisplayId, Rc<PortalDisplay>>,
dbus: Rc<DbusSocket>,
screencasts: CopyHashMap<String, Rc<ScreencastSession>>,
remote_desktop_sessions: CopyHashMap<String, Rc<RemoteDesktopSession>>,
sessions: CopyHashMap<String, Rc<PortalSession>>,
next_id: NumCell<u32>,
render_ctxs: CopyHashMap<c::dev_t, Weak<PortalRenderCtx>>,
dma_buf_ids: Rc<DmaBufIds>,
pw_con: Option<Rc<PwCon>>,
}
impl PortalState {

View file

@ -5,15 +5,19 @@ use {
ifs::wl_seat::POINTER,
object::Version,
portal::{
ptl_remote_desktop::RemoteDesktopSession,
ptl_render_ctx::{PortalRenderCtx, PortalServerRenderCtx},
ptl_screencast::ScreencastSession,
ptl_session::PortalSession,
ptr_gui::WindowData,
PortalState,
},
utils::{
bitflags::BitflagsExt, clonecell::CloneCell, copyhashmap::CopyHashMap,
errorfmt::ErrorFmt, hash_map_ext::HashMapExt, oserror::OsError,
bitflags::BitflagsExt,
clonecell::CloneCell,
copyhashmap::CopyHashMap,
errorfmt::ErrorFmt,
hash_map_ext::HashMapExt,
opaque::{opaque, Opaque},
oserror::OsError,
},
video::drm::Drm,
wire::{
@ -26,6 +30,8 @@ use {
usr_jay_output::{UsrJayOutput, UsrJayOutputOwner},
usr_jay_pointer::UsrJayPointer,
usr_jay_render_ctx::UsrJayRenderCtxOwner,
usr_jay_workspace::{UsrJayWorkspace, UsrJayWorkspaceOwner},
usr_jay_workspace_watcher::{UsrJayWorkspaceWatcher, UsrJayWorkspaceWatcherOwner},
usr_linux_dmabuf::UsrLinuxDmabuf,
usr_wl_compositor::UsrWlCompositor,
usr_wl_output::{UsrWlOutput, UsrWlOutputOwner},
@ -61,9 +67,11 @@ struct PortalDisplayPrelude {
shared_ids!(PortalDisplayId);
pub struct PortalDisplay {
pub id: PortalDisplayId,
pub unique_id: Opaque,
pub con: Rc<UsrCon>,
pub(super) state: Rc<PortalState>,
registry: Rc<UsrWlRegistry>,
_workspace_watcher: Rc<UsrJayWorkspaceWatcher>,
pub dmabuf: CloneCell<Option<Rc<UsrLinuxDmabuf>>>,
pub jc: Rc<UsrJayCompositor>,
@ -75,10 +83,10 @@ pub struct PortalDisplay {
pub outputs: CopyHashMap<u32, Rc<PortalOutput>>,
pub seats: CopyHashMap<u32, Rc<PortalSeat>>,
pub workspaces: CopyHashMap<u32, Rc<UsrJayWorkspace>>,
pub windows: CopyHashMap<WlSurfaceId, Rc<WindowData>>,
pub screencasts: CopyHashMap<String, Rc<ScreencastSession>>,
pub remote_desktop_sessions: CopyHashMap<String, Rc<RemoteDesktopSession>>,
pub sessions: CopyHashMap<String, Rc<PortalSession>>,
}
pub struct PortalOutput {
@ -215,7 +223,7 @@ impl UsrJayRenderCtxOwner for PortalDisplay {
impl UsrConOwner for PortalDisplay {
fn killed(&self) {
log::info!("Removing display {}", self.id);
for sc in self.screencasts.lock().drain_values() {
for sc in self.sessions.lock().drain_values() {
sc.kill();
}
self.windows.clear();
@ -243,6 +251,20 @@ impl UsrWlRegistryOwner for PortalDisplay {
}
}
impl UsrJayWorkspaceWatcherOwner for PortalDisplay {
fn new(self: Rc<Self>, ev: Rc<UsrJayWorkspace>, linear_id: u32) {
ev.owner.set(Some(self.clone()));
self.workspaces.set(linear_id, ev);
}
}
impl UsrJayWorkspaceOwner for PortalDisplay {
fn destroyed(&self, ws: &UsrJayWorkspace) {
self.workspaces.remove(&ws.linear_id.get());
self.con.remove_obj(ws);
}
}
impl UsrJayOutputOwner for PortalOutput {
fn destroyed(&self) {
log::info!(
@ -323,7 +345,7 @@ fn finish_display_connect(dpy: Rc<PortalDisplayPrelude>) {
con: dpy.con.clone(),
owner: Default::default(),
caps: Default::default(),
version: Version(version.min(9)),
version: Version(version.min(12)),
});
dpy.con.add_object(jc.clone());
dpy.registry.request_bind(name, jc.version.0, jc.deref());
@ -398,12 +420,15 @@ fn finish_display_connect(dpy: Rc<PortalDisplayPrelude>) {
let comp = get!(comp_opt, WlCompositor);
let fsm = get!(fsm_opt, WpFractionalScaleManagerV1);
let vp = get!(vp_opt, WpViewporter);
let ww = jc.watch_workspaces();
let dpy = Rc::new(PortalDisplay {
id: dpy.state.id(),
unique_id: opaque(),
con: dpy.con.clone(),
state: dpy.state.clone(),
registry: dpy.registry.clone(),
_workspace_watcher: ww.clone(),
dmabuf: CloneCell::new(dmabuf_opt),
jc,
outputs: Default::default(),
@ -414,13 +439,14 @@ fn finish_display_connect(dpy: Rc<PortalDisplayPrelude>) {
fsm,
vp,
windows: Default::default(),
screencasts: Default::default(),
remote_desktop_sessions: Default::default(),
sessions: Default::default(),
workspaces: Default::default(),
});
dpy.state.displays.set(dpy.id, dpy.clone());
dpy.con.owner.set(Some(dpy.clone()));
dpy.registry.owner.set(Some(dpy.clone()));
ww.owner.set(Some(dpy.clone()));
let jrc = dpy.jc.get_render_context();
jrc.owner.set(Some(dpy.clone()));
@ -464,6 +490,7 @@ fn add_output(dpy: &Rc<PortalDisplay>, name: u32, version: u32) {
con: dpy.con.clone(),
owner: Default::default(),
version: Version(version.min(4)),
name: Default::default(),
});
dpy.con.add_object(wl.clone());
dpy.registry.request_bind(name, wl.version.0, wl.deref());

View file

@ -2,17 +2,18 @@ mod remote_desktop_gui;
use {
crate::{
dbus::{prelude::Variant, DbusObject, DictEntry, DynamicType, PendingReply, FALSE},
dbus::{prelude::Variant, DbusObject, PendingReply},
ifs::jay_compositor::CREATE_EI_SESSION_SINCE,
portal::{
ptl_display::{PortalDisplay, PortalDisplayId},
ptl_remote_desktop::remote_desktop_gui::SelectionGui,
ptl_screencast::ScreencastPhase,
ptl_session::{PortalSession, PortalSessionReply},
PortalState, PORTAL_SUCCESS,
},
utils::{
clonecell::{CloneCell, UnsafeCellCloneSafe},
copyhashmap::CopyHashMap,
hash_map_ext::HashMapExt,
},
wire_dbus::{
org,
@ -21,24 +22,15 @@ use {
ConnectToEIS, ConnectToEISReply, CreateSession, CreateSessionReply,
SelectDevices, SelectDevicesReply, Start, StartReply,
},
session::{CloseReply as SessionCloseReply, Closed},
session::CloseReply as SessionCloseReply,
},
},
wl_usr::usr_ifs::usr_jay_ei_session::{UsrJayEiSession, UsrJayEiSessionOwner},
},
std::{borrow::Cow, cell::Cell, ops::Deref, rc::Rc},
std::{cell::Cell, ops::Deref, rc::Rc},
uapi::OwnedFd,
};
shared_ids!(ScreencastSessionId);
pub struct RemoteDesktopSession {
_id: ScreencastSessionId,
state: Rc<PortalState>,
pub app: String,
session_obj: DbusObject,
pub phase: CloneCell<RemoteDesktopPhase>,
}
#[derive(Clone)]
pub enum RemoteDesktopPhase {
Init,
@ -52,25 +44,23 @@ pub enum RemoteDesktopPhase {
unsafe impl UnsafeCellCloneSafe for RemoteDesktopPhase {}
pub struct SelectingDisplay {
pub session: Rc<RemoteDesktopSession>,
pub session: Rc<PortalSession>,
pub request_obj: Rc<DbusObject>,
pub reply: Rc<PendingReply<StartReply<'static>>>,
pub guis: CopyHashMap<PortalDisplayId, Rc<SelectionGui>>,
}
pub struct StartingRemoteDesktop {
pub session: Rc<RemoteDesktopSession>,
pub _request_obj: Rc<DbusObject>,
pub reply: Rc<PendingReply<StartReply<'static>>>,
pub session: Rc<PortalSession>,
pub request_obj: Rc<DbusObject>,
pub dpy: Rc<PortalDisplay>,
pub ei_session: Rc<UsrJayEiSession>,
}
pub struct StartedRemoteDesktop {
session: Rc<RemoteDesktopSession>,
dpy: Rc<PortalDisplay>,
ei_session: Rc<UsrJayEiSession>,
ei_fd: Cell<Option<Rc<OwnedFd>>>,
pub session: Rc<PortalSession>,
pub dpy: Rc<PortalDisplay>,
pub ei_session: Rc<UsrJayEiSession>,
pub ei_fd: Cell<Option<Rc<OwnedFd>>>,
}
bitflags! {
@ -83,34 +73,6 @@ bitflags! {
impl UsrJayEiSessionOwner for StartingRemoteDesktop {
fn created(&self, fd: &Rc<OwnedFd>) {
{
let inner_type = DynamicType::DictEntry(
Box::new(DynamicType::String),
Box::new(DynamicType::Variant),
);
let kt = DynamicType::Struct(vec![
DynamicType::U32,
DynamicType::Array(Box::new(inner_type.clone())),
]);
let variants = [
DictEntry {
key: "devices".into(),
value: Variant::U32(DeviceTypes::all().0),
},
DictEntry {
key: "clipboard_enabled".into(),
value: Variant::Bool(FALSE),
},
DictEntry {
key: "streams".into(),
value: Variant::Array(kt, vec![]),
},
];
self.reply.ok(&StartReply {
response: PORTAL_SUCCESS,
results: Cow::Borrowed(&variants[..]),
});
}
let started = Rc::new(StartedRemoteDesktop {
session: self.session.clone(),
dpy: self.dpy.clone(),
@ -118,14 +80,23 @@ impl UsrJayEiSessionOwner for StartingRemoteDesktop {
ei_fd: Cell::new(Some(fd.clone())),
});
self.session
.phase
.rd_phase
.set(RemoteDesktopPhase::Started(started.clone()));
started.ei_session.owner.set(Some(started.clone()));
if let ScreencastPhase::SourcesSelected(s) = self.session.sc_phase.get() {
self.session.screencast_restore(
&self.request_obj,
s.restore_data.take(),
Some(self.dpy.clone()),
);
} else {
self.session.send_start_reply(None, None, None);
}
}
fn failed(&self, reason: &str) {
log::error!("Could not create session: {}", reason);
self.reply.err(reason);
self.session.reply_err(reason);
self.session.kill();
}
}
@ -137,60 +108,28 @@ impl SelectingDisplay {
let ei_session = builder.commit();
let starting = Rc::new(StartingRemoteDesktop {
session: self.session.clone(),
_request_obj: self.request_obj.clone(),
reply: self.reply.clone(),
request_obj: self.request_obj.clone(),
dpy: dpy.clone(),
ei_session,
});
self.session
.phase
.rd_phase
.set(RemoteDesktopPhase::Starting(starting.clone()));
starting.ei_session.owner.set(Some(starting.clone()));
dpy.remote_desktop_sessions.set(
dpy.sessions.set(
self.session.session_obj.path().to_owned(),
self.session.clone(),
);
}
}
impl RemoteDesktopSession {
pub(super) fn kill(&self) {
self.session_obj.emit_signal(&Closed);
self.state
.remote_desktop_sessions
.remove(self.session_obj.path());
match self.phase.set(RemoteDesktopPhase::Terminated) {
RemoteDesktopPhase::Init => {}
RemoteDesktopPhase::DevicesSelected => {}
RemoteDesktopPhase::Terminated => {}
RemoteDesktopPhase::Selecting(s) => {
s.reply.err("Session has been terminated");
for gui in s.guis.lock().drain_values() {
gui.kill(false);
}
}
RemoteDesktopPhase::Starting(s) => {
s.reply.err("Session has been terminated");
s.ei_session.con.remove_obj(s.ei_session.deref());
s.dpy
.remote_desktop_sessions
.remove(self.session_obj.path());
}
RemoteDesktopPhase::Started(s) => {
s.ei_session.con.remove_obj(s.ei_session.deref());
s.dpy
.remote_desktop_sessions
.remove(self.session_obj.path());
}
}
}
impl PortalSession {
fn dbus_select_devices(
self: &Rc<Self>,
_req: SelectDevices,
reply: PendingReply<SelectDevicesReply<'static>>,
) {
match self.phase.get() {
match self.rd_phase.get() {
RemoteDesktopPhase::Init => {}
_ => {
self.kill();
@ -198,15 +137,19 @@ impl RemoteDesktopSession {
return;
}
}
self.phase.set(RemoteDesktopPhase::DevicesSelected);
self.rd_phase.set(RemoteDesktopPhase::DevicesSelected);
reply.ok(&SelectDevicesReply {
response: PORTAL_SUCCESS,
results: Default::default(),
});
}
fn dbus_start(self: &Rc<Self>, req: Start<'_>, reply: PendingReply<StartReply<'static>>) {
match self.phase.get() {
fn dbus_start_remote_desktop(
self: &Rc<Self>,
req: Start<'_>,
reply: PendingReply<StartReply<'static>>,
) {
match self.rd_phase.get() {
RemoteDesktopPhase::DevicesSelected => {}
_ => {
self.kill();
@ -243,11 +186,12 @@ impl RemoteDesktopSession {
reply.err("There are no running displays");
return;
}
self.phase
self.start_reply
.set(Some(PortalSessionReply::RemoteDesktop(reply)));
self.rd_phase
.set(RemoteDesktopPhase::Selecting(Rc::new(SelectingDisplay {
session: self.clone(),
request_obj: Rc::new(request_obj),
reply: Rc::new(reply),
guis,
})));
}
@ -257,7 +201,7 @@ impl RemoteDesktopSession {
_req: ConnectToEIS,
reply: PendingReply<ConnectToEISReply>,
) {
let RemoteDesktopPhase::Started(started) = self.phase.get() else {
let RemoteDesktopPhase::Started(started) = self.rd_phase.get() else {
self.kill();
reply.err("Sources have already been selected");
return;
@ -305,10 +249,7 @@ fn dbus_create_session(
reply: PendingReply<CreateSessionReply<'static>>,
) {
log::info!("Create remote desktop session {:#?}", req);
if state
.remote_desktop_sessions
.contains(req.session_handle.0.deref())
{
if state.sessions.contains(req.session_handle.0.deref()) {
reply.err("Session already exists");
return;
}
@ -319,12 +260,15 @@ fn dbus_create_session(
return;
}
};
let session = Rc::new(RemoteDesktopSession {
let session = Rc::new(PortalSession {
_id: state.id(),
state: state.clone(),
pw_con: state.pw_con.clone(),
app: req.app_id.to_string(),
session_obj: obj,
phase: CloneCell::new(RemoteDesktopPhase::Init),
sc_phase: CloneCell::new(ScreencastPhase::Init),
rd_phase: CloneCell::new(RemoteDesktopPhase::Init),
start_reply: Default::default(),
});
{
use org::freedesktop::impl_::portal::session::*;
@ -336,7 +280,7 @@ fn dbus_create_session(
session.session_obj.set_property::<version>(Variant::U32(2));
}
state
.remote_desktop_sessions
.sessions
.set(req.session_handle.0.to_string(), session);
reply.ok(&CreateSessionReply {
response: PORTAL_SUCCESS,
@ -356,7 +300,7 @@ fn dbus_select_devices(
fn dbus_start(state: &Rc<PortalState>, req: Start, reply: PendingReply<StartReply<'static>>) {
if let Some(s) = get_session(state, &reply, &req.session_handle.0) {
s.dbus_start(req, reply);
s.dbus_start_remote_desktop(req, reply);
}
}
@ -374,8 +318,8 @@ fn get_session<T>(
state: &Rc<PortalState>,
reply: &PendingReply<T>,
handle: &str,
) -> Option<Rc<RemoteDesktopSession>> {
let res = state.remote_desktop_sessions.get(handle);
) -> Option<Rc<PortalSession>> {
let res = state.sessions.get(handle);
if res.is_none() {
let msg = format!("Remote desktop session `{}` does not exist", handle);
reply.err(&msg);

View file

@ -3,7 +3,7 @@ use {
ifs::wl_seat::{wl_pointer::PRESSED, BTN_LEFT},
portal::{
ptl_display::{PortalDisplay, PortalOutput, PortalSeat},
ptl_remote_desktop::{RemoteDesktopPhase, RemoteDesktopSession},
ptl_remote_desktop::{PortalSession, RemoteDesktopPhase},
ptr_gui::{
Align, Button, ButtonOwner, Flow, GuiElement, Label, Orientation, OverlayWindow,
OverlayWindowOwner,
@ -19,7 +19,7 @@ const H_MARGIN: f32 = 30.0;
const V_MARGIN: f32 = 20.0;
pub struct SelectionGui {
remote_desktop_session: Rc<RemoteDesktopSession>,
remote_desktop_session: Rc<PortalSession>,
dpy: Rc<PortalDisplay>,
surfaces: CopyHashMap<u32, Rc<SelectionGuiSurface>>,
}
@ -46,7 +46,7 @@ impl SelectionGui {
for surface in self.surfaces.lock().drain_values() {
surface.overlay.data.kill(false);
}
if let RemoteDesktopPhase::Selecting(s) = self.remote_desktop_session.phase.get() {
if let RemoteDesktopPhase::Selecting(s) = self.remote_desktop_session.rd_phase.get() {
s.guis.remove(&self.dpy.id);
if upwards && s.guis.is_empty() {
self.remote_desktop_session.kill();
@ -99,7 +99,7 @@ impl OverlayWindowOwner for SelectionGuiSurface {
}
impl SelectionGui {
pub fn new(ss: &Rc<RemoteDesktopSession>, dpy: &Rc<PortalDisplay>) -> Rc<Self> {
pub fn new(ss: &Rc<PortalSession>, dpy: &Rc<PortalDisplay>) -> Rc<Self> {
let gui = Rc::new(SelectionGui {
remote_desktop_session: ss.clone(),
dpy: dpy.clone(),
@ -130,7 +130,7 @@ impl ButtonOwner for StaticButton {
match self.role {
ButtonRole::Accept => {
log::info!("User has accepted the request");
let selecting = match self.surface.gui.remote_desktop_session.phase.get() {
let selecting = match self.surface.gui.remote_desktop_session.rd_phase.get() {
RemoteDesktopPhase::Selecting(selecting) => selecting,
_ => return,
};

View file

@ -3,9 +3,9 @@ mod screencast_gui;
use {
crate::{
allocator::{AllocatorError, BufferObject, BufferUsage, BO_USE_RENDERING},
dbus::{prelude::Variant, DbusObject, DictEntry, DynamicType, PendingReply},
dbus::{prelude::Variant, DbusObject, DictEntry, PendingReply},
format::{Format, XRGB8888},
ifs::jay_screencast::CLIENT_BUFFERS_SINCE,
ifs::{jay_compositor::GET_TOPLEVEL_SINCE, jay_screencast::CLIENT_BUFFERS_SINCE},
pipewire::{
pw_con::PwCon,
pw_ifs::pw_client_node::{
@ -21,14 +21,16 @@ use {
},
portal::{
ptl_display::{PortalDisplay, PortalDisplayId, PortalOutput},
ptl_remote_desktop::RemoteDesktopPhase,
ptl_screencast::screencast_gui::SelectionGui,
ptl_session::{PortalSession, PortalSessionReply},
PortalState, PORTAL_SUCCESS,
},
utils::{
clonecell::{CloneCell, UnsafeCellCloneSafe},
copyhashmap::CopyHashMap,
errorfmt::ErrorFmt,
hash_map_ext::HashMapExt,
opaque::Opaque,
},
video::{dmabuf::DmaBuf, Modifier, LINEAR_MODIFIER},
wire::jay_screencast::Ready,
@ -39,7 +41,7 @@ use {
CreateSession, CreateSessionReply, SelectSources, SelectSourcesReply, Start,
StartReply,
},
session::{CloseReply as SessionCloseReply, Closed},
session::CloseReply as SessionCloseReply,
},
},
wl_usr::usr_ifs::{
@ -54,6 +56,7 @@ use {
usr_wl_buffer::UsrWlBuffer,
},
},
serde::{Deserialize, Serialize},
std::{
borrow::Cow,
cell::{Cell, RefCell},
@ -64,20 +67,10 @@ use {
thiserror::Error,
};
shared_ids!(ScreencastSessionId);
pub struct ScreencastSession {
_id: ScreencastSessionId,
state: Rc<PortalState>,
pw_con: Rc<PwCon>,
pub app: String,
session_obj: DbusObject,
pub phase: CloneCell<ScreencastPhase>,
}
#[derive(Clone)]
pub enum ScreencastPhase {
Init,
SourcesSelected,
SourcesSelected(Rc<SourcesSelectedScreencast>),
Selecting(Rc<SelectingScreencast>),
SelectingWindow(Rc<SelectingWindowScreencast>),
SelectingWorkspace(Rc<SelectingWorkspaceScreencast>),
@ -88,22 +81,27 @@ pub enum ScreencastPhase {
unsafe impl UnsafeCellCloneSafe for ScreencastPhase {}
pub struct SourcesSelectedScreencast {
pub restore_data: Cell<Option<Result<RestoreData, RestoreError>>>,
}
#[derive(Clone)]
pub struct SelectingScreencastCore {
pub session: Rc<ScreencastSession>,
pub session: Rc<PortalSession>,
pub request_obj: Rc<DbusObject>,
pub reply: Rc<PendingReply<StartReply<'static>>>,
}
pub struct SelectingScreencast {
pub core: SelectingScreencastCore,
pub guis: CopyHashMap<PortalDisplayId, Rc<SelectionGui>>,
pub restore_data: Cell<Option<RestoreData>>,
}
pub struct SelectingWindowScreencast {
pub core: SelectingScreencastCore,
pub dpy: Rc<PortalDisplay>,
pub selector: Rc<UsrJaySelectToplevel>,
pub restoring: bool,
}
pub struct SelectingWorkspaceScreencast {
@ -113,9 +111,8 @@ pub struct SelectingWorkspaceScreencast {
}
pub struct StartingScreencast {
pub session: Rc<ScreencastSession>,
pub session: Rc<PortalSession>,
pub _request_obj: Rc<DbusObject>,
pub reply: Rc<PendingReply<StartReply<'static>>>,
pub node: Rc<PwClientNode>,
pub dpy: Rc<PortalDisplay>,
pub target: ScreencastTarget,
@ -123,26 +120,26 @@ pub struct StartingScreencast {
pub enum ScreencastTarget {
Output(Rc<PortalOutput>),
Workspace(Rc<PortalOutput>, Rc<UsrJayWorkspace>),
Workspace(Rc<PortalOutput>, Rc<UsrJayWorkspace>, bool),
Toplevel(Rc<UsrJayToplevel>),
}
pub struct StartedScreencast {
session: Rc<ScreencastSession>,
node: Rc<PwClientNode>,
port: Rc<PwClientNodePort>,
buffer_objects: RefCell<Vec<Rc<dyn BufferObject>>>,
buffers: RefCell<Vec<DmaBuf>>,
pending_buffers: RefCell<Vec<Rc<UsrLinuxBufferParams>>>,
buffers_valid: Cell<bool>,
dpy: Rc<PortalDisplay>,
jay_screencast: Rc<UsrJayScreencast>,
port_buffer_valid: Cell<bool>,
fixated: Cell<bool>,
format: Cell<&'static Format>,
modifier: Cell<Modifier>,
width: Cell<i32>,
height: Cell<i32>,
pub session: Rc<PortalSession>,
pub node: Rc<PwClientNode>,
pub port: Rc<PwClientNodePort>,
pub buffer_objects: RefCell<Vec<Rc<dyn BufferObject>>>,
pub buffers: RefCell<Vec<DmaBuf>>,
pub pending_buffers: RefCell<Vec<Rc<UsrLinuxBufferParams>>>,
pub buffers_valid: Cell<bool>,
pub dpy: Rc<PortalDisplay>,
pub jay_screencast: Rc<UsrJayScreencast>,
pub port_buffer_valid: Cell<bool>,
pub fixated: Cell<bool>,
pub format: Cell<&'static Format>,
pub modifier: Cell<Modifier>,
pub width: Cell<i32>,
pub height: Cell<i32>,
}
bitflags! {
@ -163,25 +160,17 @@ bitflags! {
impl PwClientNodeOwner for StartingScreencast {
fn bound_id(&self, node_id: u32) {
{
let inner_type = DynamicType::DictEntry(
Box::new(DynamicType::String),
Box::new(DynamicType::Variant),
let output = match &self.target {
ScreencastTarget::Output(o) => Some(o),
ScreencastTarget::Workspace(o, _, _) => Some(o),
ScreencastTarget::Toplevel(_) => None,
};
let mapping_id = output.and_then(|o| o.wl.name.borrow().clone());
self.session.send_start_reply(
Some(node_id),
create_restore_data(&self.dpy, &self.target),
mapping_id.as_deref(),
);
let kt = DynamicType::Struct(vec![
DynamicType::U32,
DynamicType::Array(Box::new(inner_type.clone())),
]);
let variants = &[DictEntry {
key: "streams".into(),
value: Variant::Array(
kt,
vec![Variant::U32(node_id), Variant::Array(inner_type, vec![])],
),
}];
self.reply.ok(&StartReply {
response: PORTAL_SUCCESS,
results: Cow::Borrowed(variants),
});
}
let mut supported_formats = PwClientNodePortSupportedFormats {
media_type: Some(SPA_MEDIA_TYPE_video),
@ -221,7 +210,7 @@ impl PwClientNodeOwner for StartingScreencast {
jsc.set_output(&o.jay);
jsc.set_allow_all_workspaces(true);
}
ScreencastTarget::Workspace(o, ws) => {
ScreencastTarget::Workspace(o, ws, _) => {
jsc.set_output(&o.jay);
jsc.allow_workspace(ws);
}
@ -231,9 +220,10 @@ impl PwClientNodeOwner for StartingScreencast {
jsc.configure();
match &self.target {
ScreencastTarget::Output(_) => {}
ScreencastTarget::Workspace(_, w) => {
ScreencastTarget::Workspace(_, w, true) => {
self.dpy.con.remove_obj(&**w);
}
ScreencastTarget::Workspace(_, _, false) => {}
ScreencastTarget::Toplevel(t) => {
self.dpy.con.remove_obj(&**t);
}
@ -256,7 +246,7 @@ impl PwClientNodeOwner for StartingScreencast {
height: Cell::new(1),
});
self.session
.phase
.sc_phase
.set(ScreencastPhase::Started(started.clone()));
started.jay_screencast.owner.set(Some(started.clone()));
self.node.owner.set(Some(started.clone()));
@ -409,7 +399,11 @@ impl StartedScreencast {
impl SelectingScreencastCore {
pub fn starting(&self, dpy: &Rc<PortalDisplay>, target: ScreencastTarget) {
let node = self.session.pw_con.create_client_node(&[
let Some(pw_con) = &self.session.pw_con else {
self.session.kill();
return;
};
let node = pw_con.create_client_node(&[
("media.class".to_string(), "Video/Source".to_string()),
("node.name".to_string(), "jay-desktop-portal".to_string()),
("node.driver".to_string(), "true".to_string()),
@ -417,75 +411,28 @@ impl SelectingScreencastCore {
let starting = Rc::new(StartingScreencast {
session: self.session.clone(),
_request_obj: self.request_obj.clone(),
reply: self.reply.clone(),
node,
dpy: dpy.clone(),
target,
});
self.session
.phase
.sc_phase
.set(ScreencastPhase::Starting(starting.clone()));
starting.node.owner.set(Some(starting.clone()));
dpy.screencasts.set(
dpy.sessions.set(
self.session.session_obj.path().to_owned(),
self.session.clone(),
);
}
}
impl ScreencastSession {
pub(super) fn kill(&self) {
self.session_obj.emit_signal(&Closed);
self.state.screencasts.remove(self.session_obj.path());
match self.phase.set(ScreencastPhase::Terminated) {
ScreencastPhase::Init => {}
ScreencastPhase::SourcesSelected => {}
ScreencastPhase::Terminated => {}
ScreencastPhase::Selecting(s) => {
s.core.reply.err("Session has been terminated");
for gui in s.guis.lock().drain_values() {
gui.kill(false);
}
}
ScreencastPhase::SelectingWindow(s) => {
s.dpy.con.remove_obj(&*s.selector);
s.core.reply.err("Session has been terminated");
}
ScreencastPhase::SelectingWorkspace(s) => {
s.dpy.con.remove_obj(&*s.selector);
s.core.reply.err("Session has been terminated");
}
ScreencastPhase::Starting(s) => {
s.reply.err("Session has been terminated");
s.node.con.destroy_obj(s.node.deref());
s.dpy.screencasts.remove(self.session_obj.path());
match &s.target {
ScreencastTarget::Output(_) => {}
ScreencastTarget::Workspace(_, w) => {
s.dpy.con.remove_obj(&**w);
}
ScreencastTarget::Toplevel(t) => {
s.dpy.con.remove_obj(&**t);
}
}
}
ScreencastPhase::Started(s) => {
s.jay_screencast.con.remove_obj(s.jay_screencast.deref());
s.node.con.destroy_obj(s.node.deref());
s.dpy.screencasts.remove(self.session_obj.path());
for buffer in s.pending_buffers.borrow_mut().drain(..) {
s.dpy.con.remove_obj(&*buffer);
}
}
}
}
impl PortalSession {
fn dbus_select_sources(
self: &Rc<Self>,
_req: SelectSources,
req: SelectSources,
reply: PendingReply<SelectSourcesReply<'static>>,
) {
match self.phase.get() {
match self.sc_phase.get() {
ScreencastPhase::Init => {}
_ => {
self.kill();
@ -493,24 +440,32 @@ impl ScreencastSession {
return;
}
}
self.phase.set(ScreencastPhase::SourcesSelected);
self.sc_phase.set(ScreencastPhase::SourcesSelected(Rc::new(
SourcesSelectedScreencast {
restore_data: Cell::new(get_restore_data(&req)),
},
)));
reply.ok(&SelectSourcesReply {
response: PORTAL_SUCCESS,
results: Default::default(),
});
}
fn dbus_start(self: &Rc<Self>, req: Start<'_>, reply: PendingReply<StartReply<'static>>) {
match self.phase.get() {
ScreencastPhase::SourcesSelected => {}
fn dbus_start_screencast(
self: &Rc<Self>,
req: Start<'_>,
reply: PendingReply<StartReply<'static>>,
) {
let restore_data = match self.sc_phase.get() {
ScreencastPhase::SourcesSelected(s) => s.restore_data.take(),
_ => {
self.kill();
reply.err("Session is not in the correct phase for starting");
return;
}
}
};
let request_obj = match self.state.dbus.add_object(req.handle.to_string()) {
Ok(r) => r,
Ok(r) => Rc::new(r),
Err(_) => {
self.kill();
reply.err("Request handle is not unique");
@ -527,27 +482,141 @@ impl ScreencastSession {
}
});
}
self.start_reply
.set(Some(PortalSessionReply::ScreenCast(reply)));
self.screencast_restore(&request_obj, restore_data, None);
}
fn start_interactive_selection(
self: &Rc<Self>,
request_obj: &Rc<DbusObject>,
restore_data: Option<RestoreData>,
) {
let guis = CopyHashMap::new();
for dpy in self.state.displays.lock().values() {
if dpy.outputs.len() > 0 {
guis.set(dpy.id, SelectionGui::new(self, dpy));
guis.set(dpy.id, SelectionGui::new(self, dpy, restore_data.is_some()));
}
}
if guis.is_empty() {
self.kill();
reply.err("There are no running displays");
self.reply_err("There are no running displays");
return;
}
self.phase
self.sc_phase
.set(ScreencastPhase::Selecting(Rc::new(SelectingScreencast {
core: SelectingScreencastCore {
session: self.clone(),
request_obj: Rc::new(request_obj),
reply: Rc::new(reply),
request_obj: request_obj.clone(),
},
guis,
restore_data: Cell::new(restore_data),
})));
}
pub fn screencast_restore(
self: &Rc<Self>,
request_obj: &Rc<DbusObject>,
restore_data: Option<Result<RestoreData, RestoreError>>,
display: Option<Rc<PortalDisplay>>,
) {
if let Some(rd) = restore_data {
if let Err(e) = self.try_restore(&request_obj, rd, display) {
log::error!("Could not restore session: {}", ErrorFmt(e));
} else {
return;
}
}
self.start_interactive_selection(&request_obj, None);
}
fn try_restore(
self: &Rc<Self>,
request_obj: &Rc<DbusObject>,
restore_data: Result<RestoreData, RestoreError>,
display: Option<Rc<PortalDisplay>>,
) -> Result<(), RestoreError> {
let rd = restore_data?;
let dpy = if let Some(dpy) = display {
dpy
} else {
let dpy = self
.state
.displays
.lock()
.values()
.find(|d| d.unique_id == rd.display)
.cloned();
match dpy {
Some(dpy) => dpy,
_ => {
if self.state.displays.len() == 0 {
return Err(RestoreError::UnknownDisplay);
} else if self.state.displays.len() == 1 {
self.state.displays.lock().values().next().unwrap().clone()
} else {
self.start_interactive_selection(&request_obj, Some(rd));
return Ok(());
}
}
}
};
let start = |target: ScreencastTarget| {
SelectingScreencastCore {
session: self.clone(),
request_obj: request_obj.clone(),
}
.starting(&dpy, target);
};
match &rd.ty {
RestoreDataType::Output(d) => {
let output = dpy
.outputs
.lock()
.values()
.find(|o| o.wl.name.borrow().as_ref() == Some(&d.name))
.cloned();
let Some(output) = output else {
return Err(RestoreError::UnknownOutput);
};
start(ScreencastTarget::Output(output));
}
RestoreDataType::Workspace(ws) => {
let ws = dpy
.workspaces
.lock()
.values()
.find(|w| w.name.borrow().as_ref() == Some(&ws.name))
.cloned();
let Some(ws) = ws else {
return Err(RestoreError::UnknownWorkspace);
};
let Some(output) = dpy.outputs.get(&ws.output.get()) else {
return Err(RestoreError::UnknownOutput);
};
start(ScreencastTarget::Workspace(output, ws, false));
}
RestoreDataType::Toplevel(d) => {
if dpy.jc.version < GET_TOPLEVEL_SINCE {
return Err(RestoreError::GetToplevel);
}
let selector = dpy.jc.get_toplevel(&d.id);
let selecting = Rc::new(SelectingWindowScreencast {
core: SelectingScreencastCore {
session: self.clone(),
request_obj: request_obj.clone(),
},
dpy: dpy.clone(),
selector: selector.clone(),
restoring: true,
});
selector.owner.set(Some(selecting.clone()));
self.sc_phase
.set(ScreencastPhase::SelectingWindow(selecting));
}
}
Ok(())
}
}
impl UsrJayScreencastOwner for StartedScreencast {
@ -695,7 +764,7 @@ fn dbus_create_session(
reply: PendingReply<CreateSessionReply<'static>>,
) {
log::info!("Create Session {:#?}", req);
if state.screencasts.contains(req.session_handle.0.deref()) {
if state.sessions.contains(req.session_handle.0.deref()) {
reply.err("Session already exists");
return;
}
@ -706,13 +775,15 @@ fn dbus_create_session(
return;
}
};
let session = Rc::new(ScreencastSession {
let session = Rc::new(PortalSession {
_id: state.id(),
state: state.clone(),
pw_con: pw_con.clone(),
pw_con: Some(pw_con.clone()),
app: req.app_id.to_string(),
session_obj: obj,
phase: CloneCell::new(ScreencastPhase::Init),
sc_phase: CloneCell::new(ScreencastPhase::Init),
rd_phase: CloneCell::new(RemoteDesktopPhase::Init),
start_reply: Default::default(),
});
{
use org::freedesktop::impl_::portal::session::*;
@ -724,7 +795,7 @@ fn dbus_create_session(
session.session_obj.set_property::<version>(Variant::U32(4));
}
state
.screencasts
.sessions
.set(req.session_handle.0.to_string(), session);
reply.ok(&CreateSessionReply {
response: PORTAL_SUCCESS,
@ -744,7 +815,7 @@ fn dbus_select_sources(
fn dbus_start(state: &Rc<PortalState>, req: Start, reply: PendingReply<StartReply<'static>>) {
if let Some(s) = get_session(state, &reply, &req.session_handle.0) {
s.dbus_start(req, reply);
s.dbus_start_screencast(req, reply);
}
}
@ -752,11 +823,130 @@ fn get_session<T>(
state: &Rc<PortalState>,
reply: &PendingReply<T>,
handle: &str,
) -> Option<Rc<ScreencastSession>> {
let res = state.screencasts.get(handle);
) -> Option<Rc<PortalSession>> {
let res = state.sessions.get(handle);
if res.is_none() {
let msg = format!("Screencast session `{}` does not exist", handle);
reply.err(&msg);
}
res
}
fn create_restore_data(dpy: &PortalDisplay, rd: &ScreencastTarget) -> Option<Variant<'static>> {
let rd = RestoreData {
display: dpy.unique_id,
ty: match rd {
ScreencastTarget::Output(o) => RestoreDataType::Output(RestoreDataOutput {
name: o.wl.name.borrow().clone()?,
}),
ScreencastTarget::Workspace(_, w, _) => {
RestoreDataType::Workspace(RestoreDataWorkspace {
name: w.name.borrow().clone()?,
})
}
ScreencastTarget::Toplevel(tl) => RestoreDataType::Toplevel(RestoreDataToplevel {
id: tl.toplevel_id.borrow().clone()?,
}),
},
};
Some(Variant::Struct(vec![
Variant::String("Jay".into()),
Variant::U32(1),
Variant::Variant(Box::new(Variant::String(
serde_json::to_string(&rd).unwrap().into(),
))),
]))
}
#[derive(Debug, Error)]
pub enum RestoreError {
#[error("DBus restore data is not a struct")]
NotAStruct,
#[error("DBus restore data is not a struct with 3 fields")]
NotLen3,
#[error("DBus restore data first field is not a string")]
FirstNotString,
#[error("DBus restore data second field is not a u32")]
SecondNotU32,
#[error("DBus restore data third field is not a variant")]
ThirdNotVariant,
#[error("DBus restore data third field is not a string")]
ThirdNotString,
#[error("DBus restore data is not for Jay")]
NotJay,
#[error("DBus restore data is not version 1")]
NotVersion1,
#[error("DBus restore data could not be deserialized")]
Parse(#[source] serde_json::Error),
#[error("The display no longer exists")]
UnknownDisplay,
#[error("The output no longer exists")]
UnknownOutput,
#[error("The workspace no longer exists")]
UnknownWorkspace,
#[error("The display does not support toplevel restoration")]
GetToplevel,
}
fn get_restore_data(req: &SelectSources) -> Option<Result<RestoreData, RestoreError>> {
let restore_data = req.options.iter().find(|n| n.key == "restore_data")?;
Some(get_restore_data_(restore_data))
}
fn get_restore_data_(
restore_data: &DictEntry<Cow<str>, Variant>,
) -> Result<RestoreData, RestoreError> {
let Variant::Struct(s) = &restore_data.value else {
return Err(RestoreError::NotAStruct);
};
if s.len() != 3 {
return Err(RestoreError::NotLen3);
}
let Variant::String(compositor) = &s[0] else {
return Err(RestoreError::FirstNotString);
};
let Variant::U32(version) = &s[1] else {
return Err(RestoreError::SecondNotU32);
};
let Variant::Variant(restore_data) = &s[2] else {
return Err(RestoreError::ThirdNotVariant);
};
let Variant::String(restore_data) = &**restore_data else {
return Err(RestoreError::ThirdNotString);
};
if compositor != "Jay" {
return Err(RestoreError::NotJay);
}
if *version != 1 {
return Err(RestoreError::NotVersion1);
}
serde_json::from_str(restore_data).map_err(RestoreError::Parse)
}
#[derive(Serialize, Deserialize)]
pub struct RestoreData {
display: Opaque,
ty: RestoreDataType,
}
#[derive(Serialize, Deserialize)]
enum RestoreDataType {
Output(RestoreDataOutput),
Workspace(RestoreDataWorkspace),
Toplevel(RestoreDataToplevel),
}
#[derive(Serialize, Deserialize)]
struct RestoreDataOutput {
name: String,
}
#[derive(Serialize, Deserialize)]
struct RestoreDataWorkspace {
name: String,
}
#[derive(Serialize, Deserialize)]
struct RestoreDataToplevel {
id: String,
}

View file

@ -4,7 +4,7 @@ use {
portal::{
ptl_display::{PortalDisplay, PortalOutput, PortalSeat},
ptl_screencast::{
ScreencastPhase, ScreencastSession, ScreencastTarget, SelectingWindowScreencast,
PortalSession, ScreencastPhase, ScreencastTarget, SelectingWindowScreencast,
SelectingWorkspaceScreencast,
},
ptr_gui::{
@ -27,7 +27,7 @@ const H_MARGIN: f32 = 30.0;
const V_MARGIN: f32 = 20.0;
pub struct SelectionGui {
screencast_session: Rc<ScreencastSession>,
screencast_session: Rc<PortalSession>,
dpy: Rc<PortalDisplay>,
surfaces: CopyHashMap<u32, Rc<SelectionGuiSurface>>,
}
@ -45,6 +45,7 @@ struct StaticButton {
#[derive(Copy, Clone, Eq, PartialEq)]
enum ButtonRole {
Restore,
Accept,
SelectWorkspace,
SelectWindow,
@ -56,7 +57,7 @@ impl SelectionGui {
for surface in self.surfaces.lock().drain_values() {
surface.overlay.data.kill(false);
}
if let ScreencastPhase::Selecting(s) = self.screencast_session.phase.get() {
if let ScreencastPhase::Selecting(s) = self.screencast_session.sc_phase.get() {
s.guis.remove(&self.dpy.id);
if upwards && s.guis.is_empty() {
self.screencast_session.kill();
@ -65,7 +66,7 @@ impl SelectionGui {
}
}
fn create_accept_gui(surface: &Rc<SelectionGuiSurface>) -> Rc<dyn GuiElement> {
fn create_accept_gui(surface: &Rc<SelectionGuiSurface>, for_restore: bool) -> Rc<dyn GuiElement> {
let app = &surface.gui.screencast_session.app;
let text = if app.is_empty() {
format!("An application wants to capture the screen")
@ -74,11 +75,13 @@ fn create_accept_gui(surface: &Rc<SelectionGuiSurface>) -> Rc<dyn GuiElement> {
};
let label = Rc::new(Label::default());
*label.text.borrow_mut() = text;
let restore_button = static_button(surface, ButtonRole::Restore, "Restore Session");
let accept_button = static_button(surface, ButtonRole::Accept, "Share This Output");
let workspace_button = static_button(surface, ButtonRole::SelectWorkspace, "Share A Workspace");
let window_button = static_button(surface, ButtonRole::SelectWindow, "Share A Window");
let reject_button = static_button(surface, ButtonRole::Reject, "Reject");
for button in [
&restore_button,
&accept_button,
&workspace_button,
&window_button,
@ -88,6 +91,10 @@ fn create_accept_gui(surface: &Rc<SelectionGuiSurface>) -> Rc<dyn GuiElement> {
button.border.set(2.0);
button.padding.set(5.0);
}
restore_button.bg_color.set(Color::from_rgb(170, 170, 200));
restore_button
.bg_hover_color
.set(Color::from_rgb(170, 170, 255));
for button in [&accept_button, &workspace_button, &window_button] {
button.bg_color.set(Color::from_rgb(170, 200, 170));
button.bg_hover_color.set(Color::from_rgb(170, 255, 170));
@ -101,7 +108,11 @@ fn create_accept_gui(surface: &Rc<SelectionGuiSurface>) -> Rc<dyn GuiElement> {
flow.cross_align.set(Align::Center);
flow.in_margin.set(V_MARGIN);
flow.cross_margin.set(H_MARGIN);
let mut elements: Vec<Rc<dyn GuiElement>> = vec![label, accept_button];
let mut elements: Vec<Rc<dyn GuiElement>> = vec![label];
if for_restore {
elements.push(restore_button);
}
elements.push(accept_button);
if surface.gui.dpy.jc.caps.select_workspace.get() {
elements.push(workspace_button);
}
@ -124,7 +135,7 @@ impl OverlayWindowOwner for SelectionGuiSurface {
}
impl SelectionGui {
pub fn new(ss: &Rc<ScreencastSession>, dpy: &Rc<PortalDisplay>) -> Rc<Self> {
pub fn new(ss: &Rc<PortalSession>, dpy: &Rc<PortalDisplay>, for_restore: bool) -> Rc<Self> {
let gui = Rc::new(SelectionGui {
screencast_session: ss.clone(),
dpy: dpy.clone(),
@ -136,7 +147,7 @@ impl SelectionGui {
output: output.clone(),
overlay: OverlayWindow::new(output),
});
let element = create_accept_gui(&sgs);
let element = create_accept_gui(&sgs, for_restore);
sgs.overlay.data.content.set(Some(element));
gui.dpy
.windows
@ -153,9 +164,12 @@ impl ButtonOwner for StaticButton {
return;
}
match self.role {
ButtonRole::Accept | ButtonRole::SelectWorkspace | ButtonRole::SelectWindow => {
ButtonRole::Restore
| ButtonRole::Accept
| ButtonRole::SelectWorkspace
| ButtonRole::SelectWindow => {
log::info!("User has accepted the request");
let selecting = match self.surface.gui.screencast_session.phase.get() {
let selecting = match self.surface.gui.screencast_session.sc_phase.get() {
ScreencastPhase::Selecting(selecting) => selecting,
_ => return,
};
@ -163,7 +177,13 @@ impl ButtonOwner for StaticButton {
gui.kill(false);
}
let dpy = &self.surface.output.dpy;
if self.role == ButtonRole::Accept {
if self.role == ButtonRole::Restore {
selecting.core.session.screencast_restore(
&selecting.core.request_obj,
selecting.restore_data.take().map(Ok),
Some(self.surface.gui.dpy.clone()),
);
} else if self.role == ButtonRole::Accept {
selecting
.core
.starting(dpy, ScreencastTarget::Output(self.surface.output.clone()));
@ -178,7 +198,7 @@ impl ButtonOwner for StaticButton {
self.surface
.gui
.screencast_session
.phase
.sc_phase
.set(ScreencastPhase::SelectingWorkspace(selecting));
} else {
let selector = dpy.jc.select_toplevel(&seat.wl);
@ -186,12 +206,13 @@ impl ButtonOwner for StaticButton {
core: selecting.core.clone(),
dpy: dpy.clone(),
selector: selector.clone(),
restoring: false,
});
selector.owner.set(Some(selecting.clone()));
self.surface
.gui
.screencast_session
.phase
.sc_phase
.set(ScreencastPhase::SelectingWindow(selecting));
}
}
@ -206,11 +227,18 @@ impl ButtonOwner for StaticButton {
impl UsrJaySelectToplevelOwner for SelectingWindowScreencast {
fn done(&self, tl: Option<Rc<UsrJayToplevel>>) {
let Some(tl) = tl else {
if self.restoring {
log::warn!("Could not restore session because toplevel no longer exists");
self.core
.session
.start_interactive_selection(&self.core.request_obj, None);
return;
}
log::info!("User has aborted the selection");
self.core.session.kill();
return;
};
match self.core.session.phase.get() {
match self.core.session.sc_phase.get() {
ScreencastPhase::SelectingWindow(s) => {
self.dpy.con.remove_obj(&*s.selector);
}
@ -232,7 +260,7 @@ impl UsrJaySelectWorkspaceOwner for SelectingWorkspaceScreencast {
self.core.session.kill();
return;
};
match self.core.session.phase.get() {
match self.core.session.sc_phase.get() {
ScreencastPhase::SelectingWorkspace(s) => {
self.dpy.con.remove_obj(&*s.selector);
}
@ -252,7 +280,7 @@ impl UsrJaySelectWorkspaceOwner for SelectingWorkspaceScreencast {
}
};
self.core
.starting(&self.dpy, ScreencastTarget::Workspace(output, ws));
.starting(&self.dpy, ScreencastTarget::Workspace(output, ws, true));
}
}

169
src/portal/ptl_session.rs Normal file
View file

@ -0,0 +1,169 @@
use {
crate::{
dbus::{prelude::Variant, DbusObject, DictEntry, DynamicType, PendingReply, FALSE},
pipewire::pw_con::PwCon,
portal::{
ptl_remote_desktop::{DeviceTypes, RemoteDesktopPhase},
ptl_screencast::{ScreencastPhase, ScreencastTarget},
PortalState, PORTAL_SUCCESS,
},
utils::{clonecell::CloneCell, hash_map_ext::HashMapExt},
wire_dbus::org::freedesktop::impl_::portal::{
remote_desktop::StartReply as RdStartReply, screen_cast::StartReply as ScStartReply,
session::Closed,
},
},
std::{borrow::Cow, cell::Cell, ops::Deref, rc::Rc},
};
shared_ids!(SessionId);
pub struct PortalSession {
pub _id: SessionId,
pub state: Rc<PortalState>,
pub pw_con: Option<Rc<PwCon>>,
pub app: String,
pub session_obj: DbusObject,
pub sc_phase: CloneCell<ScreencastPhase>,
pub rd_phase: CloneCell<RemoteDesktopPhase>,
pub start_reply: Cell<Option<PortalSessionReply>>,
}
pub enum PortalSessionReply {
RemoteDesktop(PendingReply<RdStartReply<'static>>),
ScreenCast(PendingReply<ScStartReply<'static>>),
}
impl PortalSession {
pub(super) fn kill(&self) {
self.session_obj.emit_signal(&Closed);
self.state.sessions.remove(self.session_obj.path());
self.reply_err("Session has been terminated");
match self.rd_phase.set(RemoteDesktopPhase::Terminated) {
RemoteDesktopPhase::Init => {}
RemoteDesktopPhase::DevicesSelected => {}
RemoteDesktopPhase::Terminated => {}
RemoteDesktopPhase::Selecting(s) => {
for gui in s.guis.lock().drain_values() {
gui.kill(false);
}
}
RemoteDesktopPhase::Starting(s) => {
s.ei_session.con.remove_obj(s.ei_session.deref());
s.dpy.sessions.remove(self.session_obj.path());
}
RemoteDesktopPhase::Started(s) => {
s.ei_session.con.remove_obj(s.ei_session.deref());
s.dpy.sessions.remove(self.session_obj.path());
}
}
match self.sc_phase.set(ScreencastPhase::Terminated) {
ScreencastPhase::Init => {}
ScreencastPhase::SourcesSelected(_) => {}
ScreencastPhase::Terminated => {}
ScreencastPhase::Selecting(s) => {
for gui in s.guis.lock().drain_values() {
gui.kill(false);
}
}
ScreencastPhase::SelectingWindow(s) => {
s.dpy.con.remove_obj(&*s.selector);
}
ScreencastPhase::SelectingWorkspace(s) => {
s.dpy.con.remove_obj(&*s.selector);
}
ScreencastPhase::Starting(s) => {
s.node.con.destroy_obj(s.node.deref());
s.dpy.sessions.remove(self.session_obj.path());
match &s.target {
ScreencastTarget::Output(_) => {}
ScreencastTarget::Workspace(_, w, true) => {
s.dpy.con.remove_obj(&**w);
}
ScreencastTarget::Workspace(_, _, false) => {}
ScreencastTarget::Toplevel(t) => {
s.dpy.con.remove_obj(&**t);
}
}
}
ScreencastPhase::Started(s) => {
s.jay_screencast.con.remove_obj(s.jay_screencast.deref());
s.node.con.destroy_obj(s.node.deref());
s.dpy.sessions.remove(self.session_obj.path());
for buffer in s.pending_buffers.borrow_mut().drain(..) {
s.dpy.con.remove_obj(&*buffer);
}
}
}
}
pub(super) fn send_start_reply(
&self,
pw_node_id: Option<u32>,
restore_data: Option<Variant<'static>>,
mapping_id: Option<&str>,
) {
let inner_type = DynamicType::DictEntry(
Box::new(DynamicType::String),
Box::new(DynamicType::Variant),
);
let kt = DynamicType::Struct(vec![
DynamicType::U32,
DynamicType::Array(Box::new(inner_type.clone())),
]);
let mut streams = vec![];
if let Some(node_id) = pw_node_id {
streams = vec![Variant::U32(node_id), Variant::Array(inner_type, vec![])];
}
let mut variants = vec![
DictEntry {
key: "devices".into(),
value: Variant::U32(DeviceTypes::all().0),
},
DictEntry {
key: "clipboard_enabled".into(),
value: Variant::Bool(FALSE),
},
DictEntry {
key: "streams".into(),
value: Variant::Array(kt, streams),
},
];
if let Some(rd) = restore_data {
variants.push(DictEntry {
key: "restore_data".into(),
value: rd,
});
}
if let Some(mapping_id) = mapping_id {
variants.push(DictEntry {
key: "mapping_id".into(),
value: Variant::String(mapping_id.into()),
});
}
if let Some(reply) = self.start_reply.take() {
match reply {
PortalSessionReply::RemoteDesktop(reply) => {
reply.ok(&RdStartReply {
response: PORTAL_SUCCESS,
results: Cow::Borrowed(&variants),
});
}
PortalSessionReply::ScreenCast(reply) => {
reply.ok(&ScStartReply {
response: PORTAL_SUCCESS,
results: Cow::Borrowed(&variants),
});
}
}
}
}
pub(super) fn reply_err(&self, err: &str) {
if let Some(reply) = self.start_reply.take() {
match reply {
PortalSessionReply::RemoteDesktop(r) => r.err(err),
PortalSessionReply::ScreenCast(r) => r.err(err),
}
}
}
}

View file

@ -661,6 +661,7 @@ impl WindowData {
buf.free.set(false);
self.surface.attach(&buf.wl);
self.surface.damage();
self.surface.commit();
}

View file

@ -78,6 +78,7 @@ use {
clonecell::CloneCell, copyhashmap::CopyHashMap, errorfmt::ErrorFmt, fdcloser::FdCloser,
hash_map_ext::HashMapExt, linkedlist::LinkedList, numcell::NumCell, queue::AsyncQueue,
refcounted::RefCounted, run_toplevel::RunToplevel,
toplevel_identifier::ToplevelIdentifier,
},
video::{
dmabuf::DmaBufIds,
@ -107,7 +108,7 @@ use {
mem,
num::Wrapping,
ops::DerefMut,
rc::Rc,
rc::{Rc, Weak},
sync::Arc,
time::Duration,
},
@ -220,6 +221,7 @@ pub struct State {
pub cpu_worker: Rc<CpuWorker>,
pub ui_drag_enabled: Cell<bool>,
pub ui_drag_threshold_squared: Cell<i32>,
pub toplevels: CopyHashMap<ToplevelIdentifier, Weak<dyn ToplevelNode>>,
}
// impl Drop for State {
@ -875,6 +877,7 @@ impl State {
self.ei_acceptor_future.take();
self.ei_clients.clear();
self.slow_ei_clients.clear();
self.toplevels.clear();
}
pub fn damage_hardware_cursors(&self, render: bool) {

View file

@ -213,7 +213,7 @@ impl ContainerNode {
let child_node_ref = child_node.clone();
let mut child_nodes = AHashMap::new();
child_nodes.insert(child.node_id(), child_node);
let slf = Rc::new(Self {
let slf = Rc::new_cyclic(|weak| Self {
id: state.node_ids.next(),
split: Cell::new(split),
mono_child: CloneCell::new(None),
@ -238,7 +238,7 @@ impl ContainerNode {
state: state.clone(),
render_data: Default::default(),
scroller: Default::default(),
toplevel_data: ToplevelData::new(state, Default::default(), None),
toplevel_data: ToplevelData::new(state, Default::default(), None, weak),
attention_requests: Default::default(),
});
child.tl_set_parent(slf.clone());

View file

@ -22,7 +22,7 @@ use {
std::{
cell::{Cell, RefCell},
ops::Deref,
rc::Rc,
rc::{Rc, Weak},
sync::Arc,
},
};
@ -48,13 +48,14 @@ pub async fn placeholder_render_textures(state: Rc<State>) {
}
impl PlaceholderNode {
pub fn new_for(state: &Rc<State>, node: Rc<dyn ToplevelNode>) -> Self {
pub fn new_for(state: &Rc<State>, node: Rc<dyn ToplevelNode>, slf: &Weak<Self>) -> Self {
Self {
id: state.node_ids.next(),
toplevel: ToplevelData::new(
state,
node.tl_data().title.borrow().clone(),
node.node_client(),
slf,
),
destroyed: Default::default(),
update_textures_scheduled: Cell::new(false),
@ -63,10 +64,10 @@ impl PlaceholderNode {
}
}
pub fn new_empty(state: &Rc<State>) -> Self {
pub fn new_empty(state: &Rc<State>, slf: &Weak<Self>) -> Self {
Self {
id: state.node_ids.next(),
toplevel: ToplevelData::new(state, String::new(), None),
toplevel: ToplevelData::new(state, String::new(), None, slf),
destroyed: Default::default(),
update_textures_scheduled: Default::default(),
state: state.clone(),

View file

@ -282,10 +282,18 @@ pub struct ToplevelData {
pub jay_screencasts: CopyHashMap<(ClientId, JayScreencastId), Rc<JayScreencast>>,
pub ext_copy_sessions:
CopyHashMap<(ClientId, ExtImageCopyCaptureSessionV1Id), Rc<ExtImageCopyCaptureSessionV1>>,
pub slf: Weak<dyn ToplevelNode>,
}
impl ToplevelData {
pub fn new(state: &Rc<State>, title: String, client: Option<Rc<Client>>) -> Self {
pub fn new<T: ToplevelNode>(
state: &Rc<State>,
title: String,
client: Option<Rc<Client>>,
slf: &Weak<T>,
) -> Self {
let id = toplevel_identifier();
state.toplevels.set(id, slf.clone());
Self {
self_active: Cell::new(false),
client,
@ -307,12 +315,13 @@ impl ToplevelData {
wants_attention: Cell::new(false),
requested_attention: Cell::new(false),
app_id: Default::default(),
identifier: Cell::new(toplevel_identifier()),
identifier: Cell::new(id),
handles: Default::default(),
render_highlight: Default::default(),
jay_toplevels: Default::default(),
jay_screencasts: Default::default(),
ext_copy_sessions: Default::default(),
slf: slf.clone(),
}
}
@ -359,7 +368,12 @@ impl ToplevelData {
for screencast in self.ext_copy_sessions.lock().drain_values() {
screencast.stop();
}
self.identifier.set(toplevel_identifier());
{
let id = toplevel_identifier();
let prev = self.identifier.replace(id);
self.state.toplevels.remove(&prev);
self.state.toplevels.set(id, self.slf.clone());
}
{
let mut handles = self.handles.lock();
for handle in handles.drain_values() {
@ -476,7 +490,8 @@ impl ToplevelData {
log::warn!("Cannot fullscreen root container in a workspace");
return;
}
let placeholder = Rc::new(PlaceholderNode::new_for(state, node.clone()));
let placeholder =
Rc::new_cyclic(|weak| PlaceholderNode::new_for(state, node.clone(), weak));
parent.cnode_replace_child(node.tl_as_node(), placeholder.clone());
let mut kb_foci = Default::default();
if ws.visible.get() {
@ -599,6 +614,12 @@ impl ToplevelData {
}
}
impl Drop for ToplevelData {
fn drop(&mut self) {
self.state.toplevels.remove(&self.identifier.get());
}
}
pub struct TileDragDestination {
pub highlight: Rect,
pub ty: TddType,

View file

@ -1,5 +1,6 @@
use {
crate::utils::opaque::{opaque, Opaque, OpaqueError},
crate::utils::opaque::{opaque, Opaque, OpaqueError, OPAQUE_LEN},
arrayvec::ArrayString,
std::{
fmt::{Display, Formatter},
str::FromStr,
@ -13,6 +14,12 @@ pub fn activation_token() -> ActivationToken {
ActivationToken(opaque())
}
impl ActivationToken {
pub fn to_string(self) -> ArrayString<OPAQUE_LEN> {
self.0.to_string()
}
}
impl Display for ActivationToken {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)

View file

@ -1,5 +1,7 @@
use {
arrayvec::ArrayString,
rand::{thread_rng, Rng},
serde::{de, Deserialize, Deserializer, Serialize, Serializer},
std::{
fmt::{Debug, Display, Formatter},
num::ParseIntError,
@ -22,6 +24,15 @@ pub fn opaque() -> Opaque {
}
}
impl Opaque {
pub fn to_string(self) -> ArrayString<OPAQUE_LEN> {
use std::fmt::Write;
let mut s = ArrayString::new();
write!(s, "{}", self).unwrap();
s
}
}
impl Display for Opaque {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
write!(f, "{:016x}", self.hi)?;
@ -36,24 +47,44 @@ impl Debug for Opaque {
}
}
impl Serialize for Opaque {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
let s = self.to_string();
serializer.serialize_str(&s)
}
}
impl<'de> Deserialize<'de> for Opaque {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = <&str>::deserialize(deserializer)?;
Opaque::from_str(s).map_err(de::Error::custom)
}
}
impl FromStr for Opaque {
type Err = OpaqueError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() != LEN {
if s.len() != OPAQUE_LEN {
return Err(OpaqueError::InvalidLength);
}
if !s.is_char_boundary(LEN / 2) {
if !s.is_char_boundary(OPAQUE_LEN / 2) {
return Err(OpaqueError::NotAscii);
}
let (hi, lo) = s.split_at(LEN / 2);
let (hi, lo) = s.split_at(OPAQUE_LEN / 2);
let hi = u64::from_str_radix(hi, 16).map_err(OpaqueError::Parse)?;
let lo = u64::from_str_radix(lo, 16).map_err(OpaqueError::Parse)?;
Ok(Self { lo, hi })
}
}
const LEN: usize = 32;
pub const OPAQUE_LEN: usize = 32;
#[derive(Debug, Error)]
pub enum OpaqueError {

View file

@ -1,5 +1,6 @@
use {
crate::utils::opaque::{opaque, Opaque, OpaqueError},
crate::utils::opaque::{opaque, Opaque, OpaqueError, OPAQUE_LEN},
arrayvec::ArrayString,
std::{
fmt::{Display, Formatter},
str::FromStr,
@ -13,6 +14,12 @@ pub fn toplevel_identifier() -> ToplevelIdentifier {
ToplevelIdentifier(opaque())
}
impl ToplevelIdentifier {
pub fn to_string(self) -> ArrayString<OPAQUE_LEN> {
self.0.to_string()
}
}
impl Display for ToplevelIdentifier {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)

View file

@ -96,7 +96,6 @@ impl UsrJayCompositor {
jo
}
#[expect(dead_code)]
pub fn watch_workspaces(&self) -> Rc<UsrJayWorkspaceWatcher> {
let ww = Rc::new(UsrJayWorkspaceWatcher {
id: self.con.id(),
@ -143,6 +142,22 @@ impl UsrJayCompositor {
sc
}
pub fn get_toplevel(&self, id: &str) -> Rc<UsrJaySelectToplevel> {
let sc = Rc::new(UsrJaySelectToplevel {
id: self.con.id(),
con: self.con.clone(),
owner: Default::default(),
version: self.version,
});
self.con.request(GetToplevel {
self_id: self.id,
id: sc.id,
toplevel_id: id,
});
self.con.add_object(sc.clone());
sc
}
pub fn select_workspace(&self, seat: &UsrWlSeat) -> Rc<UsrJaySelectWorkspace> {
let sc = Rc::new(UsrJaySelectWorkspace {
id: self.con.id(),

View file

@ -1,9 +1,14 @@
use {
crate::{
ifs::jay_toplevel::ID_SINCE,
object::Version,
utils::clonecell::CloneCell,
wire::{jay_select_toplevel::*, JaySelectToplevelId},
wl_usr::{usr_ifs::usr_jay_toplevel::UsrJayToplevel, usr_object::UsrObject, UsrCon},
wl_usr::{
usr_ifs::usr_jay_toplevel::{UsrJayToplevel, UsrJayToplevelOwner},
usr_object::UsrObject,
UsrCon,
},
},
std::{convert::Infallible, rc::Rc},
};
@ -15,6 +20,19 @@ pub struct UsrJaySelectToplevel {
pub version: Version,
}
impl UsrJaySelectToplevel {
fn send(&self, tl: Option<Rc<UsrJayToplevel>>) {
if let Some(owner) = self.owner.get() {
owner.done(tl);
} else {
if let Some(tl) = tl {
self.con.remove_obj(&*tl);
}
}
self.con.remove_obj(self);
}
}
pub trait UsrJaySelectToplevelOwner {
fn done(&self, toplevel: Option<Rc<UsrJayToplevel>>);
}
@ -22,7 +40,7 @@ pub trait UsrJaySelectToplevelOwner {
impl JaySelectToplevelEventHandler for UsrJaySelectToplevel {
type Error = Infallible;
fn done(&self, ev: Done, _slf: &Rc<Self>) -> Result<(), Self::Error> {
fn done(&self, ev: Done, slf: &Rc<Self>) -> Result<(), Self::Error> {
let tl = if ev.id.is_none() {
None
} else {
@ -31,23 +49,31 @@ impl JaySelectToplevelEventHandler for UsrJaySelectToplevel {
con: self.con.clone(),
owner: Default::default(),
version: self.version,
toplevel_id: Default::default(),
});
self.con.add_object(tl.clone());
Some(tl)
};
match self.owner.get() {
Some(owner) => owner.done(tl),
_ => {
'send: {
if self.version >= ID_SINCE {
if let Some(tl) = tl {
self.con.remove_obj(&*tl);
tl.owner.set(Some(slf.clone()));
break 'send;
}
}
self.send(tl);
}
self.con.remove_obj(self);
Ok(())
}
}
impl UsrJayToplevelOwner for UsrJaySelectToplevel {
fn done(&self, tl: &Rc<UsrJayToplevel>) {
tl.owner.take();
self.send(Some(tl.clone()));
}
}
usr_object_base! {
self = UsrJaySelectToplevel = JaySelectToplevel;
version = self.version;

View file

@ -3,7 +3,11 @@ use {
object::Version,
utils::clonecell::CloneCell,
wire::{jay_select_workspace::*, JaySelectWorkspaceId},
wl_usr::{usr_ifs::usr_jay_workspace::UsrJayWorkspace, usr_object::UsrObject, UsrCon},
wl_usr::{
usr_ifs::usr_jay_workspace::{UsrJayWorkspace, UsrJayWorkspaceOwner},
usr_object::UsrObject,
UsrCon,
},
},
std::{convert::Infallible, rc::Rc},
};
@ -30,20 +34,30 @@ impl JaySelectWorkspaceEventHandler for UsrJaySelectWorkspace {
Ok(())
}
fn selected(&self, ev: Selected, _slf: &Rc<Self>) -> Result<(), Self::Error> {
fn selected(&self, ev: Selected, slf: &Rc<Self>) -> Result<(), Self::Error> {
let tl = Rc::new(UsrJayWorkspace {
id: ev.id,
con: self.con.clone(),
owner: Default::default(),
version: self.version,
linear_id: Default::default(),
output: Default::default(),
name: Default::default(),
});
self.con.add_object(tl.clone());
tl.owner.set(Some(slf.clone()));
Ok(())
}
}
impl UsrJayWorkspaceOwner for UsrJaySelectWorkspace {
fn done(&self, ws: &Rc<UsrJayWorkspace>) {
ws.owner.take();
match self.owner.get() {
Some(owner) => owner.done(ev.output, Some(tl)),
_ => self.con.remove_obj(&*tl),
Some(owner) => owner.done(ws.output.get(), Some(ws.clone())),
_ => self.con.remove_obj(&**ws),
}
self.con.remove_obj(self);
Ok(())
}
}

View file

@ -5,7 +5,7 @@ use {
wire::{jay_toplevel::*, JayToplevelId},
wl_usr::{usr_object::UsrObject, UsrCon},
},
std::{convert::Infallible, rc::Rc},
std::{cell::RefCell, convert::Infallible, rc::Rc},
};
pub struct UsrJayToplevel {
@ -13,10 +13,12 @@ pub struct UsrJayToplevel {
pub con: Rc<UsrCon>,
pub owner: CloneCell<Option<Rc<dyn UsrJayToplevelOwner>>>,
pub version: Version,
pub toplevel_id: RefCell<Option<String>>,
}
pub trait UsrJayToplevelOwner {
fn destroyed(&self) {}
fn done(&self, tl: &Rc<UsrJayToplevel>);
}
impl JayToplevelEventHandler for UsrJayToplevel {
@ -28,6 +30,18 @@ impl JayToplevelEventHandler for UsrJayToplevel {
}
Ok(())
}
fn id_(&self, ev: Id<'_>, _slf: &Rc<Self>) -> Result<(), Self::Error> {
*self.toplevel_id.borrow_mut() = Some(ev.id.to_string());
Ok(())
}
fn done(&self, _ev: Done, slf: &Rc<Self>) -> Result<(), Self::Error> {
if let Some(owner) = self.owner.get() {
owner.done(slf);
}
Ok(())
}
}
usr_object_base! {

View file

@ -5,7 +5,11 @@ use {
wire::{jay_workspace::*, JayWorkspaceId},
wl_usr::{usr_object::UsrObject, UsrCon},
},
std::{convert::Infallible, rc::Rc},
std::{
cell::{Cell, RefCell},
convert::Infallible,
rc::Rc,
},
};
pub struct UsrJayWorkspace {
@ -13,21 +17,20 @@ pub struct UsrJayWorkspace {
pub con: Rc<UsrCon>,
pub owner: CloneCell<Option<Rc<dyn UsrJayWorkspaceOwner>>>,
pub version: Version,
pub linear_id: Cell<u32>,
pub output: Cell<u32>,
pub name: RefCell<Option<String>>,
}
pub trait UsrJayWorkspaceOwner {
fn linear_id(self: Rc<Self>, ev: &LinearId) {
let _ = ev;
fn destroyed(&self, ws: &UsrJayWorkspace) {
let _ = ws;
}
fn name(&self, ev: &Name) {
let _ = ev;
fn done(&self, ws: &Rc<UsrJayWorkspace>) {
let _ = ws;
}
fn destroyed(&self) {}
fn done(&self) {}
fn output(self: Rc<Self>, ev: &Output) {
let _ = ev;
}
@ -41,34 +44,31 @@ impl JayWorkspaceEventHandler for UsrJayWorkspace {
type Error = Infallible;
fn linear_id(&self, ev: LinearId, _slf: &Rc<Self>) -> Result<(), Self::Error> {
if let Some(owner) = self.owner.get() {
owner.linear_id(&ev);
}
self.linear_id.set(ev.linear_id);
Ok(())
}
fn name(&self, ev: Name<'_>, _slf: &Rc<Self>) -> Result<(), Self::Error> {
*self.name.borrow_mut() = Some(ev.name.to_string());
Ok(())
}
fn destroyed(&self, _ev: Destroyed, slf: &Rc<Self>) -> Result<(), Self::Error> {
if let Some(owner) = self.owner.get() {
owner.name(&ev);
owner.destroyed(slf);
}
Ok(())
}
fn destroyed(&self, _ev: Destroyed, _slf: &Rc<Self>) -> Result<(), Self::Error> {
fn done(&self, _ev: Done, slf: &Rc<Self>) -> Result<(), Self::Error> {
if let Some(owner) = self.owner.get() {
owner.destroyed();
}
Ok(())
}
fn done(&self, _ev: Done, _slf: &Rc<Self>) -> Result<(), Self::Error> {
if let Some(owner) = self.owner.get() {
owner.done();
owner.done(slf);
}
Ok(())
}
fn output(&self, ev: Output, _slf: &Rc<Self>) -> Result<(), Self::Error> {
self.output.set(ev.global_name);
if let Some(owner) = self.owner.get() {
owner.output(&ev);
}

View file

@ -31,6 +31,9 @@ impl JayWorkspaceWatcherEventHandler for UsrJayWorkspaceWatcher {
con: self.con.clone(),
owner: Default::default(),
version: self.version,
linear_id: Default::default(),
output: Default::default(),
name: Default::default(),
});
self.con.add_object(jw.clone());
if let Some(owner) = self.owner.get() {

View file

@ -5,7 +5,7 @@ use {
wire::{wl_output::*, WlOutputId},
wl_usr::{usr_object::UsrObject, UsrCon},
},
std::{convert::Infallible, rc::Rc},
std::{cell::RefCell, convert::Infallible, rc::Rc},
};
pub struct UsrWlOutput {
@ -13,6 +13,7 @@ pub struct UsrWlOutput {
pub con: Rc<UsrCon>,
pub owner: CloneCell<Option<Rc<dyn UsrWlOutputOwner>>>,
pub version: Version,
pub name: RefCell<Option<String>>,
}
pub trait UsrWlOutputOwner {
@ -71,6 +72,7 @@ impl WlOutputEventHandler for UsrWlOutput {
}
fn name(&self, ev: Name<'_>, _slf: &Rc<Self>) -> Result<(), Self::Error> {
*self.name.borrow_mut() = Some(ev.name.to_string());
if let Some(owner) = self.owner.get() {
owner.name(&ev);
}

View file

@ -27,6 +27,16 @@ impl UsrWlSurface {
});
}
pub fn damage(&self) {
self.con.request(DamageBuffer {
self_id: self.id,
x: 0,
y: 0,
width: i32::MAX,
height: i32::MAX,
});
}
pub fn frame<F>(&self, f: F)
where
F: FnOnce() + 'static,

View file

@ -96,6 +96,11 @@ request get_xwayland (since = 11) {
id: id(jay_xwayland),
}
request get_toplevel (since = 12) {
id: id(jay_select_toplevel),
toplevel_id: str,
}
# events
event client_id {

View file

@ -3,3 +3,10 @@ request destroy {
event destroyed {
}
event id (since = 12) {
id: str,
}
event done (since = 12) {
}