From c6864a6d85ad12a027416a27338165b674d62bf6 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 20 Apr 2024 13:30:34 +0200 Subject: [PATCH 1/2] tree: fix per-workspace capture tracking --- src/compositor.rs | 4 +- src/config/handler.rs | 5 +- src/ifs/jay_screencast.rs | 33 ++++++-- src/ifs/wl_output.rs | 99 +--------------------- src/ifs/zwlr_screencopy_frame_v1.rs | 37 +++++---- src/ifs/zwlr_screencopy_manager_v1.rs | 4 - src/renderer.rs | 9 +- src/tasks/connector.rs | 4 + src/tree/output.rs | 115 ++++++++++++++++++++++++-- src/tree/workspace.rs | 26 +++++- 10 files changed, 191 insertions(+), 145 deletions(-) diff --git a/src/compositor.rs b/src/compositor.rs index 64a05570..38ae0372 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -451,6 +451,7 @@ fn create_dummy_output(state: &Rc) { update_render_data_scheduled: Cell::new(false), screencasts: Default::default(), hardware_cursor_needs_render: Cell::new(false), + screencopies: Default::default(), }); let dummy_workspace = Rc::new(WorkspaceNode { id: state.node_ids.next(), @@ -467,7 +468,8 @@ fn create_dummy_output(state: &Rc) { visible_on_desired_output: Default::default(), desired_output: CloneCell::new(dummy_output.global.output_id.clone()), jay_workspaces: Default::default(), - capture: Cell::new(false), + may_capture: Cell::new(false), + has_capture: Cell::new(false), title_texture: Cell::new(None), attention_requests: Default::default(), }); diff --git a/src/config/handler.rs b/src/config/handler.rs index b1f2f263..a5b93022 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -655,7 +655,7 @@ impl ConfigProxyHandler { fn handle_get_workspace_capture(&self, workspace: Workspace) -> Result<(), CphError> { let name = self.get_workspace(workspace)?; let capture = match self.state.workspaces.get(name.as_str()) { - Some(ws) => ws.capture.get(), + Some(ws) => ws.may_capture.get(), None => self.state.default_workspace_capture.get(), }; self.respond(Response::GetWorkspaceCapture { capture }); @@ -669,7 +669,8 @@ impl ConfigProxyHandler { ) -> Result<(), CphError> { let name = self.get_workspace(workspace)?; if let Some(ws) = self.state.workspaces.get(name.as_str()) { - ws.capture.set(capture); + ws.may_capture.set(capture); + ws.update_has_captures(); ws.output.get().schedule_update_render_data(); self.state.damage(); } diff --git a/src/ifs/jay_screencast.rs b/src/ifs/jay_screencast.rs index bcee7256..cd4215ad 100644 --- a/src/ifs/jay_screencast.rs +++ b/src/ifs/jay_screencast.rs @@ -8,7 +8,7 @@ use { object::{Object, Version}, scale::Scale, state::State, - tree::{OutputNode, ToplevelNode, WorkspaceNodeId}, + tree::{OutputNode, ToplevelNode, WorkspaceNode, WorkspaceNodeId}, utils::{ clonecell::{CloneCell, UnsafeCellCloneSafe}, errorfmt::ErrorFmt, @@ -106,6 +106,18 @@ struct ScreencastBuffer { } impl JayScreencast { + pub fn shows_ws(&self, ws: &WorkspaceNode) -> bool { + if self.show_all.get() { + return true; + } + for &id in &*self.show_workspaces.borrow() { + if id == ws.id { + return true; + } + } + false + } + pub fn new(id: JayScreencastId, client: &Rc) -> Self { Self { id, @@ -309,10 +321,7 @@ impl JayScreencast { 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(); - } + output.remove_screencast(self); } Target::Toplevel(tl) => { let data = tl.tl_data(); @@ -514,10 +523,7 @@ impl JayScreencastRequestHandler for JayScreencast { self.do_destroy(); return Ok(()); }; - if o.screencasts.is_empty() { - o.state.damage(); - } - o.screencasts.set((self.client.id, self.id), slf.clone()); + o.add_screencast(slf); new_target = Some(Target::Output(o)); } PendingTarget::Toplevel(t) => { @@ -546,11 +552,14 @@ impl JayScreencastRequestHandler for JayScreencast { need_realloc = true; } } + let mut capture_rules_changed = false; if let Some(show_all) = self.pending.show_all.take() { self.show_all.set(show_all); + capture_rules_changed = true; } if let Some(new_workspaces) = self.pending.show_workspaces.borrow_mut().take() { *self.show_workspaces.borrow_mut() = new_workspaces; + capture_rules_changed = true; } if let Some(running) = self.pending.running.take() { self.running.set(running); @@ -560,6 +569,12 @@ impl JayScreencastRequestHandler for JayScreencast { slf.schedule_realloc(); } + if capture_rules_changed { + if let Some(Target::Output(o)) = self.target.get() { + o.screencast_changed(); + } + } + Ok(()) } diff --git a/src/ifs/wl_output.rs b/src/ifs/wl_output.rs index 39255533..75cd943c 100644 --- a/src/ifs/wl_output.rs +++ b/src/ifs/wl_output.rs @@ -2,22 +2,14 @@ use { crate::{ backend, client::{Client, ClientError, ClientId}, - gfx_api::GfxTexture, globals::{Global, GlobalName}, - ifs::{ - wl_buffer::WlBufferStorage, wl_surface::WlSurface, - zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1, zxdg_output_v1::ZxdgOutputV1, - }, + ifs::{wl_surface::WlSurface, zxdg_output_v1::ZxdgOutputV1}, leaks::Tracker, object::{Object, Version}, rect::Rect, state::{ConnectorData, State}, - time::Time, tree::{calculate_logical_size, OutputNode}, - utils::{ - clonecell::CloneCell, copyhashmap::CopyHashMap, errorfmt::ErrorFmt, - linkedlist::LinkedList, transform_ext::TransformExt, - }, + utils::{clonecell::CloneCell, copyhashmap::CopyHashMap, transform_ext::TransformExt}, wire::{wl_output::*, WlOutputId, ZxdgOutputV1Id}, }, ahash::AHashMap, @@ -25,7 +17,6 @@ use { std::{ cell::{Cell, RefCell}, collections::hash_map::Entry, - ops::Deref, rc::Rc, }, thiserror::Error, @@ -68,8 +59,6 @@ pub struct WlOutputGlobal { pub width_mm: i32, pub height_mm: i32, pub bindings: RefCell>>>, - pub unused_captures: LinkedList>, - pub pending_captures: LinkedList>, pub destroyed: Cell, pub legacy_scale: Cell, pub persistent: Rc, @@ -125,8 +114,6 @@ impl WlOutputGlobal { width_mm, height_mm, bindings: Default::default(), - unused_captures: Default::default(), - pending_captures: Default::default(), destroyed: Cell::new(false), legacy_scale: Cell::new(scale.round_up()), persistent: persistent_state.clone(), @@ -210,88 +197,6 @@ impl WlOutputGlobal { Ok(()) } - pub fn perform_screencopies( - &self, - tex: &Rc, - render_hardware_cursors: bool, - x_off: i32, - y_off: i32, - size: Option<(i32, i32)>, - ) { - if self.pending_captures.is_empty() { - return; - } - let now = Time::now().unwrap(); - let mut captures = vec![]; - for capture in self.pending_captures.iter() { - captures.push(capture.deref().clone()); - let wl_buffer = match capture.buffer.take() { - Some(b) => b, - _ => { - log::warn!("Capture frame is pending but has no buffer attached"); - capture.send_failed(); - continue; - } - }; - if wl_buffer.destroyed() { - capture.send_failed(); - continue; - } - if let Some(WlBufferStorage::Shm { mem, stride }) = - wl_buffer.storage.borrow_mut().deref() - { - let res = self.state.perform_shm_screencopy( - tex, - self.pos.get(), - x_off, - y_off, - size, - &capture, - mem, - *stride, - wl_buffer.format, - Transform::None, - ); - if let Err(e) = res { - log::warn!("Could not perform shm screencopy: {}", ErrorFmt(e)); - capture.send_failed(); - continue; - } - } else { - let fb = match wl_buffer.famebuffer.get() { - Some(fb) => fb, - _ => { - log::warn!("Capture buffer has no framebuffer"); - capture.send_failed(); - continue; - } - }; - let res = self.state.perform_screencopy( - tex, - &fb, - self.pos.get(), - render_hardware_cursors, - x_off - capture.rect.x1(), - y_off - capture.rect.y1(), - size, - Transform::None, - ); - if let Err(e) = res { - log::warn!("Could not perform screencopy: {}", ErrorFmt(e)); - capture.send_failed(); - continue; - } - } - if capture.with_damage.get() { - capture.send_damage(); - } - capture.send_ready(now.0.tv_sec as _, now.0.tv_nsec as _); - } - for capture in captures { - capture.output_link.take(); - } - } - pub fn pixel_size(&self) -> (i32, i32) { let mode = self.mode.get(); self.persistent diff --git a/src/ifs/zwlr_screencopy_frame_v1.rs b/src/ifs/zwlr_screencopy_frame_v1.rs index dbd9782a..f8f121d5 100644 --- a/src/ifs/zwlr_screencopy_frame_v1.rs +++ b/src/ifs/zwlr_screencopy_frame_v1.rs @@ -9,7 +9,6 @@ use { leaks::Tracker, object::{Object, Version}, rect::Rect, - utils::linkedlist::LinkedNode, wire::{zwlr_screencopy_frame_v1::*, WlBufferId, ZwlrScreencopyFrameV1Id}, }, std::{cell::Cell, ops::Deref, rc::Rc}, @@ -28,7 +27,6 @@ pub struct ZwlrScreencopyFrameV1 { pub overlay_cursor: bool, pub used: Cell, pub with_damage: Cell, - pub output_link: Cell>>>, pub buffer: Cell>>, pub version: Version, } @@ -90,19 +88,16 @@ impl ZwlrScreencopyFrameV1 { } fn do_copy( - &self, + self: &Rc, buffer_id: WlBufferId, with_damage: bool, ) -> Result<(), ZwlrScreencopyFrameV1Error> { if self.used.replace(true) { return Err(ZwlrScreencopyFrameV1Error::AlreadyUsed); } - let link = match self.output_link.take() { - Some(l) => l, - _ => { - self.send_failed(); - return Ok(()); - } + let Some(node) = self.output.node.get() else { + self.send_failed(); + return Ok(()); }; let buffer = self.client.lookup(buffer_id)?; if (buffer.rect.width(), buffer.rect.height()) != (self.rect.width(), self.rect.height()) { @@ -122,27 +117,35 @@ impl ZwlrScreencopyFrameV1 { self.output.connector.connector.damage(); } self.with_damage.set(with_damage); - self.output.pending_captures.add_last_existing(&link); - self.output_link.set(Some(link)); + node.screencopies + .set((self.client.id, self.id), self.clone()); + node.screencast_changed(); Ok(()) } + + fn detach(&self) { + if let Some(node) = self.output.node.get() { + node.screencopies.remove(&(self.client.id, self.id)); + node.screencast_changed(); + } + } } impl ZwlrScreencopyFrameV1RequestHandler for ZwlrScreencopyFrameV1 { type Error = ZwlrScreencopyFrameV1Error; - fn copy(&self, req: Copy, _slf: &Rc) -> Result<(), Self::Error> { - self.do_copy(req.buffer, false) + fn copy(&self, req: Copy, slf: &Rc) -> Result<(), Self::Error> { + slf.do_copy(req.buffer, false) } fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.detach(); self.client.remove_obj(self)?; - self.output_link.take(); Ok(()) } - fn copy_with_damage(&self, req: CopyWithDamage, _slf: &Rc) -> Result<(), Self::Error> { - self.do_copy(req.buffer, true) + fn copy_with_damage(&self, req: CopyWithDamage, slf: &Rc) -> Result<(), Self::Error> { + slf.do_copy(req.buffer, true) } } @@ -155,7 +158,7 @@ simple_add_obj!(ZwlrScreencopyFrameV1); impl Object for ZwlrScreencopyFrameV1 { fn break_loops(&self) { - self.output_link.take(); + self.detach(); } } diff --git a/src/ifs/zwlr_screencopy_manager_v1.rs b/src/ifs/zwlr_screencopy_manager_v1.rs index 7acbf3d2..866c3691 100644 --- a/src/ifs/zwlr_screencopy_manager_v1.rs +++ b/src/ifs/zwlr_screencopy_manager_v1.rs @@ -125,7 +125,6 @@ impl ZwlrScreencopyManagerV1 { overlay_cursor, used: Cell::new(false), with_damage: Cell::new(false), - output_link: Cell::new(None), buffer: Cell::new(None), version: self.version, }); @@ -136,9 +135,6 @@ impl ZwlrScreencopyManagerV1 { frame.send_linux_dmabuf(); frame.send_buffer_done(); } - frame - .output_link - .set(Some(output.global.unused_captures.add_last(frame.clone()))); Ok(()) } } diff --git a/src/renderer.rs b/src/renderer.rs index 964e9c58..a4c97968 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -132,11 +132,9 @@ impl Renderer<'_> { x, y, ); - let has_captures = - !output.screencasts.is_empty() || !output.global.pending_captures.is_empty(); let rd = output.render_data.borrow_mut(); if let Some(aw) = &rd.active_workspace { - let c = match has_captures && aw.captured { + let c = match aw.captured { true => theme.colors.captured_focused_title_background.get(), false => theme.colors.focused_title_background.get(), }; @@ -147,10 +145,7 @@ impl Renderer<'_> { .fill_boxes2(slice::from_ref(&rd.underline), &c, x, y); let c = theme.colors.unfocused_title_background.get(); self.base.fill_boxes2(&rd.inactive_workspaces, &c, x, y); - let c = match has_captures { - true => theme.colors.captured_unfocused_title_background.get(), - false => theme.colors.unfocused_title_background.get(), - }; + let c = theme.colors.captured_unfocused_title_background.get(); self.base .fill_boxes2(&rd.captured_inactive_workspaces, &c, x, y); let c = theme.colors.attention_requested_background.get(); diff --git a/src/tasks/connector.rs b/src/tasks/connector.rs index 07b7d93c..9cf069c2 100644 --- a/src/tasks/connector.rs +++ b/src/tasks/connector.rs @@ -148,6 +148,7 @@ impl ConnectorHandler { screencasts: Default::default(), update_render_data_scheduled: Cell::new(false), hardware_cursor_needs_render: Cell::new(false), + screencopies: Default::default(), }); self.state .add_output_scale(on.global.persistent.scale.get()); @@ -235,6 +236,9 @@ impl ConnectorHandler { for sc in screencasts { sc.do_destroy(); } + for (_, sc) in on.screencopies.lock().drain() { + sc.send_failed(); + } global.destroyed.set(true); self.state.root.outputs.remove(&self.id); self.state.root.update_extents(); diff --git a/src/tree/output.rs b/src/tree/output.rs index a188010e..00cf8459 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -8,6 +8,7 @@ use { ifs::{ jay_output::JayOutput, jay_screencast::JayScreencast, + wl_buffer::WlBufferStorage, wl_output::WlOutputGlobal, wl_seat::{ collect_kb_foci2, wl_pointer::PendingScroll, NodeSeatState, SeatId, WlSeatGlobal, @@ -19,12 +20,14 @@ use { SurfaceSendPreferredTransformVisitor, }, zwlr_layer_shell_v1::{BACKGROUND, BOTTOM, OVERLAY, TOP}, + zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1, }, rect::Rect, renderer::Renderer, scale::Scale, state::State, text::{self, TextTexture}, + time::Time, tree::{ walker::NodeVisitor, Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, WorkspaceNode, @@ -33,7 +36,7 @@ use { clonecell::CloneCell, copyhashmap::CopyHashMap, errorfmt::ErrorFmt, linkedlist::LinkedList, scroller::Scroller, transform_ext::TransformExt, }, - wire::{JayOutputId, JayScreencastId}, + wire::{JayOutputId, JayScreencastId, ZwlrScreencopyFrameV1Id}, }, ahash::AHashMap, jay_config::video::Transform, @@ -66,6 +69,7 @@ pub struct OutputNode { pub hardware_cursor_needs_render: Cell, pub update_render_data_scheduled: Cell, pub screencasts: CopyHashMap<(ClientId, JayScreencastId), Rc>, + pub screencopies: CopyHashMap<(ClientId, ZwlrScreencopyFrameV1Id), Rc>, } pub async fn output_render_data(state: Rc) { @@ -81,6 +85,22 @@ pub async fn output_render_data(state: Rc) { } impl OutputNode { + pub fn add_screencast(&self, sc: &Rc) { + self.screencasts.set((sc.client.id, sc.id), sc.clone()); + self.screencast_changed(); + } + + pub fn remove_screencast(&self, sc: &JayScreencast) { + self.screencasts.remove(&(sc.client.id, sc.id)); + self.screencast_changed(); + } + + pub fn screencast_changed(&self) { + for ws in self.workspaces.iter() { + ws.update_has_captures(); + } + } + pub fn perform_screencopies( &self, tex: &Rc, @@ -90,17 +110,94 @@ impl OutputNode { size: Option<(i32, i32)>, ) { if let Some(workspace) = self.workspace.get() { - if !workspace.capture.get() { + if !workspace.may_capture.get() { return; } } - self.global - .perform_screencopies(tex, render_hardware_cursor, x_off, y_off, size); + self.perform_wlr_screencopies(tex, render_hardware_cursor, x_off, y_off, size); for sc in self.screencasts.lock().values() { sc.copy_texture(self, tex, render_hardware_cursor, x_off, y_off, size); } } + pub fn perform_wlr_screencopies( + &self, + tex: &Rc, + render_hardware_cursors: bool, + x_off: i32, + y_off: i32, + size: Option<(i32, i32)>, + ) { + if self.screencopies.is_empty() { + return; + } + let now = Time::now().unwrap(); + for (_, capture) in self.screencopies.lock().drain() { + let wl_buffer = match capture.buffer.take() { + Some(b) => b, + _ => { + log::warn!("Capture frame is pending but has no buffer attached"); + capture.send_failed(); + continue; + } + }; + if wl_buffer.destroyed() { + capture.send_failed(); + continue; + } + if let Some(WlBufferStorage::Shm { mem, stride }) = + wl_buffer.storage.borrow_mut().deref() + { + let res = self.state.perform_shm_screencopy( + tex, + self.global.pos.get(), + x_off, + y_off, + size, + &capture, + mem, + *stride, + wl_buffer.format, + Transform::None, + ); + if let Err(e) = res { + log::warn!("Could not perform shm screencopy: {}", ErrorFmt(e)); + capture.send_failed(); + continue; + } + } else { + let fb = match wl_buffer.famebuffer.get() { + Some(fb) => fb, + _ => { + log::warn!("Capture buffer has no framebuffer"); + capture.send_failed(); + continue; + } + }; + let res = self.state.perform_screencopy( + tex, + &fb, + self.global.pos.get(), + render_hardware_cursors, + x_off - capture.rect.x1(), + y_off - capture.rect.y1(), + size, + Transform::None, + ); + if let Err(e) = res { + log::warn!("Could not perform screencopy: {}", ErrorFmt(e)); + capture.send_failed(); + continue; + } + } + if capture.with_damage.get() { + capture.send_damage(); + } + capture.send_ready(now.0.tv_sec as _, now.0.tv_nsec as _); + } + self.screencast_changed(); + } + pub fn clear(&self) { self.global.clear(); self.workspace.set(None); @@ -111,6 +208,8 @@ impl OutputNode { self.render_data.borrow_mut().titles.clear(); self.lock_surface.take(); self.jay_outputs.clear(); + self.screencasts.clear(); + self.screencopies.clear(); } pub fn on_spaces_changed(self: &Rc) { @@ -228,13 +327,13 @@ impl OutputNode { if Some(ws.id) == active_id { rd.active_workspace = Some(OutputWorkspaceRenderData { rect, - captured: ws.capture.get(), + captured: ws.has_capture.get(), }); } else { if ws.attention_requests.active() { rd.attention_requested_workspaces.push(rect); } - if ws.capture.get() { + if ws.has_capture.get() { rd.captured_inactive_workspaces.push(rect); } else { rd.inactive_workspaces.push(rect); @@ -345,10 +444,12 @@ impl OutputNode { visible_on_desired_output: Cell::new(false), desired_output: CloneCell::new(self.global.output_id.clone()), jay_workspaces: Default::default(), - capture: self.state.default_workspace_capture.clone(), + may_capture: self.state.default_workspace_capture.clone(), + has_capture: Cell::new(false), title_texture: Default::default(), attention_requests: Default::default(), }); + ws.update_has_captures(); *ws.output_link.borrow_mut() = Some(self.workspaces.add_last(ws.clone())); self.state.workspaces.set(name.to_string(), ws.clone()); if self.workspace.is_none() { diff --git a/src/tree/workspace.rs b/src/tree/workspace.rs index f210c095..c7b2286f 100644 --- a/src/tree/workspace.rs +++ b/src/tree/workspace.rs @@ -49,7 +49,8 @@ pub struct WorkspaceNode { pub visible_on_desired_output: Cell, pub desired_output: CloneCell>, pub jay_workspaces: CopyHashMap<(ClientId, JayWorkspaceId), Rc>, - pub capture: Cell, + pub may_capture: Cell, + pub has_capture: Cell, pub title_texture: Cell>, pub attention_requests: ThresholdCounter, } @@ -62,11 +63,34 @@ impl WorkspaceNode { self.jay_workspaces.clear(); } + pub fn update_has_captures(&self) { + let mut has_capture = false; + let output = self.output.get(); + 'update: { + if !self.may_capture.get() { + break 'update; + } + for sc in output.screencasts.lock().values() { + if sc.shows_ws(self) { + has_capture = true; + break 'update; + } + } + if output.screencopies.is_not_empty() { + has_capture = true; + } + } + if self.has_capture.replace(has_capture) != has_capture { + output.schedule_update_render_data(); + } + } + pub fn set_output(&self, output: &Rc) { self.output.set(output.clone()); for jw in self.jay_workspaces.lock().values() { jw.send_output(output); } + self.update_has_captures(); struct OutputSetter<'a>(&'a Rc); impl NodeVisitorBase for OutputSetter<'_> { fn visit_surface(&mut self, node: &Rc) { From 33a0a40857ecdc5594de2a8168d32f2d9202bfd9 Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Sat, 20 Apr 2024 13:48:12 +0200 Subject: [PATCH 2/2] portal: implement workspace capture --- docs/features.md | 6 + release-notes.md | 1 + src/compositor.rs | 1 + src/ifs.rs | 1 + src/ifs/jay_compositor.rs | 22 ++- src/ifs/jay_select_workspace.rs | 105 +++++++++++++ src/ifs/jay_workspace.rs | 8 + src/ifs/jay_workspace_watcher.rs | 6 +- src/ifs/wl_seat.rs | 10 +- src/ifs/wl_seat/pointer_owner.rs | 142 +++++++++++++++--- src/portal/ptl_display.rs | 2 +- src/portal/ptl_screencast.rs | 44 +++++- src/portal/ptl_screencast/screencast_gui.rs | 71 ++++++++- src/renderer.rs | 11 +- src/tree.rs | 1 + src/tree/output.rs | 16 +- src/tree/workspace.rs | 2 + src/wl_usr/usr_ifs.rs | 1 + src/wl_usr/usr_ifs/usr_jay_compositor.rs | 25 ++- src/wl_usr/usr_ifs/usr_jay_screencast.rs | 9 +- .../usr_ifs/usr_jay_select_workspace.rs | 72 +++++++++ wire/jay_compositor.txt | 5 + wire/jay_select_workspace.txt | 7 + 23 files changed, 518 insertions(+), 50 deletions(-) create mode 100644 src/ifs/jay_select_workspace.rs create mode 100644 src/wl_usr/usr_ifs/usr_jay_select_workspace.rs create mode 100644 wire/jay_select_workspace.txt diff --git a/docs/features.md b/docs/features.md index 5c01f338..797b6832 100644 --- a/docs/features.md +++ b/docs/features.md @@ -72,6 +72,12 @@ You can change this GPU at runtime. ## Screen Sharing Jay supports screen sharing via xdg-desktop-portal. +There are three supported modes: + +- Window capture +- Output capture +- Workspace capture which is like output capture except that only one workspace will be + shown. ## Screen Locking diff --git a/release-notes.md b/release-notes.md index b30c29d0..442061e3 100644 --- a/release-notes.md +++ b/release-notes.md @@ -1,6 +1,7 @@ # Unreleased - Screencasts now support window capture. +- Screencasts now support workspace capture. - Add support for wp-alpha-modifier. - Add support for per-device keymaps. - Add support for virtual-keyboard-unstable-v1. diff --git a/src/compositor.rs b/src/compositor.rs index 38ae0372..eb361305 100644 --- a/src/compositor.rs +++ b/src/compositor.rs @@ -472,6 +472,7 @@ fn create_dummy_output(state: &Rc) { has_capture: Cell::new(false), title_texture: Cell::new(None), attention_requests: Default::default(), + render_highlight: Default::default(), }); *dummy_workspace.output_link.borrow_mut() = Some(dummy_output.workspaces.add_last(dummy_workspace.clone())); diff --git a/src/ifs.rs b/src/ifs.rs index 5625f1dc..2fab7574 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -17,6 +17,7 @@ pub mod jay_screencast; pub mod jay_screenshot; pub mod jay_seat_events; pub mod jay_select_toplevel; +pub mod jay_select_workspace; pub mod jay_toplevel; pub mod jay_workspace; pub mod jay_workspace_watcher; diff --git a/src/ifs/jay_compositor.rs b/src/ifs/jay_compositor.rs index 2aef869a..a28d0b05 100644 --- a/src/ifs/jay_compositor.rs +++ b/src/ifs/jay_compositor.rs @@ -15,6 +15,7 @@ use { jay_screenshot::JayScreenshot, jay_seat_events::JaySeatEvents, jay_select_toplevel::{JaySelectToplevel, JayToplevelSelector}, + jay_select_workspace::{JaySelectWorkspace, JayWorkspaceSelector}, jay_workspace_watcher::JayWorkspaceWatcher, }, leaks::Tracker, @@ -85,13 +86,14 @@ pub struct Cap; impl Cap { pub const NONE: u16 = 0; pub const WINDOW_CAPTURE: u16 = 1; + pub const SELECT_WORKSPACE: u16 = 2; } impl JayCompositor { fn send_capabilities(&self) { self.client.event(Capabilities { self_id: self.id, - cap: &[Cap::NONE, Cap::WINDOW_CAPTURE], + cap: &[Cap::NONE, Cap::WINDOW_CAPTURE, Cap::SELECT_WORKSPACE], }); } @@ -362,6 +364,24 @@ impl JayCompositorRequestHandler for JayCompositor { seat.global.select_toplevel(selector); Ok(()) } + + fn select_workspace(&self, req: SelectWorkspace, _slf: &Rc) -> Result<(), Self::Error> { + let seat = self.client.lookup(req.seat)?; + let obj = Rc::new(JaySelectWorkspace { + 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 = JayWorkspaceSelector { + ws: Default::default(), + jsw: obj.clone(), + }; + seat.global.select_workspace(selector); + Ok(()) + } } object_base! { diff --git a/src/ifs/jay_select_workspace.rs b/src/ifs/jay_select_workspace.rs new file mode 100644 index 00000000..475014e3 --- /dev/null +++ b/src/ifs/jay_select_workspace.rs @@ -0,0 +1,105 @@ +use { + crate::{ + client::{Client, ClientError}, + ifs::{jay_workspace::JayWorkspace, wl_seat::WorkspaceSelector}, + leaks::Tracker, + object::{Object, Version}, + tree::WorkspaceNode, + utils::clonecell::CloneCell, + wire::{jay_select_workspace::*, JaySelectWorkspaceId, JayWorkspaceId}, + }, + std::{cell::Cell, rc::Rc}, + thiserror::Error, +}; + +pub struct JaySelectWorkspace { + pub id: JaySelectWorkspaceId, + pub client: Rc, + pub tracker: Tracker, + pub destroyed: Cell, +} + +pub struct JayWorkspaceSelector { + pub ws: CloneCell>>, + pub jsw: Rc, +} + +impl WorkspaceSelector for JayWorkspaceSelector { + fn set(&self, ws: Rc) { + self.ws.set(Some(ws)); + } +} + +impl Drop for JayWorkspaceSelector { + fn drop(&mut self) { + if self.jsw.destroyed.get() { + return; + } + match self.ws.take() { + None => { + self.jsw.send_cancelled(); + } + Some(ws) => { + let id = match self.jsw.client.new_id() { + Ok(id) => id, + Err(e) => { + self.jsw.client.error(e); + return; + } + }; + let jw = Rc::new(JayWorkspace { + id, + client: self.jsw.client.clone(), + workspace: CloneCell::new(Some(ws.clone())), + tracker: Default::default(), + }); + track!(self.jsw.client, jw); + self.jsw.client.add_server_obj(&jw); + self.jsw + .send_selected(ws.output.get().global.name.raw(), id); + ws.jay_workspaces + .set((self.jsw.client.id, jw.id), jw.clone()); + jw.send_initial_properties(&ws); + } + }; + let _ = self.jsw.client.remove_obj(&*self.jsw); + } +} + +impl JaySelectWorkspace { + fn send_cancelled(&self) { + self.client.event(Cancelled { self_id: self.id }); + } + + fn send_selected(&self, output: u32, id: JayWorkspaceId) { + self.client.event(Selected { + self_id: self.id, + output, + id, + }); + } +} + +impl JaySelectWorkspaceRequestHandler for JaySelectWorkspace { + type Error = JaySelectWorkspaceError; +} + +object_base! { + self = JaySelectWorkspace; + version = Version(1); +} + +impl Object for JaySelectWorkspace { + fn break_loops(&self) { + self.destroyed.set(true); + } +} + +simple_add_obj!(JaySelectWorkspace); + +#[derive(Debug, Error)] +pub enum JaySelectWorkspaceError { + #[error(transparent)] + ClientError(Box), +} +efrom!(JaySelectWorkspaceError, ClientError); diff --git a/src/ifs/jay_workspace.rs b/src/ifs/jay_workspace.rs index 1c430f1f..f9a40eb4 100644 --- a/src/ifs/jay_workspace.rs +++ b/src/ifs/jay_workspace.rs @@ -19,6 +19,14 @@ pub struct JayWorkspace { } impl JayWorkspace { + pub fn send_initial_properties(&self, workspace: &WorkspaceNode) { + self.send_linear_id(workspace); + self.send_name(workspace); + self.send_output(&workspace.output.get()); + self.send_visible(workspace.visible.get()); + self.send_done(); + } + pub fn send_linear_id(&self, ws: &WorkspaceNode) { self.client.event(LinearId { self_id: self.id, diff --git a/src/ifs/jay_workspace_watcher.rs b/src/ifs/jay_workspace_watcher.rs index 0e6ab76d..902da994 100644 --- a/src/ifs/jay_workspace_watcher.rs +++ b/src/ifs/jay_workspace_watcher.rs @@ -36,11 +36,7 @@ impl JayWorkspaceWatcher { id: jw.id, linear_id: workspace.id.raw(), }); - jw.send_linear_id(workspace); - jw.send_name(workspace); - jw.send_output(&workspace.output.get()); - jw.send_visible(workspace.visible.get()); - jw.send_done(); + jw.send_initial_properties(workspace); Ok(()) } diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 56f90451..04805b19 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -83,7 +83,10 @@ use { thiserror::Error, uapi::OwnedFd, }; -pub use {event_handling::NodeSeatState, pointer_owner::ToplevelSelector}; +pub use { + event_handling::NodeSeatState, + pointer_owner::{ToplevelSelector, WorkspaceSelector}, +}; pub const POINTER: u32 = 1; const KEYBOARD: u32 = 2; @@ -1153,10 +1156,13 @@ impl WlSeatGlobal { self.forward.set(forward); } - #[allow(dead_code)] pub fn select_toplevel(self: &Rc, selector: impl ToplevelSelector) { self.pointer_owner.select_toplevel(self, selector); } + + pub fn select_workspace(self: &Rc, selector: impl WorkspaceSelector) { + self.pointer_owner.select_workspace(self, selector); + } } global_base!(WlSeatGlobal, WlSeat, WlSeatError); diff --git a/src/ifs/wl_seat/pointer_owner.rs b/src/ifs/wl_seat/pointer_owner.rs index f2c8fe9f..f02e1605 100644 --- a/src/ifs/wl_seat/pointer_owner.rs +++ b/src/ifs/wl_seat/pointer_owner.rs @@ -14,7 +14,7 @@ use { xdg_toplevel_drag_v1::XdgToplevelDragV1, }, state::DeviceHandlerData, - tree::{FindTreeUsecase, FoundNode, Node, ToplevelNode}, + tree::{FindTreeUsecase, FoundNode, Node, ToplevelNode, WorkspaceNode}, utils::{clonecell::CloneCell, smallmap::SmallMap}, }, std::{ @@ -33,6 +33,10 @@ pub trait ToplevelSelector: 'static { fn set(&self, toplevel: Rc); } +pub trait WorkspaceSelector: 'static { + fn set(&self, ws: Rc); +} + impl Default for PointerOwnerHolder { fn default() -> Self { let default = Rc::new(SimplePointerOwner { @@ -157,19 +161,32 @@ impl PointerOwnerHolder { seat.changes.or_assign(CHANGE_CURSOR_MOVED); } - pub fn select_toplevel(&self, seat: &Rc, selector: impl ToplevelSelector) { + fn select_element(&self, seat: &Rc, usecase: impl SimplePointerOwnerUsecase) { self.revert_to_default(seat); - let usecase = Rc::new(SelectToplevelUsecase { - seat: Rc::downgrade(seat), - selector, - latest: Default::default(), - }); if let Some(node) = seat.pointer_stack.borrow().last() { usecase.node_focus(seat, node); } self.owner.set(Rc::new(SimplePointerOwner { usecase })); seat.trigger_tree_changed(); } + + pub fn select_toplevel(&self, seat: &Rc, selector: impl ToplevelSelector) { + let usecase = Rc::new(SelectToplevelUsecase { + seat: Rc::downgrade(seat), + selector, + latest: Default::default(), + }); + self.select_element(seat, usecase) + } + + pub fn select_workspace(&self, seat: &Rc, selector: impl WorkspaceSelector) { + let usecase = Rc::new(SelectWorkspaceUsecase { + seat: Rc::downgrade(seat), + selector, + latest: Default::default(), + }); + self.select_element(seat, usecase) + } } trait PointerOwner { @@ -221,6 +238,12 @@ struct SelectToplevelUsecase { selector: S, } +struct SelectWorkspaceUsecase { + seat: Weak, + latest: CloneCell>>, + selector: S, +} + impl PointerOwner for SimplePointerOwner { fn button(&self, seat: &Rc, time_usec: u64, button: u32, state: KeyState) { if state != KeyState::Pressed { @@ -674,8 +697,22 @@ impl SimplePointerOwnerUsecase for DefaultPointerUsecase { } } -impl SimplePointerOwnerUsecase for Rc> { - const FIND_TREE_USECASE: FindTreeUsecase = FindTreeUsecase::SelectToplevel; +trait NodeSelectorUsecase: Sized + 'static { + const FIND_TREE_USECASE: FindTreeUsecase; + + fn default_button( + self: &Rc, + spo: &SimplePointerOwner>, + seat: &Rc, + button: u32, + pn: &Rc, + ) -> bool; + + fn node_focus(self: &Rc, seat: &Rc, node: &Rc); +} + +impl SimplePointerOwnerUsecase for Rc { + const FIND_TREE_USECASE: FindTreeUsecase = ::FIND_TREE_USECASE; const IS_DEFAULT: bool = false; fn default_button( @@ -685,17 +722,7 @@ impl SimplePointerOwnerUsecase for Rc, ) -> bool { - let Some(tl) = pn.clone().node_into_toplevel() else { - return false; - }; - let selected_toplevel = - button == BTN_RIGHT || (button == BTN_LEFT && !tl.tl_admits_children()); - if !selected_toplevel { - return false; - } - self.selector.set(tl); - spo.revert_to_default(seat); - true + ::default_button(self, spo, seat, button, pn) } fn start_drag( @@ -721,6 +748,34 @@ impl SimplePointerOwnerUsecase for Rc, node: &Rc) { + ::node_focus(self, seat, node) + } +} + +impl NodeSelectorUsecase for SelectToplevelUsecase { + const FIND_TREE_USECASE: FindTreeUsecase = FindTreeUsecase::SelectToplevel; + + fn default_button( + self: &Rc, + spo: &SimplePointerOwner>, + seat: &Rc, + button: u32, + pn: &Rc, + ) -> bool { + let Some(tl) = pn.clone().node_into_toplevel() else { + return false; + }; + let selected_toplevel = + button == BTN_RIGHT || (button == BTN_LEFT && !tl.tl_admits_children()); + if !selected_toplevel { + return false; + } + self.selector.set(tl); + spo.revert_to_default(seat); + true + } + + fn node_focus(self: &Rc, seat: &Rc, node: &Rc) { let mut damage = false; let tl = node.clone().node_into_toplevel(); if let Some(tl) = &tl { @@ -750,3 +805,50 @@ impl Drop for SelectToplevelUsecase { } } } + +impl NodeSelectorUsecase for SelectWorkspaceUsecase { + const FIND_TREE_USECASE: FindTreeUsecase = FindTreeUsecase::SelectWorkspace; + + fn default_button( + self: &Rc, + spo: &SimplePointerOwner>, + seat: &Rc, + _button: u32, + pn: &Rc, + ) -> bool { + let Some(ws) = pn.clone().node_into_workspace() else { + return false; + }; + self.selector.set(ws); + spo.revert_to_default(seat); + true + } + + fn node_focus(self: &Rc, seat: &Rc, node: &Rc) { + let mut damage = false; + let ws = node.clone().node_into_workspace(); + if let Some(ws) = &ws { + ws.render_highlight.fetch_add(1); + seat.set_known_cursor(KnownCursor::Pointer); + damage = true; + } + if let Some(prev) = self.latest.set(ws) { + prev.render_highlight.fetch_sub(1); + damage = true; + } + if damage { + seat.state.damage(); + } + } +} + +impl Drop for SelectWorkspaceUsecase { + fn drop(&mut self) { + if let Some(prev) = self.latest.take() { + prev.render_highlight.fetch_sub(1); + if let Some(seat) = self.seat.upgrade() { + seat.state.damage(); + } + } + } +} diff --git a/src/portal/ptl_display.rs b/src/portal/ptl_display.rs index 8b8fd14c..84539061 100644 --- a/src/portal/ptl_display.rs +++ b/src/portal/ptl_display.rs @@ -297,7 +297,7 @@ fn finish_display_connect(dpy: Rc) { id: dpy.con.id(), con: dpy.con.clone(), owner: Default::default(), - window_capture: Cell::new(false), + caps: Default::default(), }); 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 cfc730f3..00c42514 100644 --- a/src/portal/ptl_screencast.rs +++ b/src/portal/ptl_screencast.rs @@ -37,7 +37,9 @@ use { wl_usr::usr_ifs::{ usr_jay_screencast::{UsrJayScreencast, UsrJayScreencastOwner}, usr_jay_select_toplevel::UsrJaySelectToplevel, + usr_jay_select_workspace::UsrJaySelectWorkspace, usr_jay_toplevel::UsrJayToplevel, + usr_jay_workspace::UsrJayWorkspace, }, }, std::{ @@ -64,6 +66,7 @@ pub enum ScreencastPhase { SourcesSelected, Selecting(Rc), SelectingWindow(Rc), + SelectingWorkspace(Rc), Starting(Rc), Started(Rc), Terminated, @@ -89,6 +92,12 @@ pub struct SelectingWindowScreencast { pub selector: Rc, } +pub struct SelectingWorkspaceScreencast { + pub core: SelectingScreencastCore, + pub dpy: Rc, + pub selector: Rc, +} + pub struct StartingScreencast { pub session: Rc, pub request_obj: Rc, @@ -100,6 +109,7 @@ pub struct StartingScreencast { pub enum ScreencastTarget { Output(Rc), + Workspace(Rc, Rc), Toplevel(Rc), } @@ -156,14 +166,26 @@ impl PwClientNodeOwner for StartingScreencast { port.supported_metas.set(SUPPORTED_META_VIDEO_CROP); let jsc = self.dpy.jc.create_screencast(); match &self.target { - ScreencastTarget::Output(o) => jsc.set_output(&o.jay), + ScreencastTarget::Output(o) => { + jsc.set_output(&o.jay); + jsc.set_allow_all_workspaces(true); + } + ScreencastTarget::Workspace(o, ws) => { + jsc.set_output(&o.jay); + jsc.allow_workspace(ws); + } 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); + match &self.target { + ScreencastTarget::Output(_) => {} + ScreencastTarget::Workspace(_, w) => { + self.dpy.con.remove_obj(&**w); + } + ScreencastTarget::Toplevel(t) => { + self.dpy.con.remove_obj(&**t); + } } let started = Rc::new(StartedScreencast { session: self.session.clone(), @@ -253,12 +275,22 @@ impl ScreencastSession { s.dpy.con.remove_obj(&*s.selector); s.core.reply.err("Session has been terminated"); } + ScreencastPhase::SelectingWorkspace(s) => { + s.dpy.con.remove_obj(&*s.selector); + s.core.reply.err("Session has been terminated"); + } ScreencastPhase::Starting(s) => { s.reply.err("Session has been terminated"); s.node.con.destroy_obj(s.node.deref()); s.dpy.screencasts.remove(self.session_obj.path()); - if let ScreencastTarget::Toplevel(t) = &s.target { - s.dpy.con.remove_obj(&**t); + match &s.target { + ScreencastTarget::Output(_) => {} + ScreencastTarget::Workspace(_, w) => { + s.dpy.con.remove_obj(&**w); + } + ScreencastTarget::Toplevel(t) => { + s.dpy.con.remove_obj(&**t); + } } } ScreencastPhase::Started(s) => { diff --git a/src/portal/ptl_screencast/screencast_gui.rs b/src/portal/ptl_screencast/screencast_gui.rs index 2a91d473..016c9b66 100644 --- a/src/portal/ptl_screencast/screencast_gui.rs +++ b/src/portal/ptl_screencast/screencast_gui.rs @@ -5,6 +5,7 @@ use { ptl_display::{PortalDisplay, PortalOutput, PortalSeat}, ptl_screencast::{ ScreencastPhase, ScreencastSession, ScreencastTarget, SelectingWindowScreencast, + SelectingWorkspaceScreencast, }, ptr_gui::{ Align, Button, ButtonOwner, Flow, GuiElement, Label, Orientation, OverlayWindow, @@ -14,7 +15,9 @@ use { theme::Color, utils::copyhashmap::CopyHashMap, wl_usr::usr_ifs::{ - usr_jay_select_toplevel::UsrJaySelectToplevelOwner, usr_jay_toplevel::UsrJayToplevel, + usr_jay_select_toplevel::UsrJaySelectToplevelOwner, + usr_jay_select_workspace::UsrJaySelectWorkspaceOwner, usr_jay_toplevel::UsrJayToplevel, + usr_jay_workspace::UsrJayWorkspace, }, }, std::rc::Rc, @@ -43,7 +46,8 @@ struct StaticButton { #[derive(Copy, Clone, Eq, PartialEq)] enum ButtonRole { Accept, - Window, + SelectWorkspace, + SelectWindow, Reject, } @@ -71,14 +75,20 @@ 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 workspace_button = static_button(surface, ButtonRole::SelectWorkspace, "Share A Workspcae"); + let window_button = static_button(surface, ButtonRole::SelectWindow, "Share A Window"); let reject_button = static_button(surface, ButtonRole::Reject, "Reject"); - for button in [&accept_button, &window_button, &reject_button] { + for button in [ + &accept_button, + &workspace_button, + &window_button, + &reject_button, + ] { button.border_color.set(Color::from_gray(100)); button.border.set(2.0); button.padding.set(5.0); } - for button in [&accept_button, &window_button] { + for button in [&accept_button, &workspace_button, &window_button] { button.bg_color.set(Color::from_rgb(170, 200, 170)); button.bg_hover_color.set(Color::from_rgb(170, 255, 170)); } @@ -92,7 +102,10 @@ fn create_accept_gui(surface: &Rc) -> Rc { flow.in_margin.set(V_MARGIN); flow.cross_margin.set(H_MARGIN); let mut elements: Vec> = vec![label, accept_button]; - if surface.gui.dpy.jc.window_capture.get() { + if surface.gui.dpy.jc.caps.select_workspace.get() { + elements.push(workspace_button); + } + if surface.gui.dpy.jc.caps.window_capture.get() { elements.push(window_button); } elements.push(reject_button); @@ -140,7 +153,7 @@ impl ButtonOwner for StaticButton { return; } match self.role { - ButtonRole::Accept | ButtonRole::Window => { + ButtonRole::Accept | ButtonRole::SelectWorkspace | ButtonRole::SelectWindow => { log::info!("User has accepted the request"); let selecting = match self.surface.gui.screencast_session.phase.get() { ScreencastPhase::Selecting(selecting) => selecting, @@ -154,6 +167,19 @@ impl ButtonOwner for StaticButton { selecting .core .starting(dpy, ScreencastTarget::Output(self.surface.output.clone())); + } else if self.role == ButtonRole::SelectWorkspace { + let selector = dpy.jc.select_workspace(&seat.wl); + let selecting = Rc::new(SelectingWorkspaceScreencast { + core: selecting.core.clone(), + dpy: dpy.clone(), + selector: selector.clone(), + }); + selector.owner.set(Some(selecting.clone())); + self.surface + .gui + .screencast_session + .phase + .set(ScreencastPhase::SelectingWorkspace(selecting)); } else { let selector = dpy.jc.select_toplevel(&seat.wl); let selecting = Rc::new(SelectingWindowScreencast { @@ -199,6 +225,37 @@ impl UsrJaySelectToplevelOwner for SelectingWindowScreencast { } } +impl UsrJaySelectWorkspaceOwner for SelectingWorkspaceScreencast { + fn done(&self, output: u32, ws: Option>) { + let Some(ws) = ws else { + log::info!("User has aborted the selection"); + self.core.session.kill(); + return; + }; + match self.core.session.phase.get() { + ScreencastPhase::SelectingWorkspace(s) => { + self.dpy.con.remove_obj(&*s.selector); + } + _ => { + self.dpy.con.remove_obj(&*ws); + return; + } + } + log::info!("User has selected a workspace"); + let output = match self.dpy.outputs.get(&output) { + Some(o) => o, + _ => { + log::warn!("Workspace does not belong to any known output"); + self.dpy.con.remove_obj(&*ws); + self.core.session.kill(); + return; + } + }; + self.core + .starting(&self.dpy, ScreencastTarget::Workspace(output, ws)); + } +} + fn static_button(surface: &Rc, role: ButtonRole, text: &str) -> Rc