From 4e10415e5cde33e293574bee2bed6144e3b65dbc Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Fri, 19 Apr 2024 12:12:49 +0200 Subject: [PATCH] portal: implement window capture --- release-notes.md | 1 + src/client/objects.rs | 12 +- src/compositor.rs | 5 + src/ifs.rs | 2 + src/ifs/jay_compositor.rs | 38 ++- src/ifs/jay_screencast.rs | 282 ++++++++++++++---- src/ifs/jay_select_toplevel.rs | 100 +++++++ src/ifs/jay_toplevel.rs | 68 +++++ src/ifs/wl_surface.rs | 37 ++- src/portal/ptl_display.rs | 1 + src/portal/ptl_screencast.rs | 80 ++++- src/portal/ptl_screencast/screencast_gui.rs | 96 +++--- src/portal/ptr_gui.rs | 6 +- src/renderer.rs | 23 +- src/scale.rs | 11 + src/state.rs | 6 + src/tree/output.rs | 16 +- src/tree/toplevel.rs | 22 +- src/wl_usr/usr_ifs.rs | 2 + src/wl_usr/usr_ifs/usr_jay_compositor.rs | 20 +- src/wl_usr/usr_ifs/usr_jay_screencast.rs | 13 +- src/wl_usr/usr_ifs/usr_jay_select_toplevel.rs | 71 +++++ src/wl_usr/usr_ifs/usr_jay_toplevel.rs | 47 +++ wire/jay_compositor.txt | 5 + wire/jay_screencast.txt | 4 + wire/jay_select_toplevel.txt | 3 + wire/jay_toplevel.txt | 5 + 27 files changed, 840 insertions(+), 136 deletions(-) create mode 100644 src/ifs/jay_select_toplevel.rs create mode 100644 src/ifs/jay_toplevel.rs create mode 100644 src/wl_usr/usr_ifs/usr_jay_select_toplevel.rs create mode 100644 src/wl_usr/usr_ifs/usr_jay_toplevel.rs create mode 100644 wire/jay_select_toplevel.txt create mode 100644 wire/jay_toplevel.txt diff --git a/release-notes.md b/release-notes.md index 08ef449a..b30c29d0 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,5 +1,6 @@ # Unreleased +- Screencasts now support window capture. - Add support for wp-alpha-modifier. - Add support for per-device keymaps. - Add support for virtual-keyboard-unstable-v1. diff --git a/src/client/objects.rs b/src/client/objects.rs index 813a38bf..def03614 100644 --- a/src/client/objects.rs +++ b/src/client/objects.rs @@ -8,6 +8,7 @@ use { }, jay_output::JayOutput, jay_screencast::JayScreencast, + jay_toplevel::JayToplevel, jay_workspace::JayWorkspace, wl_buffer::WlBuffer, wl_display::WlDisplay, @@ -29,10 +30,10 @@ use { copyhashmap::{CopyHashMap, Locked}, }, wire::{ - JayOutputId, JayScreencastId, JayWorkspaceId, WlBufferId, WlDataSourceId, WlOutputId, - WlPointerId, WlRegionId, WlRegistryId, WlSeatId, WlSurfaceId, - WpLinuxDrmSyncobjTimelineV1Id, XdgPositionerId, XdgSurfaceId, XdgToplevelId, - XdgWmBaseId, ZwlrDataControlSourceV1Id, ZwpPrimarySelectionSourceV1Id, + JayOutputId, JayScreencastId, JayToplevelId, JayWorkspaceId, WlBufferId, + WlDataSourceId, WlOutputId, WlPointerId, WlRegionId, WlRegistryId, WlSeatId, + WlSurfaceId, WpLinuxDrmSyncobjTimelineV1Id, XdgPositionerId, XdgSurfaceId, + XdgToplevelId, XdgWmBaseId, ZwlrDataControlSourceV1Id, ZwpPrimarySelectionSourceV1Id, }, }, std::{cell::RefCell, mem, rc::Rc}, @@ -60,6 +61,7 @@ pub struct Objects { pub screencasts: CopyHashMap>, pub timelines: CopyHashMap>, pub zwlr_data_sources: CopyHashMap>, + pub jay_toplevels: CopyHashMap>, ids: RefCell>, } @@ -89,6 +91,7 @@ impl Objects { screencasts: Default::default(), timelines: Default::default(), zwlr_data_sources: Default::default(), + jay_toplevels: Default::default(), ids: RefCell::new(vec![]), } } @@ -122,6 +125,7 @@ impl Objects { self.screencasts.clear(); self.timelines.clear(); self.zwlr_data_sources.clear(); + self.jay_toplevels.clear(); } pub fn id(&self, client_data: &Client) -> Result diff --git a/src/compositor.rs b/src/compositor.rs index f3b358fc..64a05570 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -17,6 +17,7 @@ use { forker, globals::Globals, ifs::{ + jay_screencast::{perform_screencast_realloc, perform_toplevel_screencasts}, wl_output::{OutputId, PersistentOutputState, WlOutputGlobal}, wl_surface::{zwp_input_popup_surface_v2::input_popup_positioning, NoneSurfaceExt}, }, @@ -173,6 +174,8 @@ fn start_compositor2( pending_float_layout: Default::default(), pending_float_titles: Default::default(), pending_input_popup_positioning: Default::default(), + pending_toplevel_screencasts: Default::default(), + pending_toplevel_screencast_reallocs: Default::default(), dbus: Dbus::new(&engine, &ring, &run_toplevel), fdcloser: FdCloser::new(), logger: logger.clone(), @@ -329,6 +332,8 @@ fn start_global_event_handlers( eng.spawn2(Phase::PostLayout, float_titles(state.clone())), eng.spawn2(Phase::PostLayout, idle(state.clone(), backend.clone())), eng.spawn2(Phase::PostLayout, input_popup_positioning(state.clone())), + eng.spawn2(Phase::Present, perform_toplevel_screencasts(state.clone())), + eng.spawn2(Phase::PostLayout, perform_screencast_realloc(state.clone())), ] } diff --git a/src/ifs.rs b/src/ifs.rs index ed153a84..5625f1dc 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -16,6 +16,8 @@ pub mod jay_render_ctx; pub mod jay_screencast; pub mod jay_screenshot; pub mod jay_seat_events; +pub mod jay_select_toplevel; +pub mod jay_toplevel; pub mod jay_workspace; pub mod jay_workspace_watcher; pub mod org_kde_kwin_server_decoration; diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index d529ebfc..2aef869a 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -4,10 +4,17 @@ use { client::{Client, ClientError}, globals::{Global, GlobalName}, ifs::{ - jay_idle::JayIdle, jay_input::JayInput, jay_log_file::JayLogFile, - jay_output::JayOutput, jay_pointer::JayPointer, jay_randr::JayRandr, - jay_render_ctx::JayRenderCtx, jay_screencast::JayScreencast, - jay_screenshot::JayScreenshot, jay_seat_events::JaySeatEvents, + jay_idle::JayIdle, + jay_input::JayInput, + jay_log_file::JayLogFile, + jay_output::JayOutput, + jay_pointer::JayPointer, + jay_randr::JayRandr, + jay_render_ctx::JayRenderCtx, + jay_screencast::JayScreencast, + jay_screenshot::JayScreenshot, + jay_seat_events::JaySeatEvents, + jay_select_toplevel::{JaySelectToplevel, JayToplevelSelector}, jay_workspace_watcher::JayWorkspaceWatcher, }, leaks::Tracker, @@ -18,7 +25,7 @@ use { }, bstr::ByteSlice, log::Level, - std::{ops::Deref, rc::Rc}, + std::{cell::Cell, ops::Deref, rc::Rc}, thiserror::Error, }; @@ -77,13 +84,14 @@ pub struct Cap; impl Cap { pub const NONE: u16 = 0; + pub const WINDOW_CAPTURE: u16 = 1; } impl JayCompositor { fn send_capabilities(&self) { self.client.event(Capabilities { self_id: self.id, - cap: &[Cap::NONE], + cap: &[Cap::NONE, Cap::WINDOW_CAPTURE], }); } @@ -336,6 +344,24 @@ impl JayCompositorRequestHandler for JayCompositor { self.client.add_client_obj(&sc)?; Ok(()) } + + fn select_toplevel(&self, req: SelectToplevel, _slf: &Rc) -> 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), + }); + track!(self.client, obj); + self.client.add_client_obj(&obj)?; + let selector = JayToplevelSelector { + tl: Default::default(), + jst: obj.clone(), + }; + seat.global.select_toplevel(selector); + Ok(()) + } } object_base! { diff --git a/src/ifs/jay_screencast.rs b/src/ifs/jay_screencast.rs index 89da2708..bcee7256 100644 --- a/src/ifs/jay_screencast.rs +++ b/src/ifs/jay_screencast.rs @@ -3,12 +3,17 @@ use { client::{Client, ClientError}, format::XRGB8888, gfx_api::{GfxContext, GfxError, GfxFramebuffer, GfxTexture}, - ifs::jay_output::JayOutput, + ifs::{jay_output::JayOutput, jay_toplevel::JayToplevel}, leaks::Tracker, object::{Object, Version}, - tree::{OutputNode, WorkspaceNodeId}, + scale::Scale, + state::State, + tree::{OutputNode, ToplevelNode, WorkspaceNodeId}, utils::{ - clonecell::CloneCell, errorfmt::ErrorFmt, numcell::NumCell, option_ext::OptionExt, + clonecell::{CloneCell, UnsafeCellCloneSafe}, + errorfmt::ErrorFmt, + numcell::NumCell, + option_ext::OptionExt, }, video::{ dmabuf::DmaBuf, @@ -19,6 +24,7 @@ use { }, ahash::AHashSet, indexmap::{indexset, IndexSet}, + jay_config::video::Transform, once_cell::sync::Lazy, std::{ cell::{Cell, RefCell}, @@ -28,6 +34,28 @@ use { thiserror::Error, }; +pub async fn perform_toplevel_screencasts(state: Rc) { + loop { + let screencast = state.pending_toplevel_screencasts.pop().await; + screencast.perform_toplevel_screencast(); + } +} + +pub async fn perform_screencast_realloc(state: Rc) { + loop { + let screencast = state.pending_toplevel_screencast_reallocs.pop().await; + screencast.realloc_scheduled.set(false); + match state.render_ctx.get() { + None => screencast.do_destroy(), + Some(ctx) => { + if let Err(e) = screencast.realloc(&ctx) { + screencast.client.error(e); + } + } + } + } +} + pub struct JayScreencast { pub id: JayScreencastId, pub client: Rc, @@ -38,20 +66,35 @@ pub struct JayScreencast { buffers_acked: Cell, buffers: RefCell>, missed_frame: Cell, - output: CloneCell>>, + target: CloneCell>, destroyed: Cell, running: Cell, show_all: Cell, show_workspaces: RefCell>, linear: Cell, pending: Pending, + need_realloc: Cell, + realloc_scheduled: Cell, +} + +#[derive(Clone)] +enum Target { + Output(Rc), + Toplevel(Rc), +} + +unsafe impl UnsafeCellCloneSafe for Target {} + +enum PendingTarget { + Output(Rc), + Toplevel(Rc), } #[derive(Default)] struct Pending { linear: Cell>, running: Cell>, - output: Cell>>>, + target: Cell>>, show_all: Cell>, show_workspaces: RefCell>>, } @@ -71,19 +114,80 @@ impl JayScreencast { config_serial: Default::default(), config_acked: Cell::new(true), buffers_serial: Default::default(), - buffers_acked: Cell::new(false), + buffers_acked: Cell::new(true), buffers: Default::default(), missed_frame: Cell::new(false), - output: Default::default(), + target: Default::default(), destroyed: Cell::new(false), running: Cell::new(false), show_all: Cell::new(false), show_workspaces: Default::default(), linear: Cell::new(false), pending: Default::default(), + need_realloc: Cell::new(false), + realloc_scheduled: Cell::new(false), } } + pub fn schedule_toplevel_screencast(self: &Rc) { + if !self.running.get() { + return; + } + self.client + .state + .pending_toplevel_screencasts + .push(self.clone()); + } + + fn perform_toplevel_screencast(&self) { + if self.destroyed.get() || !self.running.get() { + return; + } + let Some(target) = self.target.get() else { + return; + }; + let Target::Toplevel(tl) = target else { + log::warn!("Tried to perform window screencast for output screencast"); + return; + }; + let scale = match tl.tl_data().workspace.get() { + None => Scale::default(), + Some(w) => w.output.get().global.persistent.scale.get(), + }; + let mut buffer = self.buffers.borrow_mut(); + for (idx, buffer) in buffer.deref_mut().iter_mut().enumerate() { + if buffer.free { + let res = buffer.fb.render_node( + tl.tl_as_node(), + &self.client.state, + Some(tl.node_absolute_position()), + None, + scale, + true, + true, + false, + Transform::None, + ); + match res { + Ok(_) => { + self.client.event(Ready { + self_id: self.id, + idx: idx as _, + }); + buffer.free = false; + return; + } + Err(e) => { + log::error!("Could not perform window copy: {}", ErrorFmt(e)); + break; + } + } + } + } + self.missed_frame.set(true); + self.client.event(MissedFrame { self_id: self.id }) + } + fn send_buffers(&self) { self.buffers_acked.set(false); let serial = self.buffers_serial.fetch_add(1) + 1; @@ -115,11 +219,13 @@ impl JayScreencast { fn send_config(&self) { self.config_acked.set(false); let serial = self.config_serial.fetch_add(1) + 1; - if let Some(output) = self.output.get() { - self.client.event(ConfigOutput { - self_id: self.id, - linear_id: output.id.raw(), - }); + if let Some(target) = self.target.get() { + if let Target::Output(output) = target { + self.client.event(ConfigOutput { + self_id: self.id, + linear_id: output.id.raw(), + }); + } } self.client.event(ConfigAllowAllWorkspaces { self_id: self.id, @@ -200,10 +306,21 @@ impl JayScreencast { } fn detach(&self) { - if let Some(output) = self.output.take() { - output.screencasts.remove(&(self.client.id, self.id)); - if output.screencasts.is_empty() { - output.state.damage(); + if let Some(target) = self.target.take() { + match target { + Target::Output(output) => { + output.screencasts.remove(&(self.client.id, self.id)); + if output.screencasts.is_empty() { + output.state.damage(); + } + } + Target::Toplevel(tl) => { + let data = tl.tl_data(); + data.jay_screencasts.remove(&(self.client.id, self.id)); + if data.jay_screencasts.is_empty() { + self.client.state.damage(); + } + } } } } @@ -214,17 +331,39 @@ impl JayScreencast { self.client.event(Destroyed { self_id: self.id }); } - pub fn realloc(&self, ctx: &Rc) -> Result<(), JayScreencastError> { + pub fn schedule_realloc(self: &Rc) { + self.need_realloc.set(true); + if !self.realloc_scheduled.replace(true) { + self.client + .state + .pending_toplevel_screencast_reallocs + .push(self.clone()); + } + } + + fn realloc(&self, ctx: &Rc) -> Result<(), JayScreencastError> { + if !self.destroyed.get() && self.buffers_acked.get() { + self.do_realloc(ctx) + } else { + Ok(()) + } + } + + fn do_realloc(&self, ctx: &Rc) -> Result<(), JayScreencastError> { + self.need_realloc.set(false); let mut buffers = vec![]; let formats = ctx.formats(); let format = match formats.get(&XRGB8888.drm) { Some(f) => f, _ => return Err(JayScreencastError::XRGB8888), }; - if let Some(output) = self.output.get() { - let (width, height) = output.global.pixel_size(); + if let Some(target) = self.target.get() { + let (width, height) = target_size(Some(&target)); let num = 3; for _ in 0..num { + if width == 0 || height == 0 { + continue; + } let mut usage = GBM_BO_USE_RENDERING; let modifiers = match self.linear.get() { true if format.write_modifiers.contains(&LINEAR_MODIFIER) => { @@ -267,8 +406,11 @@ impl JayScreencast { } fn damage(&self) { - if let Some(output) = self.output.get() { - output.global.connector.connector.damage(); + if let Some(target) = self.target.get() { + match target { + Target::Output(o) => o.global.connector.connector.damage(), + Target::Toplevel(_) => self.client.state.damage(), + } } } } @@ -284,14 +426,14 @@ impl JayScreencastRequestHandler for JayScreencast { fn set_output(&self, req: SetOutput, _slf: &Rc) -> Result<(), Self::Error> { let output = if req.output.is_some() { - Some(self.client.lookup(req.output)?) + Some(PendingTarget::Output(self.client.lookup(req.output)?)) } else { None }; if self.destroyed.get() || !self.config_acked.get() { return Ok(()); } - self.pending.output.set(Some(output)); + self.pending.target.set(Some(output)); Ok(()) } @@ -362,19 +504,42 @@ impl JayScreencastRequestHandler for JayScreencast { let mut need_realloc = false; - if let Some(output) = self.pending.output.take() { - let output = output.and_then(|o| o.output.get()); - if output_size(&output) != output_size(&self.output.get()) { + if let Some(target) = self.pending.target.take() { + self.detach(); + let mut new_target = None; + if let Some(new) = target { + match new { + PendingTarget::Output(o) => { + let Some(o) = o.output.get() else { + self.do_destroy(); + return Ok(()); + }; + if o.screencasts.is_empty() { + o.state.damage(); + } + o.screencasts.set((self.client.id, self.id), slf.clone()); + new_target = Some(Target::Output(o)); + } + PendingTarget::Toplevel(t) => { + if t.destroyed.get() { + self.do_destroy(); + return Ok(()); + } + let t = t.toplevel.clone(); + let data = t.tl_data(); + if data.jay_screencasts.is_empty() { + data.state.damage(); + } + data.jay_screencasts + .set((self.client.id, self.id), slf.clone()); + new_target = Some(Target::Toplevel(t)); + } + } + } + if target_size(new_target.as_ref()) != target_size(self.target.get().as_ref()) { need_realloc = true; } - self.detach(); - if let Some(new) = &output { - if new.screencasts.is_empty() { - new.state.damage(); - } - new.screencasts.set((self.client.id, self.id), slf.clone()); - } - self.output.set(output); + self.target.set(new_target); } if let Some(linear) = self.pending.linear.take() { if self.linear.replace(linear) != linear { @@ -392,29 +557,21 @@ impl JayScreencastRequestHandler for JayScreencast { } if need_realloc { - let ctx = match self.client.state.render_ctx.get() { - Some(ctx) => ctx, - _ => { - self.do_destroy(); - return Ok(()); - } - }; - if let Err(e) = self.realloc(&ctx) { - log::error!("Could not allocate buffers: {}", ErrorFmt(e)); - self.do_destroy(); - return Ok(()); - } + slf.schedule_realloc(); } Ok(()) } - fn ack_buffers(&self, req: AckBuffers, _slf: &Rc) -> Result<(), Self::Error> { + fn ack_buffers(&self, req: AckBuffers, slf: &Rc) -> Result<(), Self::Error> { if self.destroyed.get() { return Ok(()); } if req.serial == self.buffers_serial.get() { self.buffers_acked.set(true); + if self.need_realloc.get() { + slf.schedule_realloc(); + } } Ok(()) } @@ -443,6 +600,19 @@ impl JayScreencastRequestHandler for JayScreencast { } Ok(()) } + + fn set_toplevel(&self, req: SetToplevel, _slf: &Rc) -> Result<(), Self::Error> { + let toplevel = if req.id.is_some() { + Some(PendingTarget::Toplevel(self.client.lookup(req.id)?)) + } else { + None + }; + if self.destroyed.get() || !self.config_acked.get() { + return Ok(()); + } + self.pending.target.set(Some(toplevel)); + Ok(()) + } } object_base! { @@ -477,9 +647,19 @@ pub enum JayScreencastError { } efrom!(JayScreencastError, ClientError); -fn output_size(output: &Option>) -> (i32, i32) { - match output { - Some(o) => o.global.pixel_size(), - _ => (0, 0), +fn target_size(target: Option<&Target>) -> (i32, i32) { + if let Some(target) = target { + match target { + Target::Output(o) => return o.global.pixel_size(), + Target::Toplevel(t) => { + let data = t.tl_data(); + let (dw, dh) = data.desired_extents.get().size(); + if let Some(ws) = data.workspace.get() { + let scale = ws.output.get().global.persistent.scale.get(); + return scale.pixel_size(dw, dh); + }; + } + } } + (0, 0) } diff --git a/src/ifs/jay_select_toplevel.rs b/src/ifs/jay_select_toplevel.rs new file mode 100644 index 00000000..417805e6 --- /dev/null +++ b/src/ifs/jay_select_toplevel.rs @@ -0,0 +1,100 @@ +use { + crate::{ + client::{Client, ClientError}, + ifs::{jay_toplevel::JayToplevel, wl_seat::ToplevelSelector}, + leaks::Tracker, + object::{Object, Version}, + tree::ToplevelNode, + utils::clonecell::CloneCell, + wire::{jay_select_toplevel::*, JaySelectToplevelId, JayToplevelId}, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +pub struct JaySelectToplevel { + pub id: JaySelectToplevelId, + pub client: Rc, + pub tracker: Tracker, + pub destroyed: Cell, +} + +pub struct JayToplevelSelector { + pub tl: CloneCell>>, + pub jst: Rc, +} + +impl ToplevelSelector for JayToplevelSelector { + fn set(&self, toplevel: Rc) { + self.tl.set(Some(toplevel)); + } +} + +impl Drop for JayToplevelSelector { + fn drop(&mut self) { + if self.jst.destroyed.get() { + return; + } + let id = match self.tl.take() { + None => JayToplevelId::NONE, + Some(toplevel) => { + let id = match self.jst.client.new_id() { + Ok(id) => id, + Err(e) => { + self.jst.client.error(e); + return; + } + }; + let jtl = Rc::new(JayToplevel { + id, + client: self.jst.client.clone(), + tracker: Default::default(), + toplevel, + destroyed: Cell::new(false), + }); + track!(self.jst.client, jtl); + self.jst.client.add_server_obj(&jtl); + jtl.toplevel + .tl_data() + .jay_toplevels + .set((jtl.client.id, jtl.id), jtl.clone()); + jtl.id + } + }; + self.jst.send_done(id); + let _ = self.jst.client.remove_obj(&*self.jst); + } +} + +impl JaySelectToplevel { + fn send_done(&self, id: JayToplevelId) { + self.client.event(Done { + self_id: self.id, + id, + }); + } +} + +impl JaySelectToplevelRequestHandler for JaySelectToplevel { + type Error = JaySelectToplevelError; +} + +object_base! { + self = JaySelectToplevel; + version = Version(1); +} + +impl Object for JaySelectToplevel { + fn break_loops(&self) { + self.destroyed.set(true); + } +} + +simple_add_obj!(JaySelectToplevel); + +#[derive(Debug, Error)] +pub enum JaySelectToplevelError { + #[error(transparent)] + ClientError(Box), +} +efrom!(JaySelectToplevelError, ClientError); diff --git a/src/ifs/jay_toplevel.rs b/src/ifs/jay_toplevel.rs new file mode 100644 index 00000000..15ac9729 --- /dev/null +++ b/src/ifs/jay_toplevel.rs @@ -0,0 +1,68 @@ +use { + crate::{ + client::{Client, ClientError}, + leaks::Tracker, + object::{Object, Version}, + tree::ToplevelNode, + wire::{jay_toplevel::*, JayToplevelId}, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +pub struct JayToplevel { + pub id: JayToplevelId, + pub client: Rc, + pub tracker: Tracker, + pub toplevel: Rc, + pub destroyed: Cell, +} + +impl JayToplevel { + fn detach(&self) { + self.destroyed.set(true); + self.toplevel + .tl_data() + .jay_toplevels + .remove(&(self.client.id, self.id)); + } + + pub fn destroy(&self) { + self.destroyed.set(true); + self.send_destroyed(); + } + + fn send_destroyed(&self) { + self.client.event(Destroyed { self_id: self.id }); + } +} + +impl JayToplevelRequestHandler for JayToplevel { + type Error = JayToplevelError; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.detach(); + self.client.remove_obj(self)?; + Ok(()) + } +} + +object_base! { + self = JayToplevel; + version = Version(1); +} + +impl Object for JayToplevel { + fn break_loops(&self) { + self.detach(); + } +} + +dedicated_add_obj!(JayToplevel, JayToplevelId, jay_toplevels); + +#[derive(Debug, Error)] +pub enum JayToplevelError { + #[error(transparent)] + ClientError(Box), +} +efrom!(JayToplevelError, ClientError); diff --git a/src/ifs/wl_surface.rs b/src/ifs/wl_surface.rs index 3d2233b0..778c7ea3 100644 --- a/src/ifs/wl_surface.rs +++ b/src/ifs/wl_surface.rs @@ -38,8 +38,8 @@ use { wp_linux_drm_syncobj_surface_v1::WpLinuxDrmSyncobjSurfaceV1, wp_tearing_control_v1::WpTearingControlV1, wp_viewport::WpViewport, - x_surface::XSurface, - xdg_surface::{PendingXdgSurfaceData, XdgSurfaceError}, + x_surface::{xwindow::Xwindow, XSurface}, + xdg_surface::{xdg_toplevel::XdgToplevel, PendingXdgSurfaceData, XdgSurfaceError}, zwlr_layer_surface_v1::{PendingLayerSurfaceData, ZwlrLayerSurfaceV1Error}, }, wp_content_type_v1::ContentType, @@ -51,8 +51,8 @@ use { rect::{Rect, Region}, renderer::Renderer, tree::{ - FindTreeResult, FoundNode, Node, NodeId, NodeVisitor, NodeVisitorBase, OutputNode, - ToplevelNode, + ContainerNode, FindTreeResult, FoundNode, Node, NodeId, NodeVisitor, NodeVisitorBase, + OutputNode, PlaceholderNode, ToplevelNode, }, utils::{ cell_ext::CellExt, clonecell::CloneCell, copyhashmap::CopyHashMap, errorfmt::ErrorFmt, @@ -126,11 +126,40 @@ impl SurfaceRole { } pub struct SurfaceSendPreferredScaleVisitor; + +impl SurfaceSendPreferredScaleVisitor { + fn schedule_realloc(&self, tl: &impl ToplevelNode) { + for sc in tl.tl_data().jay_screencasts.lock().values() { + sc.schedule_realloc(); + } + } +} + impl NodeVisitorBase for SurfaceSendPreferredScaleVisitor { fn visit_surface(&mut self, node: &Rc) { node.on_scale_change(); node.node_visit_children(self); } + + fn visit_toplevel(&mut self, node: &Rc) { + self.schedule_realloc(&**node); + node.node_visit_children(self); + } + + fn visit_xwindow(&mut self, node: &Rc) { + self.schedule_realloc(&**node); + node.node_visit_children(self); + } + + fn visit_container(&mut self, node: &Rc) { + self.schedule_realloc(&**node); + node.node_visit_children(self); + } + + fn visit_placeholder(&mut self, node: &Rc) { + self.schedule_realloc(&**node); + node.node_visit_children(self); + } } pub struct SurfaceSendPreferredTransformVisitor; diff --git a/src/portal/ptl_display.rs b/src/portal/ptl_display.rs index 0b30712d..8b8fd14c 100644 --- a/src/portal/ptl_display.rs +++ b/src/portal/ptl_display.rs @@ -297,6 +297,7 @@ fn finish_display_connect(dpy: Rc) { id: dpy.con.id(), con: dpy.con.clone(), owner: Default::default(), + window_capture: Cell::new(false), }); dpy.con.add_object(jc.clone()); dpy.registry.request_bind(name, version, jc.deref()); diff --git a/src/portal/ptl_screencast.rs b/src/portal/ptl_screencast.rs index 20202a2d..cfc730f3 100644 --- a/src/portal/ptl_screencast.rs +++ b/src/portal/ptl_screencast.rs @@ -34,7 +34,11 @@ use { session::{CloseReply as SessionCloseReply, Closed}, }, }, - wl_usr::usr_ifs::usr_jay_screencast::{UsrJayScreencast, UsrJayScreencastOwner}, + wl_usr::usr_ifs::{ + usr_jay_screencast::{UsrJayScreencast, UsrJayScreencastOwner}, + usr_jay_select_toplevel::UsrJaySelectToplevel, + usr_jay_toplevel::UsrJayToplevel, + }, }, std::{ borrow::Cow, @@ -59,6 +63,7 @@ pub enum ScreencastPhase { Init, SourcesSelected, Selecting(Rc), + SelectingWindow(Rc), Starting(Rc), Started(Rc), Terminated, @@ -66,12 +71,22 @@ pub enum ScreencastPhase { unsafe impl UnsafeCellCloneSafe for ScreencastPhase {} -pub struct SelectingScreencast { +#[derive(Clone)] +pub struct SelectingScreencastCore { pub session: Rc, pub request_obj: Rc, pub reply: Rc>>, +} + +pub struct SelectingScreencast { + pub core: SelectingScreencastCore, pub guis: CopyHashMap>, - pub output_selected: Cell, +} + +pub struct SelectingWindowScreencast { + pub core: SelectingScreencastCore, + pub dpy: Rc, + pub selector: Rc, } pub struct StartingScreencast { @@ -80,7 +95,12 @@ pub struct StartingScreencast { pub reply: Rc>>, pub node: Rc, pub dpy: Rc, - pub output: Rc, + pub target: ScreencastTarget, +} + +pub enum ScreencastTarget { + Output(Rc), + Toplevel(Rc), } pub struct StartedScreencast { @@ -135,10 +155,16 @@ impl PwClientNodeOwner for StartingScreencast { 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); + match &self.target { + ScreencastTarget::Output(o) => jsc.set_output(&o.jay), + ScreencastTarget::Toplevel(t) => jsc.set_toplevel(t), + } jsc.set_use_linear_buffers(true); jsc.set_allow_all_workspaces(true); jsc.configure(); + if let ScreencastTarget::Toplevel(t) = &self.target { + self.dpy.con.remove_obj(&**t); + } let started = Rc::new(StartedScreencast { session: self.session.clone(), node: self.node.clone(), @@ -183,6 +209,32 @@ impl PwClientNodeOwner for StartedScreencast { } } +impl SelectingScreencastCore { + pub fn starting(&self, dpy: &Rc, target: ScreencastTarget) { + let node = 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.session.clone(), + request_obj: self.request_obj.clone(), + reply: self.reply.clone(), + node, + dpy: dpy.clone(), + target, + }); + self.session + .phase + .set(ScreencastPhase::Starting(starting.clone())); + starting.node.owner.set(Some(starting.clone())); + dpy.screencasts.set( + self.session.session_obj.path().to_owned(), + self.session.clone(), + ); + } +} + impl ScreencastSession { pub(super) fn kill(&self) { self.session_obj.emit_signal(&Closed); @@ -192,15 +244,22 @@ impl ScreencastSession { ScreencastPhase::SourcesSelected => {} ScreencastPhase::Terminated => {} ScreencastPhase::Selecting(s) => { - s.reply.err("Session has been terminated"); + s.core.reply.err("Session has been terminated"); for (_, gui) in s.guis.lock().drain() { gui.kill(false); } } + ScreencastPhase::SelectingWindow(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()); + if let ScreencastTarget::Toplevel(t) = &s.target { + s.dpy.con.remove_obj(&**t); + } } ScreencastPhase::Started(s) => { s.jay_screencast.con.remove_obj(s.jay_screencast.deref()); @@ -270,11 +329,12 @@ impl ScreencastSession { } self.phase .set(ScreencastPhase::Selecting(Rc::new(SelectingScreencast { - session: self.clone(), - request_obj: Rc::new(request_obj), - reply: Rc::new(reply), + core: SelectingScreencastCore { + session: self.clone(), + request_obj: Rc::new(request_obj), + reply: Rc::new(reply), + }, guis, - output_selected: Cell::new(false), }))); } } diff --git a/src/portal/ptl_screencast/screencast_gui.rs b/src/portal/ptl_screencast/screencast_gui.rs index 57e8ee0c..2a91d473 100644 --- a/src/portal/ptl_screencast/screencast_gui.rs +++ b/src/portal/ptl_screencast/screencast_gui.rs @@ -2,8 +2,10 @@ use { crate::{ ifs::wl_seat::{wl_pointer::PRESSED, BTN_LEFT}, portal::{ - ptl_display::{PortalDisplay, PortalOutput}, - ptl_screencast::{ScreencastPhase, ScreencastSession, StartingScreencast}, + ptl_display::{PortalDisplay, PortalOutput, PortalSeat}, + ptl_screencast::{ + ScreencastPhase, ScreencastSession, ScreencastTarget, SelectingWindowScreencast, + }, ptr_gui::{ Align, Button, ButtonOwner, Flow, GuiElement, Label, Orientation, OverlayWindow, OverlayWindowOwner, @@ -11,6 +13,9 @@ use { }, theme::Color, utils::copyhashmap::CopyHashMap, + wl_usr::usr_ifs::{ + usr_jay_select_toplevel::UsrJaySelectToplevelOwner, usr_jay_toplevel::UsrJayToplevel, + }, }, std::rc::Rc, }; @@ -38,6 +43,7 @@ struct StaticButton { #[derive(Copy, Clone, Eq, PartialEq)] enum ButtonRole { Accept, + Window, Reject, } @@ -65,17 +71,17 @@ fn create_accept_gui(surface: &Rc) -> Rc { let label = Rc::new(Label::default()); *label.text.borrow_mut() = text; let accept_button = static_button(surface, ButtonRole::Accept, "Share This Output"); + let window_button = static_button(surface, ButtonRole::Window, "Share A Window"); let reject_button = static_button(surface, ButtonRole::Reject, "Reject"); - let buttons = [&accept_button, &reject_button]; - for button in buttons { + for button in [&accept_button, &window_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)); + for button in [&accept_button, &window_button] { + button.bg_color.set(Color::from_rgb(170, 200, 170)); + 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 @@ -85,7 +91,12 @@ fn create_accept_gui(surface: &Rc) -> Rc { 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]; + let mut elements: Vec> = vec![label, accept_button]; + if surface.gui.dpy.jc.window_capture.get() { + elements.push(window_button); + } + elements.push(reject_button); + *flow.elements.borrow_mut() = elements; flow } @@ -124,12 +135,12 @@ impl SelectionGui { } impl ButtonOwner for StaticButton { - fn button(&self, button: u32, state: u32) { + fn button(&self, seat: &PortalSeat, button: u32, state: u32) { if button != BTN_LEFT || state != PRESSED { return; } match self.role { - ButtonRole::Accept => { + ButtonRole::Accept | ButtonRole::Window => { log::info!("User has accepted the request"); let selecting = match self.surface.gui.screencast_session.phase.get() { ScreencastPhase::Selecting(selecting) => selecting, @@ -138,34 +149,25 @@ impl ButtonOwner for StaticButton { 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( + let dpy = &self.surface.output.dpy; + if self.role == ButtonRole::Accept { + selecting + .core + .starting(dpy, ScreencastTarget::Output(self.surface.output.clone())); + } else { + let selector = dpy.jc.select_toplevel(&seat.wl); + let selecting = Rc::new(SelectingWindowScreencast { + core: selecting.core.clone(), + dpy: dpy.clone(), + selector: selector.clone(), + }); + selector.owner.set(Some(selecting.clone())); self.surface .gui .screencast_session - .session_obj - .path() - .to_owned(), - self.surface.gui.screencast_session.clone(), - ); + .phase + .set(ScreencastPhase::SelectingWindow(selecting)); + } } ButtonRole::Reject => { log::info!("User has rejected the screencast request"); @@ -175,6 +177,28 @@ impl ButtonOwner for StaticButton { } } +impl UsrJaySelectToplevelOwner for SelectingWindowScreencast { + fn done(&self, tl: Option>) { + let Some(tl) = tl else { + log::info!("User has aborted the selection"); + self.core.session.kill(); + return; + }; + match self.core.session.phase.get() { + ScreencastPhase::SelectingWindow(s) => { + self.dpy.con.remove_obj(&*s.selector); + } + _ => { + self.dpy.con.remove_obj(&*tl); + return; + } + } + log::info!("User has selected a window"); + self.core + .starting(&self.dpy, ScreencastTarget::Toplevel(tl)); + } +} + fn static_button(surface: &Rc, role: ButtonRole, text: &str) -> Rc