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