Merge pull request #291 from mahkoh/jorth/session-restore
portal: implement session restoration
This commit is contained in:
commit
06d7fff905
39 changed files with 987 additions and 380 deletions
6
Cargo.lock
generated
6
Cargo.lock
generated
|
|
@ -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",
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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! {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()));
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
)+
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
169
src/portal/ptl_session.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -661,6 +661,7 @@ impl WindowData {
|
|||
buf.free.set(false);
|
||||
|
||||
self.surface.attach(&buf.wl);
|
||||
self.surface.damage();
|
||||
self.surface.commit();
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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! {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -3,3 +3,10 @@ request destroy {
|
|||
|
||||
event destroyed {
|
||||
}
|
||||
|
||||
event id (since = 12) {
|
||||
id: str,
|
||||
}
|
||||
|
||||
event done (since = 12) {
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue