From a162055f1da23557cb447388e14caa70b59a4562 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 30 Jul 2022 19:21:30 +0200 Subject: [PATCH] portal: add a desktop portal --- etc/jay.portal | 4 + ...reedesktop.impl.portal.desktop.jay.service | 4 + etc/xdg-desktop-portal-jay.service | 7 + src/cli.rs | 5 +- src/dbus.rs | 6 - src/dbus/socket.rs | 4 - src/ifs/wl_seat.rs | 2 +- src/ifs/wl_seat/wl_pointer.rs | 2 +- src/it/test_backend.rs | 2 +- src/macros.rs | 34 + src/main.rs | 1 + src/pipewire.rs | 2 - src/pipewire/pw_mem.rs | 2 + src/portal.rs | 161 ++++ src/portal/ptl_display.rs | 486 ++++++++++ src/portal/ptl_render_ctx.rs | 6 + src/portal/ptl_screencast.rs | 450 +++++++++ src/portal/ptl_screencast/screencast_gui.rs | 182 ++++ src/portal/ptr_gui.rs | 876 ++++++++++++++++++ src/render/renderer.rs | 2 +- src/render/renderer/framebuffer.rs | 22 + src/render/renderer/renderer_base.rs | 70 +- src/theme.rs | 4 + src/utils/copyhashmap.rs | 6 +- src/video/drm.rs | 4 +- src/wire_dbus.rs | 2 +- src/wl_usr.rs | 16 +- src/wl_usr/usr_ifs/usr_jay_compositor.rs | 1 + src/wl_usr/usr_ifs/usr_jay_screencast.rs | 2 + src/wl_usr/usr_ifs/usr_wl_pointer.rs | 1 + src/wl_usr/usr_ifs/usr_wl_shm.rs | 1 + src/wl_usr/usr_ifs/usr_wlr_layer_surface.rs | 2 + src/wl_usr/usr_ifs/usr_wp_viewport.rs | 1 + .../usr_ifs/usr_zwlr_screencopy_frame.rs | 1 + .../usr_ifs/usr_zwlr_screencopy_manager.rs | 1 + .../org.freedesktop.impl.portal.Request.txt | 1 + ...org.freedesktop.impl.portal.ScreenCast.txt | 34 + .../org.freedesktop.impl.portal.Session.txt | 9 + 38 files changed, 2389 insertions(+), 27 deletions(-) create mode 100644 etc/jay.portal create mode 100644 etc/org.freedesktop.impl.portal.desktop.jay.service create mode 100644 etc/xdg-desktop-portal-jay.service create mode 100644 src/portal.rs create mode 100644 src/portal/ptl_display.rs create mode 100644 src/portal/ptl_render_ctx.rs create mode 100644 src/portal/ptl_screencast.rs create mode 100644 src/portal/ptl_screencast/screencast_gui.rs create mode 100644 src/portal/ptr_gui.rs create mode 100644 wire-dbus/org.freedesktop.impl.portal.Request.txt create mode 100644 wire-dbus/org.freedesktop.impl.portal.ScreenCast.txt create mode 100644 wire-dbus/org.freedesktop.impl.portal.Session.txt diff --git a/etc/jay.portal b/etc/jay.portal new file mode 100644 index 00000000..019f7796 --- /dev/null +++ b/etc/jay.portal @@ -0,0 +1,4 @@ +[portal] +DBusName=org.freedesktop.impl.portal.desktop.jay +Interfaces=org.freedesktop.impl.portal.ScreenCast; +UseIn=jay diff --git a/etc/org.freedesktop.impl.portal.desktop.jay.service b/etc/org.freedesktop.impl.portal.desktop.jay.service new file mode 100644 index 00000000..b3b6344b --- /dev/null +++ b/etc/org.freedesktop.impl.portal.desktop.jay.service @@ -0,0 +1,4 @@ +[D-BUS Service] +Name=org.freedesktop.impl.portal.desktop.jay +Exec=/bin/false +SystemdService=xdg-desktop-portal-jay.service diff --git a/etc/xdg-desktop-portal-jay.service b/etc/xdg-desktop-portal-jay.service new file mode 100644 index 00000000..4fe86765 --- /dev/null +++ b/etc/xdg-desktop-portal-jay.service @@ -0,0 +1,7 @@ +[Unit] +Description=Jay Portal + +[Service] +Type=dbus +BusName=org.freedesktop.impl.portal.desktop.jay +ExecStart=/home/julian/bin/jay portal diff --git a/src/cli.rs b/src/cli.rs index 677c7434..d0c6cd08 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -9,7 +9,7 @@ mod set_log_level; mod unlock; use { - crate::compositor::start_compositor, + crate::{compositor::start_compositor, portal}, ::log::Level, clap::{ArgEnum, Args, Parser, Subcommand}, clap_complete::Shell, @@ -53,6 +53,8 @@ pub enum Cmd { RunPrivileged(RunPrivilegedArgs), /// Tests the events produced by a seat. SeatTest(SeatTestArgs), + /// Run the desktop portal. + Portal, #[cfg(feature = "it")] RunTests, } @@ -214,6 +216,7 @@ pub fn main() { Cmd::Unlock => unlock::main(cli.global), Cmd::RunPrivileged(a) => run_privileged::main(cli.global, a), Cmd::SeatTest(a) => seat_test::main(cli.global, a), + Cmd::Portal => portal::run(cli.global), #[cfg(feature = "it")] Cmd::RunTests => crate::it::run_tests(), } diff --git a/src/dbus.rs b/src/dbus.rs index 3946b835..65be51f2 100644 --- a/src/dbus.rs +++ b/src/dbus.rs @@ -276,10 +276,8 @@ const ALLOW_INTERACTIVE_AUTHORIZATION: u8 = 0x4; pub const DBUS_NAME_FLAG_ALLOW_REPLACEMENT: u32 = 0x1; #[allow(dead_code)] pub const DBUS_NAME_FLAG_REPLACE_EXISTING: u32 = 0x2; -#[allow(dead_code)] pub const DBUS_NAME_FLAG_DO_NOT_QUEUE: u32 = 0x4; -#[allow(dead_code)] pub const DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER: u32 = 1; #[allow(dead_code)] pub const DBUS_REQUEST_NAME_REPLY_IN_QUEUE: u32 = 2; @@ -604,7 +602,6 @@ impl Drop for DbusObject { } impl DbusObject { - #[allow(dead_code)] pub fn add_method(&self, handler: F) where T: MethodCall<'static>, @@ -623,7 +620,6 @@ impl DbusObject { self.data.methods.set(key, rhd); } - #[allow(dead_code)] pub fn set_property(&self, value: Variant<'static>) where T: Property + 'static, @@ -649,12 +645,10 @@ impl DbusObject { self.data.properties.set(key, phd); } - #[allow(dead_code)] pub fn emit_signal<'a, T: Signal<'a>>(&self, signal: &T) { self.socket.emit_signal(&self.data.path, signal); } - #[allow(dead_code)] pub fn path(&self) -> &str { &self.data.path } diff --git a/src/dbus/socket.rs b/src/dbus/socket.rs index 9c4ed3d5..8f7b430a 100644 --- a/src/dbus/socket.rs +++ b/src/dbus/socket.rs @@ -43,7 +43,6 @@ impl DbusSocket { } } - #[allow(dead_code)] pub fn call_noreply<'a, T: MethodCall<'a>>(&self, destination: &str, path: &str, msg: T) { if !self.dead.get() { self.send_call(path, destination, NO_REPLY_EXPECTED, &msg); @@ -135,7 +134,6 @@ impl DbusSocket { } } - #[allow(dead_code)] pub fn add_object( self: &Rc, object: impl Into>, @@ -158,7 +156,6 @@ impl DbusSocket { } } - #[allow(dead_code)] pub fn handle_signal( self: &Rc, sender: Option<&str>, @@ -271,7 +268,6 @@ impl DbusSocket { ); } - #[allow(dead_code)] pub fn emit_signal<'a, T: Signal<'a>>(&self, path: &str, msg: &T) -> u32 { let (msg, serial) = self.format_signal(path, msg); self.bufio.send(msg); diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 9f89b8a2..0ff04a8d 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -76,7 +76,7 @@ use { uapi::{c, Errno, OwnedFd}, }; -const POINTER: u32 = 1; +pub const POINTER: u32 = 1; const KEYBOARD: u32 = 2; #[allow(dead_code)] const TOUCH: u32 = 4; diff --git a/src/ifs/wl_seat/wl_pointer.rs b/src/ifs/wl_seat/wl_pointer.rs index 8677602e..67f5297d 100644 --- a/src/ifs/wl_seat/wl_pointer.rs +++ b/src/ifs/wl_seat/wl_pointer.rs @@ -17,7 +17,7 @@ use { const ROLE: u32 = 0; pub(super) const RELEASED: u32 = 0; -pub(super) const PRESSED: u32 = 1; +pub const PRESSED: u32 = 1; pub const VERTICAL_SCROLL: u32 = 0; pub const HORIZONTAL_SCROLL: u32 = 1; diff --git a/src/it/test_backend.rs b/src/it/test_backend.rs index 45f38ea2..132aa588 100644 --- a/src/it/test_backend.rs +++ b/src/it/test_backend.rs @@ -168,7 +168,7 @@ impl TestBackend { return Err(TestBackendError::NoDrmNode); }; let file = match uapi::open(node.as_path(), c::O_RDWR | c::O_CLOEXEC, 0) { - Ok(f) => f, + Ok(f) => Rc::new(f), Err(e) => { return Err(TestBackendError::OpenDrmNode( node.as_os_str().as_bytes().as_bstr().to_string(), diff --git a/src/macros.rs b/src/macros.rs index da5a4db5..57524d96 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -174,6 +174,40 @@ macro_rules! id { }; } +macro_rules! shared_ids { + ($id:ident) => { + shared_ids!($id, u32); + }; + ($id:ident, $ty:ty) => { + #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] + pub struct $id($ty); + + impl $id { + #[allow(dead_code)] + pub fn raw(&self) -> $ty { + self.0 + } + + #[allow(dead_code)] + pub fn from_raw(id: $ty) -> Self { + Self(id) + } + } + + impl From<$ty> for $id { + fn from(id: $ty) -> Self { + Self(id) + } + } + + impl std::fmt::Display for $id { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } + } + }; +} + macro_rules! linear_ids { ($ids:ident, $id:ident) => { linear_ids!($ids, $id, u32); diff --git a/src/main.rs b/src/main.rs index f7276334..58c89c15 100644 --- a/src/main.rs +++ b/src/main.rs @@ -60,6 +60,7 @@ mod logind; mod object; mod pango; mod pipewire; +mod portal; mod rect; mod render; mod screenshoter; diff --git a/src/pipewire.rs b/src/pipewire.rs index 2c635bc8..af77bc66 100644 --- a/src/pipewire.rs +++ b/src/pipewire.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - pub mod pw_con; pub mod pw_formatter; pub mod pw_ifs; diff --git a/src/pipewire/pw_mem.rs b/src/pipewire/pw_mem.rs index be424daf..6e9a7768 100644 --- a/src/pipewire/pw_mem.rs +++ b/src/pipewire/pw_mem.rs @@ -41,6 +41,7 @@ pub struct PwMemTyped { _phantom: PhantomData, } +#[allow(dead_code)] pub struct PwMemSlice { mem: Rc, range: Range, @@ -134,6 +135,7 @@ impl PwMemMap { } impl PwMemTyped { + #[allow(dead_code)] pub unsafe fn read(&self) -> &T { (self.mem.map.ptr.cast::().add(self.offset) as *const T).deref() } diff --git a/src/portal.rs b/src/portal.rs new file mode 100644 index 00000000..cbcba339 --- /dev/null +++ b/src/portal.rs @@ -0,0 +1,161 @@ +mod ptl_display; +mod ptl_render_ctx; +mod ptl_screencast; +mod ptr_gui; + +use { + crate::{ + async_engine::{AsyncEngine, SpawnedFuture}, + cli::GlobalArgs, + dbus::{ + Dbus, DbusSocket, BUS_DEST, BUS_PATH, DBUS_NAME_FLAG_DO_NOT_QUEUE, + DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER, + }, + io_uring::IoUring, + logger, + pipewire::pw_con::{PwCon, PwConHolder, PwConOwner}, + portal::{ + ptl_display::{watch_displays, PortalDisplay, PortalDisplayId}, + ptl_render_ctx::PortalRenderCtx, + ptl_screencast::{add_screencast_dbus_members, ScreencastSession}, + }, + utils::{ + copyhashmap::CopyHashMap, errorfmt::ErrorFmt, numcell::NumCell, + run_toplevel::RunToplevel, xrd::xrd, + }, + wheel::Wheel, + wire_dbus::org, + }, + std::{ + cell::Cell, + rc::{Rc, Weak}, + }, + uapi::c, +}; + +const PORTAL_SUCCESS: u32 = 0; +#[allow(dead_code)] +const PORTAL_CANCELLED: u32 = 1; +#[allow(dead_code)] +const PORTAL_ENDED: u32 = 2; + +pub fn run(global: GlobalArgs) { + logger::Logger::install_stderr(global.log_level.into()); + let xrd = match xrd() { + Some(xrd) => xrd, + _ => { + fatal!("XDG_RUNTIME_DIR is not set"); + } + }; + let eng = AsyncEngine::new(); + let ring = match IoUring::new(&eng, 32) { + Ok(r) => r, + Err(e) => { + fatal!("Could not create an IO-uring: {}", ErrorFmt(e)); + } + }; + let wheel = match Wheel::new(&eng, &ring) { + Ok(w) => w, + Err(e) => { + fatal!("Could not create a timer wheel: {}", ErrorFmt(e)); + } + }; + let pw_con = match PwConHolder::new(&eng, &ring) { + Ok(p) => p, + Err(e) => { + fatal!("Could not connect to pipewire: {}", ErrorFmt(e)); + } + }; + let (_rtl_future, rtl) = RunToplevel::install(&eng); + let dbus = Dbus::new(&eng, &ring, &rtl); + let dbus = init_dbus_session(&dbus); + let state = Rc::new(PortalState { + xrd, + ring, + eng, + wheel, + pw_con: pw_con.con.clone(), + displays: Default::default(), + watch_displays: Cell::new(None), + dbus, + screencasts: Default::default(), + next_id: NumCell::new(1), + render_ctxs: Default::default(), + }); + let _root = { + let obj = state + .dbus + .add_object("/org/freedesktop/portal/desktop") + .unwrap(); + add_screencast_dbus_members(&state, &obj); + obj + }; + state + .watch_displays + .set(Some(state.eng.spawn(watch_displays(state.clone())))); + state.pw_con.owner.set(Some(state.clone())); + if let Err(e) = state.ring.run() { + fatal!("The IO-uring returned an error: {}", ErrorFmt(e)); + } +} + +const UNIQUE_NAME: &str = "org.freedesktop.impl.portal.desktop.jay"; + +fn init_dbus_session(dbus: &Dbus) -> Rc { + let session = match dbus.session() { + Ok(s) => s, + Err(e) => { + fatal!("Could not connect to dbus session daemon: {}", ErrorFmt(e)); + } + }; + session.call( + BUS_DEST, + BUS_PATH, + org::freedesktop::dbus::RequestName { + name: UNIQUE_NAME.into(), + flags: DBUS_NAME_FLAG_DO_NOT_QUEUE, + }, + |rv| match rv { + Ok(r) if r.rv == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER => { + log::info!("Acquired unique name {}", UNIQUE_NAME); + return; + } + Ok(r) => { + fatal!("Could not acquire unique name {}: {}", UNIQUE_NAME, r.rv); + } + Err(e) => { + fatal!( + "Could not communicate with the session bus: {}", + ErrorFmt(e) + ); + } + }, + ); + session +} + +struct PortalState { + xrd: String, + ring: Rc, + eng: Rc, + wheel: Rc, + pw_con: Rc, + displays: CopyHashMap>, + watch_displays: Cell>>, + dbus: Rc, + screencasts: CopyHashMap>, + next_id: NumCell, + render_ctxs: CopyHashMap>, +} + +impl PortalState { + pub fn id>(&self) -> T { + T::from(self.next_id.fetch_add(1)) + } +} + +impl PwConOwner for PortalState { + fn killed(&self) { + fatal!("The pipewire connection has been closed"); + } +} diff --git a/src/portal/ptl_display.rs b/src/portal/ptl_display.rs new file mode 100644 index 00000000..cf8ad7ab --- /dev/null +++ b/src/portal/ptl_display.rs @@ -0,0 +1,486 @@ +use { + crate::{ + ifs::wl_seat::POINTER, + portal::{ptl_render_ctx::PortalRenderCtx, ptr_gui::WindowData, PortalState}, + render::RenderContext, + utils::{ + bitflags::BitflagsExt, clonecell::CloneCell, copyhashmap::CopyHashMap, + errorfmt::ErrorFmt, oserror::OsError, + }, + video::drm::Drm, + wire::{ + wl_pointer, JayCompositor, WlCompositor, WlOutput, WlSeat, WlSurfaceId, + WpFractionalScaleManagerV1, WpViewporter, ZwlrLayerShellV1, ZwpLinuxDmabufV1, + }, + wl_usr::{ + usr_ifs::{ + usr_jay_compositor::UsrJayCompositor, + usr_jay_output::{UsrJayOutput, UsrJayOutputOwner}, + usr_jay_pointer::UsrJayPointer, + usr_jay_render_ctx::UsrJayRenderCtxOwner, + usr_linux_dmabuf::UsrLinuxDmabuf, + usr_wl_compositor::UsrWlCompositor, + usr_wl_output::{UsrWlOutput, UsrWlOutputOwner}, + usr_wl_pointer::{UsrWlPointer, UsrWlPointerOwner}, + usr_wl_registry::{UsrWlRegistry, UsrWlRegistryOwner}, + usr_wl_seat::{UsrWlSeat, UsrWlSeatOwner}, + usr_wlr_layer_shell::UsrWlrLayerShell, + usr_wp_fractional_scale_manager::UsrWpFractionalScaleManager, + usr_wp_viewporter::UsrWpViewporter, + }, + UsrCon, UsrConOwner, + }, + }, + ahash::AHashMap, + std::{ + cell::{Cell, RefCell}, + ops::Deref, + os::unix::ffi::OsStrExt, + rc::Rc, + str::FromStr, + }, + uapi::{c, AsUstr, OwnedFd}, +}; +use crate::portal::ptl_screencast::ScreencastSession; + +struct PortalDisplayPrelude { + con: Rc, + state: Rc, + registry: Rc, + globals: RefCell>>, +} + +shared_ids!(PortalDisplayId); +pub struct PortalDisplay { + pub id: PortalDisplayId, + pub con: Rc, + pub(super) state: Rc, + registry: Rc, + pub dmabuf: CloneCell>>, + + pub jc: Rc, + pub ls: Rc, + pub comp: Rc, + pub fsm: Rc, + pub vp: Rc, + pub render_ctx: CloneCell>>, + + pub outputs: CopyHashMap>, + pub seats: CopyHashMap>, + + pub windows: CopyHashMap>, + pub screencasts: CopyHashMap>, +} + +pub struct PortalOutput { + pub global_id: u32, + pub dpy: Rc, + pub wl: Rc, + pub jay: Rc, +} + +pub struct PortalSeat { + pub global_id: u32, + pub dpy: Rc, + pub wl: Rc, + pub jay_pointer: Rc, + pub pointer: CloneCell>>, + pub name: RefCell, + pub capabilities: Cell, + pub pointer_focus: CloneCell>>, +} + +impl UsrWlSeatOwner for PortalSeat { + fn name(&self, name: &str) { + *self.name.borrow_mut() = name.to_string(); + } + + fn capabilities(self: Rc, value: u32) { + let old = self.capabilities.replace(value); + if old.contains(POINTER) != value.contains(POINTER) { + if old.contains(POINTER) { + if let Some(pointer) = self.pointer.take() { + pointer.con.remove_obj(pointer.deref()); + } + } else { + let pointer = self.wl.get_pointer(); + pointer.owner.set(Some(self.clone())); + self.pointer.set(Some(pointer)); + } + } + } +} + +impl UsrWlPointerOwner for PortalSeat { + fn enter(&self, ev: &wl_pointer::Enter) { + if let Some(window) = self.dpy.windows.get(&ev.surface) { + self.pointer_focus.set(Some(window.clone())); + window.motion(self, ev.surface_x, ev.surface_y, true); + } + } + + fn leave(&self, _ev: &wl_pointer::Leave) { + self.pointer_focus.take(); + } + + fn motion(&self, ev: &wl_pointer::Motion) { + if let Some(window) = self.pointer_focus.get() { + window.motion(self, ev.surface_x, ev.surface_y, false); + } + } + + fn button(&self, ev: &wl_pointer::Button) { + if let Some(window) = self.pointer_focus.get() { + window.button(self, ev.button, ev.state); + } + } +} + +impl UsrWlRegistryOwner for PortalDisplayPrelude { + fn global(self: Rc, name: u32, interface: &str, version: u32) { + self.globals + .borrow_mut() + .entry(interface.to_string()) + .or_default() + .push((name, version)); + } +} + +impl UsrJayRenderCtxOwner for PortalDisplay { + fn no_device(&self) { + self.render_ctx.take(); + } + + fn device(&self, fd: Rc) { + self.render_ctx.take(); + let dev_id = match uapi::fstat(fd.raw()) { + Ok(s) => s.st_dev, + Err(e) => { + log::error!("Could not fstat display device: {}", ErrorFmt(e)); + return; + } + }; + if let Some(ctx) = self.state.render_ctxs.get(&dev_id) { + if let Some(ctx) = ctx.upgrade() { + self.render_ctx.set(Some(ctx)); + } + } + if self.render_ctx.get().is_none() { + let drm = Drm::open_existing(fd); + let ctx = match RenderContext::from_drm_device(&drm) { + Ok(c) => c, + Err(e) => { + log::error!( + "Could not create render context from drm device: {}", + ErrorFmt(e) + ); + return; + } + }; + let ctx = Rc::new(PortalRenderCtx { + dev_id, + ctx: Rc::new(ctx), + }); + self.render_ctx.set(Some(ctx.clone())); + self.state.render_ctxs.set(dev_id, Rc::downgrade(&ctx)); + } + } +} + +impl UsrConOwner for PortalDisplay { + fn killed(&self) { + log::info!("Removing display {}", self.id); + for (_, sc) in self.screencasts.lock().drain() { + sc.kill(); + } + self.windows.clear(); + self.state.displays.remove(&self.id); + } +} + +impl UsrWlRegistryOwner for PortalDisplay { + fn global(self: Rc, name: u32, interface: &str, version: u32) { + if interface == WlOutput.name() { + add_output(&self, name, version); + } else if interface == WlSeat.name() { + add_seat(&self, name, version); + } else if interface == ZwpLinuxDmabufV1.name() { + let ls = Rc::new(UsrLinuxDmabuf { + id: self.con.id(), + con: self.con.clone(), + owner: Default::default(), + }); + self.con.add_object(ls.clone()); + self.registry.request_bind(name, version, ls.deref()); + self.dmabuf.set(Some(ls)); + } + } +} + +impl UsrJayOutputOwner for PortalOutput { + fn destroyed(&self) { + log::info!( + "Display {}: Output {} removed", + self.dpy.con.server_id, + self.global_id, + ); + self.dpy.outputs.remove(&self.global_id); + self.dpy.con.remove_obj(self.wl.deref()); + self.dpy.con.remove_obj(self.jay.deref()); + } +} + +impl UsrWlOutputOwner for PortalOutput {} + +fn maybe_add_display(state: &Rc, name: &str) { + let tail = match name.strip_prefix("wayland-") { + Some(t) => t, + _ => return, + }; + let head = match tail.strip_suffix(".jay") { + Some(h) => h, + _ => return, + }; + let num = match u32::from_str(head) { + Ok(n) => n, + _ => return, + }; + let path = format!("{}/{}", state.xrd, name); + let con = match UsrCon::new(&state.ring, &state.wheel, &state.eng, &path, num) { + Ok(c) => c, + Err(e) => { + log::error!( + "Could not connect to wayland display {}: {}", + name, + ErrorFmt(e) + ); + return; + } + }; + let registry = con.get_registry(); + let dpy = Rc::new(PortalDisplayPrelude { + con: con.clone(), + state: state.clone(), + registry, + globals: Default::default(), + }); + dpy.registry.owner.set(Some(dpy.clone())); + con.sync(move || { + finish_display_connect(dpy); + }); + log::info!("Connected to wayland display {num}: {name}"); +} + +fn finish_display_connect(dpy: Rc) { + let mut jc_opt = None; + let mut ls_opt = None; + let mut fsm_opt = None; + let mut comp_opt = None; + let mut vp_opt = None; + let mut dmabuf_opt = None; + let mut outputs = vec![]; + let mut seats = vec![]; + for (interface, instances) in dpy.globals.borrow_mut().deref() { + for &(name, version) in instances { + if interface == JayCompositor.name() { + let jc = Rc::new(UsrJayCompositor { + id: dpy.con.id(), + con: dpy.con.clone(), + owner: Default::default(), + }); + dpy.con.add_object(jc.clone()); + dpy.registry.request_bind(name, version, jc.deref()); + jc_opt = Some(jc); + } else if interface == WpFractionalScaleManagerV1.name() { + let ls = Rc::new(UsrWpFractionalScaleManager { + id: dpy.con.id(), + con: dpy.con.clone(), + }); + dpy.con.add_object(ls.clone()); + dpy.registry.request_bind(name, version, ls.deref()); + fsm_opt = Some(ls); + } else if interface == ZwlrLayerShellV1.name() { + let ls = Rc::new(UsrWlrLayerShell { + id: dpy.con.id(), + con: dpy.con.clone(), + }); + dpy.con.add_object(ls.clone()); + dpy.registry.request_bind(name, version, ls.deref()); + ls_opt = Some(ls); + } else if interface == WpViewporter.name() { + let ls = Rc::new(UsrWpViewporter { + id: dpy.con.id(), + con: dpy.con.clone(), + }); + dpy.con.add_object(ls.clone()); + dpy.registry.request_bind(name, version, ls.deref()); + vp_opt = Some(ls); + } else if interface == WlCompositor.name() { + let ls = Rc::new(UsrWlCompositor { + id: dpy.con.id(), + con: dpy.con.clone(), + }); + dpy.con.add_object(ls.clone()); + dpy.registry.request_bind(name, version, ls.deref()); + comp_opt = Some(ls); + } else if interface == ZwpLinuxDmabufV1.name() { + let ls = Rc::new(UsrLinuxDmabuf { + id: dpy.con.id(), + con: dpy.con.clone(), + owner: Default::default(), + }); + dpy.con.add_object(ls.clone()); + dpy.registry.request_bind(name, version, ls.deref()); + dmabuf_opt = Some(ls); + } else if interface == WlOutput.name() { + outputs.push((name, version)); + } else if interface == WlSeat.name() { + seats.push((name, version)); + } + } + } + macro_rules! get { + ($opt:expr, $ty:expr) => { + match $opt { + Some(c) => c, + _ => { + log::error!("Compositor did not advertise a {}", $ty.name()); + dpy.con.kill(); + return; + } + } + }; + } + let jc = get!(jc_opt, JayCompositor); + let ls = get!(ls_opt, ZwlrLayerShellV1); + let comp = get!(comp_opt, WlCompositor); + let fsm = get!(fsm_opt, WpFractionalScaleManagerV1); + let vp = get!(vp_opt, WpViewporter); + + let dpy = Rc::new(PortalDisplay { + id: dpy.state.id(), + con: dpy.con.clone(), + state: dpy.state.clone(), + registry: dpy.registry.clone(), + dmabuf: CloneCell::new(dmabuf_opt), + jc, + outputs: Default::default(), + render_ctx: Default::default(), + seats: Default::default(), + ls, + comp, + fsm, + vp, + windows: Default::default(), + screencasts: Default::default(), + }); + + dpy.state.displays.set(dpy.id, dpy.clone()); + dpy.con.owner.set(Some(dpy.clone())); + dpy.registry.owner.set(Some(dpy.clone())); + + let jrc = dpy.jc.get_render_context(); + jrc.owner.set(Some(dpy.clone())); + + for (name, version) in outputs { + add_output(&dpy, name, version); + } + for (name, version) in seats { + add_seat(&dpy, name, version); + } + log::info!("Display {} initialized", dpy.id); +} + +fn add_seat(dpy: &Rc, name: u32, version: u32) { + let wl = Rc::new(UsrWlSeat { + id: dpy.con.id(), + con: dpy.con.clone(), + owner: Default::default(), + }); + dpy.con.add_object(wl.clone()); + dpy.registry.request_bind(name, version, wl.deref()); + let jay_pointer = dpy.jc.get_pointer(&wl); + let js = Rc::new(PortalSeat { + global_id: name, + dpy: dpy.clone(), + wl, + jay_pointer, + pointer: Default::default(), + name: RefCell::new("".to_string()), + capabilities: Cell::new(0), + pointer_focus: Default::default(), + }); + js.wl.owner.set(Some(js.clone())); + dpy.seats.set(name, js); +} + +fn add_output(dpy: &Rc, name: u32, version: u32) { + let wl = Rc::new(UsrWlOutput { + id: dpy.con.id(), + con: dpy.con.clone(), + owner: Default::default(), + }); + dpy.con.add_object(wl.clone()); + dpy.registry.request_bind(name, version, wl.deref()); + let jo = dpy.jc.get_output(&wl); + let po = Rc::new(PortalOutput { + global_id: name, + dpy: dpy.clone(), + wl: wl.clone(), + jay: jo.clone(), + }); + po.wl.owner.set(Some(po.clone())); + po.jay.owner.set(Some(po.clone())); + dpy.outputs.set(name, po); +} + +pub(super) async fn watch_displays(state: Rc) { + let inotify = Rc::new(uapi::inotify_init1(c::IN_CLOEXEC | c::IN_NONBLOCK).unwrap()); + if let Err(e) = uapi::inotify_add_watch(inotify.raw(), state.xrd.as_str(), c::IN_CREATE) { + log::error!( + "Cannot watch directory `{}`: {}", + state.xrd, + ErrorFmt(OsError::from(e)) + ); + return; + } + let rd = match std::fs::read_dir(&state.xrd) { + Ok(rd) => rd, + Err(e) => { + log::error!("Cannot enumerate `{}`: {}", state.xrd, ErrorFmt(e)); + return; + } + }; + for entry in rd { + let entry = match entry { + Ok(e) => e, + Err(e) => { + log::error!("Cannot enumerate `{}`: {}", state.xrd, ErrorFmt(e)); + return; + } + }; + if let Ok(s) = std::str::from_utf8(entry.file_name().as_bytes()) { + maybe_add_display(&state, s); + } + } + let mut buf = vec![0u8; 4096]; + loop { + if let Err(e) = state.ring.readable(&inotify).await { + log::error!("Cannot wait for `{}` to change: {}", state.xrd, ErrorFmt(e)); + } + let events = match uapi::inotify_read(inotify.raw(), &mut buf[..]) { + Ok(s) => s, + Err(e) => { + log::error!("Could not read from inotify fd: {}", ErrorFmt(e)); + return; + } + }; + for event in events { + if event.mask.contains(c::IN_CREATE) { + if let Ok(s) = std::str::from_utf8(event.name().as_ustr().as_bytes()) { + maybe_add_display(&state, s); + } + } + } + } +} diff --git a/src/portal/ptl_render_ctx.rs b/src/portal/ptl_render_ctx.rs new file mode 100644 index 00000000..f4589fdf --- /dev/null +++ b/src/portal/ptl_render_ctx.rs @@ -0,0 +1,6 @@ +use {crate::render::RenderContext, std::rc::Rc, uapi::c}; + +pub struct PortalRenderCtx { + pub dev_id: c::dev_t, + pub ctx: Rc, +} diff --git a/src/portal/ptl_screencast.rs b/src/portal/ptl_screencast.rs new file mode 100644 index 00000000..be779c64 --- /dev/null +++ b/src/portal/ptl_screencast.rs @@ -0,0 +1,450 @@ +mod screencast_gui; + +use { + crate::{ + dbus::{prelude::Variant, DbusObject, DictEntry, DynamicType, PendingReply}, + pipewire::{ + pw_ifs::pw_client_node::{ + PwClientNode, PwClientNodeBufferConfig, PwClientNodeOwner, PwClientNodePort, + PwClientNodePortSupportedFormats, SUPPORTED_META_VIDEO_CROP, + }, + pw_pod::{ + spa_point, spa_rectangle, spa_region, PwPodRectangle, SPA_DATA_DmaBuf, + SPA_MEDIA_SUBTYPE_raw, SPA_MEDIA_TYPE_video, SpaChunkFlags, SPA_STATUS_HAVE_DATA, + }, + }, + portal::{ + ptl_display::{PortalDisplay, PortalDisplayId, PortalOutput}, + ptl_screencast::screencast_gui::SelectionGui, + PortalState, PORTAL_SUCCESS, + }, + utils::{ + clonecell::{CloneCell, UnsafeCellCloneSafe}, + copyhashmap::CopyHashMap, + }, + video::dmabuf::DmaBuf, + wire::jay_screencast::Ready, + wire_dbus::{ + org, + org::freedesktop::impl_::portal::{ + screen_cast::{ + CreateSession, CreateSessionReply, SelectSources, SelectSourcesReply, Start, + StartReply, + }, + session::{CloseReply as SessionCloseReply, Closed}, + }, + }, + wl_usr::usr_ifs::usr_jay_screencast::{UsrJayScreencast, UsrJayScreencastOwner}, + }, + std::{ + borrow::Cow, + cell::{Cell, RefCell}, + ops::Deref, + rc::Rc, + }, +}; + +shared_ids!(ScreencastSessionId); +pub struct ScreencastSession { + _id: ScreencastSessionId, + state: Rc, + pub app: String, + session_obj: DbusObject, + pub phase: CloneCell, +} + +#[derive(Clone)] +pub enum ScreencastPhase { + Init, + SourcesSelected, + Selecting(Rc), + Starting(Rc), + Started(Rc), + Terminated, +} + +unsafe impl UnsafeCellCloneSafe for ScreencastPhase {} + +pub struct SelectingScreencast { + pub session: Rc, + pub request_obj: Rc, + pub reply: Rc>>, + pub guis: CopyHashMap>, + pub output_selected: Cell, +} + +pub struct StartingScreencast { + pub session: Rc, + pub request_obj: Rc, + pub reply: Rc>>, + pub node: Rc, + pub dpy: Rc, + pub output: Rc, +} + +pub struct StartedScreencast { + session: Rc, + node: Rc, + port: Rc, + buffers: RefCell>, + dpy: Rc, + jay_screencast: Rc, +} + +bitflags! { + CursorModes: u32; + + HIDDEN = 1, + EMBEDDED = 2, + METADATA = 4, +} + +bitflags! { + SourceTypes: u32; + + MONITOR = 1, + WINDOW = 2, +} + +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 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 port = self.node.create_port(true); + port.can_alloc_buffers.set(true); + port.supported_metas.set(SUPPORTED_META_VIDEO_CROP); + let jsc = self.dpy.jc.create_screencast(); + jsc.set_output(&self.output.jay); + jsc.set_use_linear_buffers(true); + jsc.set_allow_all_workspaces(true); + jsc.configure(); + let started = Rc::new(StartedScreencast { + session: self.session.clone(), + node: self.node.clone(), + port, + buffers: Default::default(), + dpy: self.dpy.clone(), + jay_screencast: jsc, + }); + self.session + .phase + .set(ScreencastPhase::Started(started.clone())); + started.jay_screencast.owner.set(Some(started.clone())); + self.node.owner.set(Some(started.clone())); + } +} + +impl PwClientNodeOwner for StartedScreencast { + fn port_format_changed(&self, port: &Rc) { + self.node.send_port_update(port); + } + + fn use_buffers(&self, port: &Rc) { + self.node + .send_port_output_buffers(port, &self.buffers.borrow_mut()); + } + + fn start(self: Rc) { + self.jay_screencast.set_running(true); + self.jay_screencast.configure(); + } + + fn pause(self: Rc) { + self.jay_screencast.set_running(false); + self.jay_screencast.configure(); + } + + fn suspend(self: Rc) { + self.jay_screencast.set_running(false); + self.jay_screencast.configure(); + } +} + +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.reply.err("Session has been terminated"); + for (_, gui) in s.guis.lock().drain() { + gui.kill(false); + } + } + 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()); + } + 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()); + } + } + } + + fn dbus_select_sources( + self: &Rc, + _req: SelectSources, + reply: PendingReply>, + ) { + match self.phase.get() { + ScreencastPhase::Init => {} + _ => { + self.kill(); + reply.err("Sources have already been selected"); + return; + } + } + self.phase.set(ScreencastPhase::SourcesSelected); + reply.ok(&SelectSourcesReply { + response: PORTAL_SUCCESS, + results: Default::default(), + }); + } + + fn dbus_start(self: &Rc, req: Start<'_>, reply: PendingReply>) { + match self.phase.get() { + ScreencastPhase::SourcesSelected => {} + _ => { + 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 { + 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(ScreencastPhase::Selecting(Rc::new(SelectingScreencast { + session: self.clone(), + request_obj: Rc::new(request_obj), + reply: Rc::new(reply), + guis, + output_selected: Cell::new(false), + }))); + } +} + +impl UsrJayScreencastOwner for StartedScreencast { + fn buffers(&self, buffers: Vec) { + if buffers.len() == 0 { + return; + } + let buffer = &buffers[0]; + *self.port.supported_formats.borrow_mut() = Some(PwClientNodePortSupportedFormats { + media_type: Some(SPA_MEDIA_TYPE_video), + media_sub_type: Some(SPA_MEDIA_SUBTYPE_raw), + video_size: Some(PwPodRectangle { + width: buffer.width as _, + height: buffer.height as _, + }), + formats: vec![buffer.format], + modifiers: vec![buffer.modifier], + }); + let bc = PwClientNodeBufferConfig { + num_buffers: buffers.len(), + planes: buffer.planes.len(), + stride: Some(buffer.planes[0].stride), + size: Some(buffer.planes[0].stride * buffer.height as u32), + align: 16, + data_type: SPA_DATA_DmaBuf, + }; + self.port.buffer_config.set(Some(bc)); + self.node.send_port_update(&self.port); + self.node.send_active(true); + *self.buffers.borrow_mut() = buffers; + } + + fn ready(&self, ev: &Ready) { + let idx = ev.idx as usize; + unsafe { + let mut used = false; + if let Some(io) = self.port.io_buffers.lock().values().next() { + let io = io.write(); + if io.status != SPA_STATUS_HAVE_DATA { + used = true; + if io.buffer_id != ev.idx { + if (io.buffer_id as usize) < self.buffers.borrow_mut().len() { + self.jay_screencast.release_buffer(io.buffer_id as usize); + } + } + io.buffer_id = ev.idx; + io.status = SPA_STATUS_HAVE_DATA; + } + } + if !used { + self.jay_screencast.release_buffer(idx); + } + { + let pbuffers = self.port.buffers.borrow_mut(); + let buffers = self.buffers.borrow_mut(); + if let Some(pbuffer) = pbuffers.get(idx) { + let buffer = &buffers[idx]; + for (chunk, plane) in pbuffer.chunks.iter().zip(buffer.planes.iter()) { + let chunk = chunk.write(); + chunk.flags = SpaChunkFlags::none(); + chunk.offset = plane.offset; + chunk.stride = plane.stride; + chunk.size = plane.stride * buffer.height as u32; + } + if let Some(crop) = &pbuffer.meta_video_crop { + crop.write().region = spa_region { + position: spa_point { x: 0, y: 0 }, + size: spa_rectangle { + width: buffer.width as _, + height: buffer.height as _, + }, + }; + } + } + } + } + if let Some(wfd) = self.port.node.transport_out.get() { + let _ = uapi::eventfd_write(wfd.raw(), 1); + } + } + + fn destroyed(&self) { + self.session.kill(); + } +} + +pub(super) fn add_screencast_dbus_members(state_: &Rc, object: &DbusObject) { + use org::freedesktop::impl_::portal::screen_cast::*; + 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_sources(&state, req, pr); + }); + let state = state_.clone(); + object.add_method::(move |req, pr| { + dbus_start(&state, req, pr); + }); + object.set_property::(Variant::U32(MONITOR.0)); + object.set_property::(Variant::U32(EMBEDDED.0)); + object.set_property::(Variant::U32(4)); +} + +fn dbus_create_session( + state: &Rc, + req: CreateSession, + reply: PendingReply>, +) { + log::info!("Create Session {:#?}", req); + if state.screencasts.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(ScreencastSession { + _id: state.id(), + state: state.clone(), + app: req.app_id.to_string(), + session_obj: obj, + phase: CloneCell::new(ScreencastPhase::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(4)); + } + state + .screencasts + .set(req.session_handle.0.to_string(), session); + reply.ok(&CreateSessionReply { + response: PORTAL_SUCCESS, + results: Default::default(), + }); +} + +fn dbus_select_sources( + state: &Rc, + req: SelectSources, + reply: PendingReply>, +) { + if let Some(s) = get_session(state, &reply, &req.session_handle.0) { + s.dbus_select_sources(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 get_session( + state: &Rc, + reply: &PendingReply, + handle: &str, +) -> Option> { + let res = state.screencasts.get(handle); + if res.is_none() { + let msg = format!("Screencast session `{}` does not exist", handle); + reply.err(&msg); + } + res +} diff --git a/src/portal/ptl_screencast/screencast_gui.rs b/src/portal/ptl_screencast/screencast_gui.rs new file mode 100644 index 00000000..df4f1247 --- /dev/null +++ b/src/portal/ptl_screencast/screencast_gui.rs @@ -0,0 +1,182 @@ +use { + crate::{ + ifs::wl_seat::{wl_pointer::PRESSED, BTN_LEFT}, + portal::{ + ptl_display::{PortalDisplay, PortalOutput}, + ptl_screencast::{ScreencastPhase, ScreencastSession, StartingScreencast}, + ptr_gui::{ + Align, Button, ButtonOwner, Flow, GuiElement, Label, Orientation, OverlayWindow, + OverlayWindowOwner, + }, + }, + theme::Color, + utils::copyhashmap::CopyHashMap, + }, + std::rc::Rc, +}; + +const H_MARGIN: f32 = 30.0; +const V_MARGIN: f32 = 20.0; + +pub struct SelectionGui { + screencast_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() { + surface.overlay.data.kill(false); + } + if let ScreencastPhase::Selecting(s) = self.screencast_session.phase.get() { + s.guis.remove(&self.dpy.id); + if upwards && s.guis.is_empty() { + self.screencast_session.kill(); + } + } + } +} + +fn create_accept_gui(surface: &Rc) -> Rc { + let app = &surface.gui.screencast_session.app; + let text = if app.is_empty() { + format!("An application wants to capture the screen") + } else { + format!("`{}` wants to capture the screen", app) + }; + let label = Rc::new(Label::default()); + *label.text.borrow_mut() = text; + let accept_button = static_button(surface, ButtonRole::Accept, "Share This Output"); + let reject_button = static_button(surface, ButtonRole::Reject, "Reject"); + let buttons = [&accept_button, &reject_button]; + for button in buttons { + 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 { + screencast_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, 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.screencast_session.phase.get() { + ScreencastPhase::Selecting(selecting) => selecting, + _ => return, + }; + for (_, gui) in selecting.guis.lock().drain() { + gui.kill(false); + } + let node = self.surface.gui.dpy.state.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()), + ]); + let starting = Rc::new(StartingScreencast { + session: self.surface.gui.screencast_session.clone(), + request_obj: selecting.request_obj.clone(), + reply: selecting.reply.clone(), + node, + dpy: self.surface.gui.dpy.clone(), + output: self.surface.output.clone(), + }); + self.surface + .gui + .screencast_session + .phase + .set(ScreencastPhase::Starting(starting.clone())); + starting.node.owner.set(Some(starting.clone())); + self.surface.gui.dpy.screencasts.set( + self.surface.gui.screencast_session.session_obj.path().to_owned(), + self.surface.gui.screencast_session.clone(), + ); + } + ButtonRole::Reject => { + log::info!("User has rejected the screencast request"); + self.surface.gui.screencast_session.kill(); + } + } + } +} + +fn static_button(surface: &Rc, role: ButtonRole, text: &str) -> Rc