From 665127e6c08ef55efd39cea75b682def61f06158 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Wed, 24 Jul 2024 17:57:48 +0200 Subject: [PATCH] portal: implement RemoteDesktop portal --- etc/jay-portals.conf | 1 + etc/jay.portal | 2 +- release-notes.md | 1 + src/ei/ei_acceptor.rs | 3 +- src/ei/ei_client.rs | 43 +- src/ifs.rs | 2 + src/ifs/jay_compositor.rs | 18 +- src/ifs/jay_ei_session.rs | 81 ++++ src/ifs/jay_ei_session_builder.rs | 97 +++++ src/portal.rs | 5 + src/portal/ptl_display.rs | 8 +- src/portal/ptl_remote_desktop.rs | 384 ++++++++++++++++++ .../ptl_remote_desktop/remote_desktop_gui.rs | 159 ++++++++ src/utils/pid_info.rs | 8 +- src/wl_usr/usr_ifs.rs | 2 + src/wl_usr/usr_ifs/usr_jay_compositor.rs | 19 +- src/wl_usr/usr_ifs/usr_jay_ei_session.rs | 72 ++++ .../usr_ifs/usr_jay_ei_session_builder.rs | 57 +++ ....freedesktop.impl.portal.RemoteDesktop.txt | 41 ++ wire/jay_compositor.txt | 4 + wire/jay_ei_session.txt | 15 + wire/jay_ei_session_builder.txt | 7 + 22 files changed, 994 insertions(+), 35 deletions(-) create mode 100644 src/ifs/jay_ei_session.rs create mode 100644 src/ifs/jay_ei_session_builder.rs create mode 100644 src/portal/ptl_remote_desktop.rs create mode 100644 src/portal/ptl_remote_desktop/remote_desktop_gui.rs create mode 100644 src/wl_usr/usr_ifs/usr_jay_ei_session.rs create mode 100644 src/wl_usr/usr_ifs/usr_jay_ei_session_builder.rs create mode 100644 wire-dbus/org.freedesktop.impl.portal.RemoteDesktop.txt create mode 100644 wire/jay_ei_session.txt create mode 100644 wire/jay_ei_session_builder.txt diff --git a/etc/jay-portals.conf b/etc/jay-portals.conf index a3e4e097..078add3e 100644 --- a/etc/jay-portals.conf +++ b/etc/jay-portals.conf @@ -1,3 +1,4 @@ [preferred] default=gtk org.freedesktop.impl.portal.ScreenCast=jay +org.freedesktop.impl.portal.RemoteDesktop=jay diff --git a/etc/jay.portal b/etc/jay.portal index 99a7d867..c6d67365 100644 --- a/etc/jay.portal +++ b/etc/jay.portal @@ -1,3 +1,3 @@ [portal] DBusName=org.freedesktop.impl.portal.desktop.jay -Interfaces=org.freedesktop.impl.portal.ScreenCast; +Interfaces=org.freedesktop.impl.portal.ScreenCast;org.freedesktop.impl.portal.RemoteDesktop; diff --git a/release-notes.md b/release-notes.md index 1778b9fc..ec48660a 100644 --- a/release-notes.md +++ b/release-notes.md @@ -5,6 +5,7 @@ - Add support for tearing. - Add support for touch input. - Add support for libei. +- Add support for RemoteDesktop portal. # 1.4.0 (2024-07-07) diff --git a/src/ei/ei_acceptor.rs b/src/ei/ei_acceptor.rs index 9f02f776..b3b2d211 100644 --- a/src/ei/ei_acceptor.rs +++ b/src/ei/ei_acceptor.rs @@ -146,8 +146,7 @@ async fn accept(fd: Rc, state: Rc) { break; } }; - let id = state.clients.id(); - if let Err(e) = state.ei_clients.spawn(id, &state, fd) { + if let Err(e) = state.ei_clients.spawn(&state, fd) { log::error!("Could not spawn a client: {}", ErrorFmt(e)); break; } diff --git a/src/ei/ei_client.rs b/src/ei/ei_client.rs index f5a22f14..9d69e56c 100644 --- a/src/ei/ei_client.rs +++ b/src/ei/ei_client.rs @@ -16,6 +16,7 @@ use { asyncevent::AsyncEvent, buffd::{EiMsgFormatter, EiMsgParser, EiMsgParserError, OutBufferSwapchain}, clonecell::CloneCell, + debug_fn::debug_fn, errorfmt::ErrorFmt, numcell::NumCell, pid_info::{get_pid_info, get_socket_creds, PidInfo}, @@ -31,7 +32,7 @@ use { ops::DerefMut, rc::Rc, }, - uapi::{c, OwnedFd}, + uapi::OwnedFd, }; mod ei_error; @@ -64,26 +65,21 @@ impl EiClients { mem::take(self.shutdown_clients.borrow_mut().deref_mut()); } - pub fn spawn( - &self, - id: ClientId, - global: &Rc, - socket: Rc, - ) -> Result<(), EiClientError> { + pub fn spawn(&self, global: &Rc, socket: Rc) -> Result<(), EiClientError> { let Some((uid, pid)) = get_socket_creds(&socket) else { return Ok(()); }; - self.spawn2(id, global, socket, uid, pid)?; + let pid_info = get_pid_info(uid, pid); + self.spawn2(global, socket, Some(pid_info), None)?; Ok(()) } pub fn spawn2( &self, - id: ClientId, global: &Rc, socket: Rc, - uid: c::uid_t, - pid: c::pid_t, + pid_info: Option, + app_id: Option, ) -> Result, EiClientError> { let versions = EiInterfaceVersions { ei_button: EiInterfaceVersion::new(1), @@ -100,7 +96,7 @@ impl EiClients { ei_touchscreen: EiInterfaceVersion::new(1), }; let data = Rc::new(EiClient { - id, + id: global.clients.id(), state: global.clone(), context: Cell::new(EiContext::Receiver), connection: Default::default(), @@ -111,10 +107,11 @@ impl EiClients { flush_request: Default::default(), shutdown: Default::default(), tracker: Default::default(), - pid_info: get_pid_info(uid, pid), + pid_info, disconnect_announced: Cell::new(false), versions, name: Default::default(), + app_id, last_serial: Default::default(), }); track!(data, data); @@ -127,12 +124,17 @@ impl EiClients { data: data.clone(), }; log::info!( - "Client {} connected, pid: {}, uid: {}, fd: {}, comm: {:?}", - id, - pid, - uid, - client.data.socket.raw(), - data.pid_info.comm, + "Client {} connected{:?}", + data.id, + debug_fn(|fmt| { + if let Some(p) = &data.pid_info { + write!(fmt, ", pid: {}, uid: {}, comm: {:?}", p.pid, p.uid, p.comm)?; + } + if let Some(app_id) = &data.app_id { + write!(fmt, ", app-id: {app_id:?}")?; + } + Ok(()) + }), ); self.clients.borrow_mut().insert(client.data.id, client); Ok(data) @@ -199,10 +201,11 @@ pub struct EiClient { flush_request: AsyncEvent, shutdown: AsyncEvent, pub tracker: Tracker, - pub pid_info: PidInfo, + pub pid_info: Option, pub disconnect_announced: Cell, pub versions: EiInterfaceVersions, pub name: RefCell>, + pub app_id: Option, pub last_serial: NumCell, } diff --git a/src/ifs.rs b/src/ifs.rs index 851dd9fa..3d81f770 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -7,6 +7,8 @@ pub mod ext_session_lock_v1; pub mod ipc; pub mod jay_compositor; pub mod jay_damage_tracking; +pub mod jay_ei_session; +pub mod jay_ei_session_builder; pub mod jay_idle; pub mod jay_input; pub mod jay_log_file; diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index e7faef22..dd778d0c 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -4,6 +4,7 @@ use { client::{Client, ClientCaps, ClientError, CAP_JAY_COMPOSITOR}, globals::{Global, GlobalName}, ifs::{ + jay_ei_session_builder::JayEiSessionBuilder, jay_idle::JayIdle, jay_input::JayInput, jay_log_file::JayLogFile, @@ -30,6 +31,8 @@ use { thiserror::Error, }; +pub const CREATE_EI_SESSION_SINCE: Version = Version(5); + pub struct JayCompositorGlobal { name: GlobalName, } @@ -66,7 +69,7 @@ impl Global for JayCompositorGlobal { } fn version(&self) -> u32 { - 4 + 5 } fn required_caps(&self) -> ClientCaps { @@ -377,6 +380,19 @@ impl JayCompositorRequestHandler for JayCompositor { seat.global.select_workspace(selector); Ok(()) } + + fn create_ei_session(&self, req: CreateEiSession, _slf: &Rc) -> Result<(), Self::Error> { + let obj = Rc::new(JayEiSessionBuilder { + id: req.id, + client: self.client.clone(), + tracker: Default::default(), + version: self.version, + app_id: Default::default(), + }); + track!(self.client, obj); + self.client.add_client_obj(&obj)?; + Ok(()) + } } object_base! { diff --git a/src/ifs/jay_ei_session.rs b/src/ifs/jay_ei_session.rs new file mode 100644 index 00000000..8aa516ca --- /dev/null +++ b/src/ifs/jay_ei_session.rs @@ -0,0 +1,81 @@ +use { + crate::{ + client::{Client, ClientError, ClientId}, + leaks::Tracker, + object::{Object, Version}, + wire::{ + jay_ei_session::{Created, Destroyed, Failed, JayEiSessionRequestHandler, Release}, + JayEiSessionId, + }, + }, + std::rc::Rc, + thiserror::Error, + uapi::OwnedFd, +}; + +pub struct JayEiSession { + pub id: JayEiSessionId, + pub client: Rc, + pub ei_client_id: Option, + pub tracker: Tracker, + pub version: Version, +} + +impl JayEiSession { + pub fn send_created(&self, fd: &Rc) { + self.client.event(Created { + self_id: self.id, + fd: fd.clone(), + }); + } + + pub fn send_failed(&self, reason: &str) { + self.client.event(Failed { + self_id: self.id, + reason, + }); + } + + fn send_destroyed(&self) { + self.client.event(Destroyed { self_id: self.id }); + } + + fn kill(&self, send_destroyed: bool) { + if let Some(id) = self.ei_client_id { + self.client.state.ei_clients.shutdown(id); + } + if send_destroyed { + self.send_destroyed(); + } + } +} + +impl JayEiSessionRequestHandler for JayEiSession { + type Error = JayEiSessionError; + + fn release(&self, _req: Release, _slf: &Rc) -> Result<(), Self::Error> { + self.kill(false); + self.client.remove_obj(self)?; + Ok(()) + } +} + +object_base! { + self = JayEiSession; + version = self.version; +} + +impl Object for JayEiSession { + fn break_loops(&self) { + self.kill(false); + } +} + +simple_add_obj!(JayEiSession); + +#[derive(Debug, Error)] +pub enum JayEiSessionError { + #[error(transparent)] + ClientError(Box), +} +efrom!(JayEiSessionError, ClientError); diff --git a/src/ifs/jay_ei_session_builder.rs b/src/ifs/jay_ei_session_builder.rs new file mode 100644 index 00000000..e42b36fa --- /dev/null +++ b/src/ifs/jay_ei_session_builder.rs @@ -0,0 +1,97 @@ +use { + crate::{ + client::{Client, ClientError}, + ei::ei_client::EiClientError, + ifs::jay_ei_session::JayEiSession, + leaks::Tracker, + object::{Object, Version}, + utils::{errorfmt::ErrorFmt, oserror::OsError}, + wire::{ + jay_ei_session_builder::{Commit, JayEiSessionBuilderRequestHandler, SetAppId}, + JayEiSessionBuilderId, + }, + }, + std::{cell::RefCell, rc::Rc}, + thiserror::Error, + uapi::c, +}; + +pub struct JayEiSessionBuilder { + pub id: JayEiSessionBuilderId, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, + pub app_id: RefCell>, +} + +impl JayEiSessionBuilderRequestHandler for JayEiSessionBuilder { + type Error = JayEiSessionBuilderError; + + fn commit(&self, req: Commit, _slf: &Rc) -> Result<(), Self::Error> { + self.client.remove_obj(self)?; + let app_id = self.app_id.borrow().clone(); + if app_id.is_none() { + return Err(JayEiSessionBuilderError::NoAppId); + } + let res = (move || { + let con = uapi::socketpair(c::AF_UNIX, c::SOCK_STREAM | c::SOCK_CLOEXEC, 0); + let (server, client) = match con { + Ok(w) => w, + Err(e) => return Err(JayEiSessionBuilderError::SocketPair(e.into())), + }; + let ei_client_id = self + .client + .state + .ei_clients + .spawn2(&self.client.state, Rc::new(server), None, app_id) + .map_err(JayEiSessionBuilderError::SpawnClient)? + .id; + Ok((ei_client_id, Rc::new(client))) + })(); + let obj = Rc::new(JayEiSession { + id: req.id, + client: self.client.clone(), + ei_client_id: res.as_ref().ok().map(|v| v.0), + tracker: Default::default(), + version: self.version, + }); + track!(self.client, obj); + self.client.add_client_obj(&obj)?; + match res { + Ok((_, fd)) => obj.send_created(&fd), + Err(e) => { + let e = format!("Could not spawn client: {}", ErrorFmt(e)); + log::error!("{}", e); + obj.send_failed(&e); + } + } + Ok(()) + } + + fn set_app_id(&self, req: SetAppId<'_>, _slf: &Rc) -> Result<(), Self::Error> { + *self.app_id.borrow_mut() = Some(req.app_id.to_string()); + Ok(()) + } +} + +object_base! { + self = JayEiSessionBuilder; + version = self.version; +} + +impl Object for JayEiSessionBuilder {} + +simple_add_obj!(JayEiSessionBuilder); + +#[derive(Debug, Error)] +pub enum JayEiSessionBuilderError { + #[error(transparent)] + ClientError(Box), + #[error("Could not create a socketpair")] + SocketPair(#[source] OsError), + #[error("Could not spawn a new client")] + SpawnClient(#[source] EiClientError), + #[error("Commit called without app-id")] + NoAppId, +} +efrom!(JayEiSessionBuilderError, ClientError); diff --git a/src/portal.rs b/src/portal.rs index fdea28c4..368a435b 100644 --- a/src/portal.rs +++ b/src/portal.rs @@ -1,4 +1,5 @@ mod ptl_display; +mod ptl_remote_desktop; mod ptl_render_ctx; mod ptl_screencast; mod ptr_gui; @@ -17,6 +18,7 @@ use { pipewire::pw_con::{PwConHolder, PwConOwner}, portal::{ ptl_display::{watch_displays, PortalDisplay, PortalDisplayId}, + ptl_remote_desktop::{add_remote_desktop_dbus_members, RemoteDesktopSession}, ptl_render_ctx::PortalRenderCtx, ptl_screencast::{add_screencast_dbus_members, ScreencastSession}, }, @@ -195,6 +197,7 @@ async fn run_async( displays: Default::default(), dbus, screencasts: Default::default(), + remote_desktop_sessions: Default::default(), next_id: NumCell::new(1), render_ctxs: Default::default(), dma_buf_ids: Default::default(), @@ -210,6 +213,7 @@ async fn run_async( if let Some(pw_con) = &pw_con { add_screencast_dbus_members(&state, &pw_con.con, &obj); } + add_remote_desktop_dbus_members(&state, &obj); obj }; watch_displays(state.clone()).await; @@ -288,6 +292,7 @@ struct PortalState { displays: CopyHashMap>, dbus: Rc, screencasts: CopyHashMap>, + remote_desktop_sessions: CopyHashMap>, next_id: NumCell, render_ctxs: CopyHashMap>, dma_buf_ids: Rc, diff --git a/src/portal/ptl_display.rs b/src/portal/ptl_display.rs index 51255c0d..675bb597 100644 --- a/src/portal/ptl_display.rs +++ b/src/portal/ptl_display.rs @@ -4,8 +4,8 @@ use { ifs::wl_seat::POINTER, object::Version, portal::{ - ptl_render_ctx::PortalRenderCtx, ptl_screencast::ScreencastSession, - ptr_gui::WindowData, PortalState, + ptl_remote_desktop::RemoteDesktopSession, ptl_render_ctx::PortalRenderCtx, + ptl_screencast::ScreencastSession, ptr_gui::WindowData, PortalState, }, utils::{ bitflags::BitflagsExt, clonecell::CloneCell, copyhashmap::CopyHashMap, @@ -74,6 +74,7 @@ pub struct PortalDisplay { pub windows: CopyHashMap>, pub screencasts: CopyHashMap>, + pub remote_desktop_sessions: CopyHashMap>, } pub struct PortalOutput { @@ -303,7 +304,7 @@ fn finish_display_connect(dpy: Rc) { con: dpy.con.clone(), owner: Default::default(), caps: Default::default(), - version: Version(version.min(4)), + version: Version(version.min(5)), }); dpy.con.add_object(jc.clone()); dpy.registry.request_bind(name, version, jc.deref()); @@ -395,6 +396,7 @@ fn finish_display_connect(dpy: Rc) { vp, windows: Default::default(), screencasts: Default::default(), + remote_desktop_sessions: Default::default(), }); dpy.state.displays.set(dpy.id, dpy.clone()); diff --git a/src/portal/ptl_remote_desktop.rs b/src/portal/ptl_remote_desktop.rs new file mode 100644 index 00000000..8f14349d --- /dev/null +++ b/src/portal/ptl_remote_desktop.rs @@ -0,0 +1,384 @@ +mod remote_desktop_gui; + +use { + crate::{ + dbus::{prelude::Variant, DbusObject, DictEntry, DynamicType, PendingReply, FALSE}, + ifs::jay_compositor::CREATE_EI_SESSION_SINCE, + portal::{ + ptl_display::{PortalDisplay, PortalDisplayId}, + ptl_remote_desktop::remote_desktop_gui::SelectionGui, + PortalState, PORTAL_SUCCESS, + }, + utils::{ + clonecell::{CloneCell, UnsafeCellCloneSafe}, + copyhashmap::CopyHashMap, + hash_map_ext::HashMapExt, + }, + wire_dbus::{ + org, + org::freedesktop::impl_::portal::{ + remote_desktop::{ + ConnectToEIS, ConnectToEISReply, CreateSession, CreateSessionReply, + SelectDevices, SelectDevicesReply, Start, StartReply, + }, + session::{CloseReply as SessionCloseReply, Closed}, + }, + }, + wl_usr::usr_ifs::usr_jay_ei_session::{UsrJayEiSession, UsrJayEiSessionOwner}, + }, + std::{borrow::Cow, cell::Cell, ops::Deref, rc::Rc}, + uapi::OwnedFd, +}; + +shared_ids!(ScreencastSessionId); +pub struct RemoteDesktopSession { + _id: ScreencastSessionId, + state: Rc, + pub app: String, + session_obj: DbusObject, + pub phase: CloneCell, +} + +#[derive(Clone)] +pub enum RemoteDesktopPhase { + Init, + DevicesSelected, + Selecting(Rc), + Starting(Rc), + Started(Rc), + Terminated, +} + +unsafe impl UnsafeCellCloneSafe for RemoteDesktopPhase {} + +pub struct SelectingDisplay { + pub session: Rc, + pub request_obj: Rc, + pub reply: Rc>>, + pub guis: CopyHashMap>, +} + +pub struct StartingRemoteDesktop { + pub session: Rc, + pub _request_obj: Rc, + pub reply: Rc>>, + pub dpy: Rc, + pub ei_session: Rc, +} + +pub struct StartedRemoteDesktop { + session: Rc, + dpy: Rc, + ei_session: Rc, + ei_fd: Cell>>, +} + +bitflags! { + DeviceTypes: u32; + + KEYBOARD = 1, + POINTER = 2, + TOUCHSCREEN = 4, +} + +impl UsrJayEiSessionOwner for StartingRemoteDesktop { + fn created(&self, fd: &Rc) { + { + 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(), + ei_session: self.ei_session.clone(), + ei_fd: Cell::new(Some(fd.clone())), + }); + self.session + .phase + .set(RemoteDesktopPhase::Started(started.clone())); + started.ei_session.owner.set(Some(started.clone())); + } + + fn failed(&self, reason: &str) { + log::error!("Could not create session: {}", reason); + self.reply.err(reason); + self.session.kill(); + } +} + +impl SelectingDisplay { + pub fn starting(&self, dpy: &Rc) { + let builder = dpy.jc.create_ei_session(); + builder.set_app_id(&self.session.app); + let ei_session = builder.commit(); + let starting = Rc::new(StartingRemoteDesktop { + session: self.session.clone(), + _request_obj: self.request_obj.clone(), + reply: self.reply.clone(), + dpy: dpy.clone(), + ei_session, + }); + self.session + .phase + .set(RemoteDesktopPhase::Starting(starting.clone())); + starting.ei_session.owner.set(Some(starting.clone())); + dpy.remote_desktop_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()); + } + } + } + + fn dbus_select_devices( + self: &Rc, + _req: SelectDevices, + reply: PendingReply>, + ) { + match self.phase.get() { + RemoteDesktopPhase::Init => {} + _ => { + self.kill(); + reply.err("Devices have already been selected"); + return; + } + } + self.phase.set(RemoteDesktopPhase::DevicesSelected); + reply.ok(&SelectDevicesReply { + response: PORTAL_SUCCESS, + results: Default::default(), + }); + } + + fn dbus_start(self: &Rc, req: Start<'_>, reply: PendingReply>) { + match self.phase.get() { + RemoteDesktopPhase::DevicesSelected => {} + _ => { + 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, + Err(_) => { + self.kill(); + reply.err("Request handle is not unique"); + return; + } + }; + { + use org::freedesktop::impl_::portal::request::*; + request_obj.add_method::({ + let slf = self.clone(); + move |_, pr| { + slf.kill(); + pr.ok(&CloseReply); + } + }); + } + let guis = CopyHashMap::new(); + for dpy in self.state.displays.lock().values() { + if dpy.outputs.len() > 0 && dpy.jc.version >= CREATE_EI_SESSION_SINCE { + guis.set(dpy.id, SelectionGui::new(self, dpy)); + } + } + if guis.is_empty() { + self.kill(); + reply.err("There are no running displays"); + return; + } + self.phase + .set(RemoteDesktopPhase::Selecting(Rc::new(SelectingDisplay { + session: self.clone(), + request_obj: Rc::new(request_obj), + reply: Rc::new(reply), + guis, + }))); + } + + fn dbus_connect_to_eis( + self: &Rc, + _req: ConnectToEIS, + reply: PendingReply, + ) { + let RemoteDesktopPhase::Started(started) = self.phase.get() else { + self.kill(); + reply.err("Sources have already been selected"); + return; + }; + let Some(fd) = started.ei_fd.take() else { + self.kill(); + reply.err("EI file descriptor has already been consumed"); + return; + }; + reply.ok(&ConnectToEISReply { fd }); + } +} + +impl UsrJayEiSessionOwner for StartedRemoteDesktop { + fn destroyed(&self) { + self.session.kill(); + } +} + +pub(super) fn add_remote_desktop_dbus_members(state_: &Rc, object: &DbusObject) { + use org::freedesktop::impl_::portal::remote_desktop::*; + let state = state_.clone(); + object.add_method::(move |req, pr| { + dbus_create_session(&state, req, pr); + }); + let state = state_.clone(); + object.add_method::(move |req, pr| { + dbus_select_devices(&state, req, pr); + }); + let state = state_.clone(); + object.add_method::(move |req, pr| { + dbus_start(&state, req, pr); + }); + let state = state_.clone(); + object.add_method::(move |req, pr| { + dbus_connect_to_eis(&state, req, pr); + }); + object.set_property::(Variant::U32(DeviceTypes::all().0)); + object.set_property::(Variant::U32(2)); +} + +fn dbus_create_session( + state: &Rc, + req: CreateSession, + reply: PendingReply>, +) { + log::info!("Create remote desktop session {:#?}", req); + if state + .remote_desktop_sessions + .contains(req.session_handle.0.deref()) + { + reply.err("Session already exists"); + return; + } + let obj = match state.dbus.add_object(req.session_handle.0.to_string()) { + Ok(obj) => obj, + Err(_) => { + reply.err("Session path is not unique"); + return; + } + }; + let session = Rc::new(RemoteDesktopSession { + _id: state.id(), + state: state.clone(), + app: req.app_id.to_string(), + session_obj: obj, + phase: CloneCell::new(RemoteDesktopPhase::Init), + }); + { + use org::freedesktop::impl_::portal::session::*; + let ses = session.clone(); + session.session_obj.add_method::(move |_, pr| { + ses.kill(); + pr.ok(&SessionCloseReply); + }); + session.session_obj.set_property::(Variant::U32(2)); + } + state + .remote_desktop_sessions + .set(req.session_handle.0.to_string(), session); + reply.ok(&CreateSessionReply { + response: PORTAL_SUCCESS, + results: Default::default(), + }); +} + +fn dbus_select_devices( + state: &Rc, + req: SelectDevices, + reply: PendingReply>, +) { + if let Some(s) = get_session(state, &reply, &req.session_handle.0) { + s.dbus_select_devices(req, reply); + } +} + +fn dbus_start(state: &Rc, req: Start, reply: PendingReply>) { + if let Some(s) = get_session(state, &reply, &req.session_handle.0) { + s.dbus_start(req, reply); + } +} + +fn dbus_connect_to_eis( + state: &Rc, + req: ConnectToEIS, + reply: PendingReply, +) { + if let Some(s) = get_session(state, &reply, &req.session_handle.0) { + s.dbus_connect_to_eis(req, reply); + } +} + +fn get_session( + state: &Rc, + reply: &PendingReply, + handle: &str, +) -> Option> { + let res = state.remote_desktop_sessions.get(handle); + if res.is_none() { + let msg = format!("Remote desktop session `{}` does not exist", handle); + reply.err(&msg); + } + res +} diff --git a/src/portal/ptl_remote_desktop/remote_desktop_gui.rs b/src/portal/ptl_remote_desktop/remote_desktop_gui.rs new file mode 100644 index 00000000..1963a32f --- /dev/null +++ b/src/portal/ptl_remote_desktop/remote_desktop_gui.rs @@ -0,0 +1,159 @@ +use { + crate::{ + ifs::wl_seat::{wl_pointer::PRESSED, BTN_LEFT}, + portal::{ + ptl_display::{PortalDisplay, PortalOutput, PortalSeat}, + ptl_remote_desktop::{RemoteDesktopPhase, RemoteDesktopSession}, + ptr_gui::{ + Align, Button, ButtonOwner, Flow, GuiElement, Label, Orientation, OverlayWindow, + OverlayWindowOwner, + }, + }, + theme::Color, + utils::{copyhashmap::CopyHashMap, hash_map_ext::HashMapExt}, + }, + std::rc::Rc, +}; + +const H_MARGIN: f32 = 30.0; +const V_MARGIN: f32 = 20.0; + +pub struct SelectionGui { + remote_desktop_session: Rc, + dpy: Rc, + surfaces: CopyHashMap>, +} + +pub struct SelectionGuiSurface { + gui: Rc, + output: Rc, + overlay: Rc, +} + +struct StaticButton { + surface: Rc, + role: ButtonRole, +} + +#[derive(Copy, Clone, Eq, PartialEq)] +enum ButtonRole { + Accept, + Reject, +} + +impl SelectionGui { + pub fn kill(&self, upwards: bool) { + for surface in self.surfaces.lock().drain_values() { + surface.overlay.data.kill(false); + } + if let RemoteDesktopPhase::Selecting(s) = self.remote_desktop_session.phase.get() { + s.guis.remove(&self.dpy.id); + if upwards && s.guis.is_empty() { + self.remote_desktop_session.kill(); + } + } + } +} + +fn create_accept_gui(surface: &Rc) -> Rc { + let app = &surface.gui.remote_desktop_session.app; + let text = if app.is_empty() { + format!("An application wants to generate/monitor input") + } else { + format!("`{}` wants to generate/monitor input", app) + }; + let label = Rc::new(Label::default()); + *label.text.borrow_mut() = text; + let accept_button = static_button(surface, ButtonRole::Accept, "Allow"); + let reject_button = static_button(surface, ButtonRole::Reject, "Reject"); + for button in [&accept_button, &reject_button] { + button.border_color.set(Color::from_gray(100)); + button.border.set(2.0); + button.padding.set(5.0); + } + accept_button.bg_color.set(Color::from_rgb(170, 200, 170)); + accept_button + .bg_hover_color + .set(Color::from_rgb(170, 255, 170)); + reject_button.bg_color.set(Color::from_rgb(200, 170, 170)); + reject_button + .bg_hover_color + .set(Color::from_rgb(255, 170, 170)); + let flow = Rc::new(Flow::default()); + flow.orientation.set(Orientation::Vertical); + flow.cross_align.set(Align::Center); + flow.in_margin.set(V_MARGIN); + flow.cross_margin.set(H_MARGIN); + *flow.elements.borrow_mut() = vec![label, accept_button, reject_button]; + flow +} + +impl OverlayWindowOwner for SelectionGuiSurface { + fn kill(&self, upwards: bool) { + self.gui.dpy.windows.remove(&self.overlay.data.surface.id); + self.gui.surfaces.remove(&self.output.global_id); + if upwards && self.gui.surfaces.is_empty() { + self.gui.kill(true); + } + } +} + +impl SelectionGui { + pub fn new(ss: &Rc, dpy: &Rc) -> Rc { + let gui = Rc::new(SelectionGui { + remote_desktop_session: ss.clone(), + dpy: dpy.clone(), + surfaces: Default::default(), + }); + for output in dpy.outputs.lock().values() { + let sgs = Rc::new(SelectionGuiSurface { + gui: gui.clone(), + output: output.clone(), + overlay: OverlayWindow::new(output), + }); + let element = create_accept_gui(&sgs); + sgs.overlay.data.content.set(Some(element)); + gui.dpy + .windows + .set(sgs.overlay.data.surface.id, sgs.overlay.data.clone()); + gui.surfaces.set(output.global_id, sgs); + } + gui + } +} + +impl ButtonOwner for StaticButton { + fn button(&self, _seat: &PortalSeat, button: u32, state: u32) { + if button != BTN_LEFT || state != PRESSED { + return; + } + match self.role { + ButtonRole::Accept => { + log::info!("User has accepted the request"); + let selecting = match self.surface.gui.remote_desktop_session.phase.get() { + RemoteDesktopPhase::Selecting(selecting) => selecting, + _ => return, + }; + for gui in selecting.guis.lock().drain_values() { + gui.kill(false); + } + selecting.starting(&self.surface.output.dpy); + } + ButtonRole::Reject => { + log::info!("User has rejected the remote desktop request"); + self.surface.gui.remote_desktop_session.kill(); + } + } + } +} + +fn static_button(surface: &Rc, role: ButtonRole, text: &str) -> Rc