diff --git a/jay-config/src/theme.rs b/jay-config/src/theme.rs index 3cbc2331..38e3a19e 100644 --- a/jay-config/src/theme.rs +++ b/jay-config/src/theme.rs @@ -257,6 +257,10 @@ pub mod colors { /// /// Default: `#23092c`. const 14 => ATTENTION_REQUESTED_BACKGROUND_COLOR, + /// Color used to highlight parts of the UI. + /// + /// Default: `#9d28c67f`. + const 15 => HIGHLIGHT_COLOR, } /// Sets the color of GUI element. 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/config/handler.rs b/src/config/handler.rs index c778d0a0..b1f2f263 100644 --- a/src/config/handler.rs +++ b/src/config/handler.rs @@ -1430,6 +1430,7 @@ impl ConfigProxyHandler { FOCUSED_INACTIVE_TITLE_TEXT_COLOR => &colors.focused_inactive_title_text, BAR_STATUS_TEXT_COLOR => &colors.bar_text, ATTENTION_REQUESTED_BACKGROUND_COLOR => &colors.attention_requested_background, + HIGHLIGHT_COLOR => &colors.highlight, _ => return Err(CphError::UnknownColor(colorable.0)), }; Ok(colorable) 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 eb714db3..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, }; @@ -44,6 +51,7 @@ impl JayCompositorGlobal { }); track!(client, obj); client.add_client_obj(&obj)?; + obj.send_capabilities(); Ok(()) } } @@ -72,7 +80,21 @@ pub struct JayCompositor { tracker: Tracker, } +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::WINDOW_CAPTURE], + }); + } + fn take_screenshot_impl( &self, id: JayScreenshotId, @@ -322,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_seat.rs b/src/ifs/wl_seat.rs index 029f082a..56f90451 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -11,7 +11,6 @@ pub mod zwp_relative_pointer_v1; pub mod zwp_virtual_keyboard_manager_v1; pub mod zwp_virtual_keyboard_v1; -pub use event_handling::NodeSeatState; use { crate::{ async_engine::SpawnedFuture, @@ -84,6 +83,7 @@ use { thiserror::Error, uapi::OwnedFd, }; +pub use {event_handling::NodeSeatState, pointer_owner::ToplevelSelector}; pub const POINTER: u32 = 1; const KEYBOARD: u32 = 2; @@ -94,6 +94,7 @@ const TOUCH: u32 = 4; const MISSING_CAPABILITY: u32 = 0; pub const BTN_LEFT: u32 = 0x110; +pub const BTN_RIGHT: u32 = 0x111; pub const SEAT_NAME_SINCE: Version = Version(2); @@ -1151,6 +1152,11 @@ impl WlSeatGlobal { pub fn set_forward(&self, forward: bool) { self.forward.set(forward); } + + #[allow(dead_code)] + pub fn select_toplevel(self: &Rc, selector: impl ToplevelSelector) { + self.pointer_owner.select_toplevel(self, selector); + } } global_base!(WlSeatGlobal, WlSeat, WlSeatError); diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index 50377725..92d86a21 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -39,7 +39,7 @@ use { isnt::std_1::primitive::{IsntSlice2Ext, IsntSliceExt}, jay_config::keyboard::{ mods::{Modifiers, CAPS, NUM, RELEASE}, - syms::KeySym, + syms::{KeySym, SYM_Escape}, }, smallvec::SmallVec, std::{cell::RefCell, collections::hash_map::Entry, rc::Rc}, @@ -346,7 +346,7 @@ impl WlSeatGlobal { } pub(super) fn key_event( - &self, + self: &Rc, time_usec: u64, key: u32, key_state: KeyState, @@ -375,14 +375,17 @@ impl WlSeatGlobal { let mut shortcuts = SmallVec::<[_; 1]>::new(); let new_mods; { - if !self.state.lock.locked.get() { - let mut mods = xkb_state.mods().mods_effective & !(CAPS.0 | NUM.0); - if state == wl_keyboard::RELEASED { - mods |= RELEASE.0; + let mut mods = xkb_state.mods().mods_effective & !(CAPS.0 | NUM.0); + if state == wl_keyboard::RELEASED { + mods |= RELEASE.0; + } + let scs = &*self.shortcuts.borrow(); + let keysyms = xkb_state.unmodified_keysyms(key); + for &sym in keysyms { + if sym == SYM_Escape.0 && mods == 0 { + self.pointer_owner.revert_to_default(self); } - let scs = &*self.shortcuts.borrow(); - let keysyms = xkb_state.unmodified_keysyms(key); - for &sym in keysyms { + if !self.state.lock.locked.get() { if let Some(key_mods) = scs.get(&sym) { for (key_mods, mask) in key_mods { if mods & mask == key_mods { diff --git a/src/ifs/wl_seat/pointer_owner.rs b/src/ifs/wl_seat/pointer_owner.rs index 7d4490c2..f2c8fe9f 100644 --- a/src/ifs/wl_seat/pointer_owner.rs +++ b/src/ifs/wl_seat/pointer_owner.rs @@ -1,35 +1,46 @@ use { crate::{ backend::{AxisSource, KeyState, ScrollAxis, AXIS_120}, + cursor::KnownCursor, fixed::Fixed, ifs::{ ipc, ipc::wl_data_source::WlDataSource, wl_seat::{ - wl_pointer::PendingScroll, Dnd, DroppedDnd, WlSeatError, WlSeatGlobal, - CHANGE_CURSOR_MOVED, + wl_pointer::PendingScroll, Dnd, DroppedDnd, WlSeatError, WlSeatGlobal, BTN_LEFT, + BTN_RIGHT, CHANGE_CURSOR_MOVED, }, wl_surface::WlSurface, xdg_toplevel_drag_v1::XdgToplevelDragV1, }, state::DeviceHandlerData, - tree::{FoundNode, Node}, + tree::{FindTreeUsecase, FoundNode, Node, ToplevelNode}, utils::{clonecell::CloneCell, smallmap::SmallMap}, }, - std::{cell::Cell, rc::Rc}, + std::{ + cell::Cell, + rc::{Rc, Weak}, + }, }; pub struct PointerOwnerHolder { - default: Rc, + default: Rc>, owner: CloneCell>, pending_scroll: PendingScroll, } +pub trait ToplevelSelector: 'static { + fn set(&self, toplevel: Rc); +} + impl Default for PointerOwnerHolder { fn default() -> Self { + let default = Rc::new(SimplePointerOwner { + usecase: DefaultPointerUsecase, + }); Self { - default: Rc::new(DefaultPointerOwner), - owner: CloneCell::new(Rc::new(DefaultPointerOwner)), + default: default.clone(), + owner: CloneCell::new(default.clone()), pending_scroll: Default::default(), } } @@ -145,6 +156,20 @@ impl PointerOwnerHolder { seat.pointer_owner.owner.set(self.default.clone()); seat.changes.or_assign(CHANGE_CURSOR_MOVED); } + + pub fn select_toplevel(&self, seat: &Rc, selector: impl ToplevelSelector) { + 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(); + } } trait PointerOwner { @@ -167,9 +192,12 @@ trait PointerOwner { fn remove_dnd_icon(&self); } -struct DefaultPointerOwner; +struct SimplePointerOwner { + usecase: T, +} -struct GrabPointerOwner { +struct SimpleGrabPointerOwner { + usecase: T, buttons: SmallMap, node: Rc, serial: u32, @@ -184,7 +212,16 @@ struct DndPointerOwner { pos_y: Cell, } -impl PointerOwner for DefaultPointerOwner { +#[derive(Copy, Clone)] +struct DefaultPointerUsecase; + +struct SelectToplevelUsecase { + 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 { return; @@ -193,12 +230,18 @@ impl PointerOwner for DefaultPointerOwner { Some(n) => n, _ => return, }; + if self.usecase.default_button(self, seat, button, &pn) { + return; + } let serial = seat.state.next_serial(pn.node_client().as_deref()); - seat.pointer_owner.owner.set(Rc::new(GrabPointerOwner { - buttons: SmallMap::new_with(button, ()), - node: pn.clone(), - serial, - })); + seat.pointer_owner + .owner + .set(Rc::new(SimpleGrabPointerOwner { + usecase: self.usecase.clone(), + buttons: SmallMap::new_with(button, ()), + node: pn.clone(), + serial, + })); pn.node_seat_state().add_pointer_grab(seat); pn.node_on_button(seat, time_usec, button, state, serial); } @@ -220,7 +263,7 @@ impl PointerOwner for DefaultPointerOwner { }); seat.state .root - .node_find_tree_at(x_int, y_int, &mut found_tree); + .node_find_tree_at(x_int, y_int, &mut found_tree, T::FIND_TREE_USECASE); let mut divergence = found_tree.len().min(stack.len()); for (i, (found, stack)) in found_tree.iter().zip(stack.iter()).enumerate() { if found.node.node_id() != stack.node_id() { @@ -266,6 +309,7 @@ impl PointerOwner for DefaultPointerOwner { } if let Some(node) = stack.last() { node.node_on_pointer_focus(seat); + self.usecase.node_focus(seat, node); } } found_tree.clear(); @@ -289,8 +333,12 @@ impl PointerOwner for DefaultPointerOwner { seat.dropped_dnd.borrow_mut().take(); } - fn revert_to_default(&self, _seat: &Rc) { - // nothing + fn revert_to_default(&self, seat: &Rc) { + if !T::IS_DEFAULT { + seat.pointer_owner.set_default_pointer_owner(seat); + seat.trigger_tree_changed(); + seat.state.damage(); + } } fn dnd_target_removed(&self, seat: &Rc) { @@ -310,7 +358,7 @@ impl PointerOwner for DefaultPointerOwner { } } -impl PointerOwner for GrabPointerOwner { +impl PointerOwner for SimpleGrabPointerOwner { fn button(&self, seat: &Rc, time_usec: u64, button: u32, state: KeyState) { match state { KeyState::Released => { @@ -318,7 +366,7 @@ impl PointerOwner for GrabPointerOwner { if self.buttons.is_empty() { self.node.node_seat_state().remove_pointer_grab(seat); // log::info!("button"); - seat.pointer_owner.set_default_pointer_owner(seat); + self.usecase.release_grab(seat); seat.tree_changed.trigger(); } } @@ -354,57 +402,8 @@ impl PointerOwner for GrabPointerOwner { icon: Option>, serial: u32, ) -> Result<(), WlSeatError> { - let button = match self.buttons.iter().next() { - Some((b, _)) => b, - None => return Ok(()), - }; - if self.buttons.len() != 1 { - return Ok(()); - } - if serial != self.serial { - return Ok(()); - } - if self.node.node_id() != origin.node_id { - return Ok(()); - } - if let Some(icon) = &icon { - icon.set_dnd_icon_seat(seat.id, Some(seat)); - } - if let Some(new) = &src { - ipc::attach_seat(&**new, seat, ipc::Role::Dnd)?; - if let Some(drag) = new.toplevel_drag.get() { - drag.start_drag(); - } - } - *seat.dropped_dnd.borrow_mut() = None; - let pointer_owner = Rc::new(DndPointerOwner { - button, - dnd: Dnd { - seat: seat.clone(), - client: origin.client.clone(), - src, - }, - target: CloneCell::new(seat.state.root.clone()), - icon: CloneCell::new(icon), - pos_x: Cell::new(Fixed::from_int(0)), - pos_y: Cell::new(Fixed::from_int(0)), - }); - { - let mut stack = seat.pointer_stack.borrow_mut(); - for node in stack.drain(1..).rev() { - node.node_on_leave(seat); - node.node_seat_state().leave(seat); - } - } - self.node.node_seat_state().remove_pointer_grab(seat); - // { - // let old = seat.keyboard_node.set(seat.state.root.clone()); - // old.seat_state().unfocus(seat); - // old.unfocus(seat); - // } - seat.pointer_owner.owner.set(pointer_owner.clone()); - pointer_owner.apply_changes(seat); - Ok(()) + self.usecase + .start_drag(self, seat, origin, src, icon, serial) } fn cancel_dnd(&self, seat: &Rc) { @@ -482,7 +481,7 @@ impl PointerOwner for DndPointerOwner { }); seat.state .root - .node_find_tree_at(x_int, y_int, &mut found_tree); + .node_find_tree_at(x_int, y_int, &mut found_tree, FindTreeUsecase::None); let FoundNode { node, x, y } = found_tree.pop().unwrap(); found_tree.clear(); (node, x, y) @@ -562,3 +561,192 @@ impl PointerOwner for DndPointerOwner { self.icon.set(None); } } + +trait SimplePointerOwnerUsecase: Sized + Clone + 'static { + const FIND_TREE_USECASE: FindTreeUsecase; + const IS_DEFAULT: bool; + + fn default_button( + &self, + spo: &SimplePointerOwner, + seat: &Rc, + button: u32, + pn: &Rc, + ) -> bool; + + fn start_drag( + &self, + grab: &SimpleGrabPointerOwner, + seat: &Rc, + origin: &Rc, + src: Option>, + icon: Option>, + serial: u32, + ) -> Result<(), WlSeatError>; + + fn release_grab(&self, seat: &Rc); + + fn node_focus(&self, seat: &Rc, node: &Rc); +} + +impl SimplePointerOwnerUsecase for DefaultPointerUsecase { + const FIND_TREE_USECASE: FindTreeUsecase = FindTreeUsecase::None; + const IS_DEFAULT: bool = true; + + fn default_button( + &self, + _spo: &SimplePointerOwner, + _seat: &Rc, + _button: u32, + _pn: &Rc, + ) -> bool { + false + } + + fn start_drag( + &self, + grab: &SimpleGrabPointerOwner, + seat: &Rc, + origin: &Rc, + src: Option>, + icon: Option>, + serial: u32, + ) -> Result<(), WlSeatError> { + let button = match grab.buttons.iter().next() { + Some((b, _)) => b, + None => return Ok(()), + }; + if grab.buttons.len() != 1 { + return Ok(()); + } + if serial != grab.serial { + return Ok(()); + } + if grab.node.node_id() != origin.node_id { + return Ok(()); + } + if let Some(icon) = &icon { + icon.set_dnd_icon_seat(seat.id, Some(seat)); + } + if let Some(new) = &src { + ipc::attach_seat(&**new, seat, ipc::Role::Dnd)?; + if let Some(drag) = new.toplevel_drag.get() { + drag.start_drag(); + } + } + *seat.dropped_dnd.borrow_mut() = None; + let pointer_owner = Rc::new(DndPointerOwner { + button, + dnd: Dnd { + seat: seat.clone(), + client: origin.client.clone(), + src, + }, + target: CloneCell::new(seat.state.root.clone()), + icon: CloneCell::new(icon), + pos_x: Cell::new(Fixed::from_int(0)), + pos_y: Cell::new(Fixed::from_int(0)), + }); + { + let mut stack = seat.pointer_stack.borrow_mut(); + for node in stack.drain(1..).rev() { + node.node_on_leave(seat); + node.node_seat_state().leave(seat); + } + } + grab.node.node_seat_state().remove_pointer_grab(seat); + // { + // let old = seat.keyboard_node.set(seat.state.root.clone()); + // old.seat_state().unfocus(seat); + // old.unfocus(seat); + // } + seat.pointer_owner.owner.set(pointer_owner.clone()); + pointer_owner.apply_changes(seat); + Ok(()) + } + + fn release_grab(&self, seat: &Rc) { + seat.pointer_owner.set_default_pointer_owner(seat); + } + + fn node_focus(&self, _seat: &Rc, _node: &Rc) { + // nothing + } +} + +impl SimplePointerOwnerUsecase for Rc> { + const FIND_TREE_USECASE: FindTreeUsecase = FindTreeUsecase::SelectToplevel; + const IS_DEFAULT: bool = false; + + fn default_button( + &self, + 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 start_drag( + &self, + _grab: &SimpleGrabPointerOwner, + seat: &Rc, + _origin: &Rc, + src: Option>, + _icon: Option>, + _serial: u32, + ) -> Result<(), WlSeatError> { + if let Some(src) = src { + src.send_cancelled(seat); + } + Ok(()) + } + + fn release_grab(&self, seat: &Rc) { + seat.pointer_owner.owner.set(Rc::new(SimplePointerOwner { + usecase: self.clone(), + })); + seat.changes.or_assign(CHANGE_CURSOR_MOVED); + } + + fn node_focus(&self, seat: &Rc, node: &Rc) { + let mut damage = false; + let tl = node.clone().node_into_toplevel(); + if let Some(tl) = &tl { + tl.tl_data().render_highlight.fetch_add(1); + if !tl.tl_admits_children() { + seat.set_known_cursor(KnownCursor::Pointer); + } + damage = true; + } + if let Some(prev) = self.latest.set(tl) { + prev.tl_data().render_highlight.fetch_sub(1); + damage = true; + } + if damage { + seat.state.damage(); + } + } +} + +impl Drop for SelectToplevelUsecase { + fn drop(&mut self) { + if let Some(prev) = self.latest.take() { + prev.tl_data().render_highlight.fetch_sub(1); + if let Some(seat) = self.seat.upgrade() { + seat.state.damage(); + } + } + } +} 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/ifs/wl_surface/ext_session_lock_surface_v1.rs b/src/ifs/wl_surface/ext_session_lock_surface_v1.rs index b708b0db..dcc36b78 100644 --- a/src/ifs/wl_surface/ext_session_lock_surface_v1.rs +++ b/src/ifs/wl_surface/ext_session_lock_surface_v1.rs @@ -9,7 +9,7 @@ use { leaks::Tracker, object::{Object, Version}, rect::Rect, - tree::{FindTreeResult, FoundNode, Node, NodeId, NodeVisitor, OutputNode}, + tree::{FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, OutputNode}, utils::numcell::NumCell, wire::{ext_session_lock_surface_v1::*, ExtSessionLockSurfaceV1Id, WlSurfaceId}, }, @@ -122,7 +122,13 @@ impl Node for ExtSessionLockSurfaceV1 { self.surface.node_absolute_position() } - fn node_find_tree_at(&self, x: i32, y: i32, tree: &mut Vec) -> FindTreeResult { + fn node_find_tree_at( + &self, + x: i32, + y: i32, + tree: &mut Vec, + _usecase: FindTreeUsecase, + ) -> FindTreeResult { self.surface.find_tree_at_(x, y, tree) } diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index cb06db52..28b8c391 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -11,8 +11,8 @@ use { renderer::Renderer, state::State, tree::{ - Direction, FindTreeResult, FoundNode, Node, NodeId, NodeVisitor, StackedNode, - ToplevelData, ToplevelNode, ToplevelNodeBase, WorkspaceNode, + Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, + StackedNode, ToplevelData, ToplevelNode, ToplevelNodeBase, WorkspaceNode, }, utils::{clonecell::CloneCell, copyhashmap::CopyHashMap, linkedlist::LinkedNode}, wire::WlSurfaceId, @@ -326,7 +326,16 @@ impl Node for Xwindow { self.toplevel_data.update_self_active(self, active); } - fn node_find_tree_at(&self, x: i32, y: i32, tree: &mut Vec) -> FindTreeResult { + fn node_find_tree_at( + &self, + x: i32, + y: i32, + tree: &mut Vec, + usecase: FindTreeUsecase, + ) -> FindTreeResult { + if usecase == FindTreeUsecase::SelectToplevel { + return FindTreeResult::AcceptsInput; + } let rect = self.x.surface.buffer_abs_pos.get(); if x < rect.width() && y < rect.height() { tree.push(FoundNode { @@ -340,7 +349,7 @@ impl Node for Xwindow { } fn node_render(&self, renderer: &mut Renderer, x: i32, y: i32, bounds: Option<&Rect>) { - renderer.render_surface(&self.x.surface, x, y, bounds) + renderer.render_xwindow(self, x, y, bounds) } fn node_client(&self) -> Option> { @@ -359,6 +368,10 @@ impl Node for Xwindow { // log::info!("wl-surface focus"); seat.set_known_cursor(KnownCursor::Default); } + + fn node_into_toplevel(self: Rc) -> Option> { + Some(self) + } } impl ToplevelNodeBase for Xwindow { @@ -428,6 +441,10 @@ impl ToplevelNodeBase for Xwindow { fn tl_scanout_surface(&self) -> Option> { Some(self.x.surface.clone()) } + + fn tl_admits_children(&self) -> bool { + false + } } impl StackedNode for Xwindow { diff --git a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs index 56dac416..96bb64a1 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs @@ -15,7 +15,10 @@ use { object::Object, rect::Rect, renderer::Renderer, - tree::{FindTreeResult, FoundNode, Node, NodeId, NodeVisitor, StackedNode, WorkspaceNode}, + tree::{ + FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, StackedNode, + WorkspaceNode, + }, utils::{clonecell::CloneCell, linkedlist::LinkedNode}, wire::{xdg_popup::*, XdgPopupId}, }, @@ -314,7 +317,16 @@ impl Node for XdgPopup { self.xdg.absolute_desired_extents.get() } - fn node_find_tree_at(&self, x: i32, y: i32, tree: &mut Vec) -> FindTreeResult { + fn node_find_tree_at( + &self, + x: i32, + y: i32, + tree: &mut Vec, + usecase: FindTreeUsecase, + ) -> FindTreeResult { + if usecase == FindTreeUsecase::SelectToplevel { + return FindTreeResult::Other; + } self.xdg.find_tree_at(x, y, tree) } diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index 54bc9815..48b81111 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -20,8 +20,9 @@ use { renderer::Renderer, state::State, tree::{ - Direction, FindTreeResult, FoundNode, Node, NodeId, NodeVisitor, OutputNode, - ToplevelData, ToplevelNode, ToplevelNodeBase, ToplevelNodeId, WorkspaceNode, + Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, + OutputNode, ToplevelData, ToplevelNode, ToplevelNodeBase, ToplevelNodeId, + WorkspaceNode, }, utils::clonecell::CloneCell, wire::{xdg_toplevel::*, XdgToplevelId}, @@ -492,12 +493,21 @@ impl Node for XdgToplevel { self.toplevel_data.update_self_active(self, active); } - fn node_find_tree_at(&self, x: i32, y: i32, tree: &mut Vec) -> FindTreeResult { + fn node_find_tree_at( + &self, + x: i32, + y: i32, + tree: &mut Vec, + usecase: FindTreeUsecase, + ) -> FindTreeResult { + if usecase == FindTreeUsecase::SelectToplevel { + return FindTreeResult::AcceptsInput; + } self.xdg.find_tree_at(x, y, tree) } fn node_render(&self, renderer: &mut Renderer, x: i32, y: i32, bounds: Option<&Rect>) { - renderer.render_xdg_surface(&self.xdg, x, y, bounds) + renderer.render_xdg_toplevel(self, x, y, bounds) } fn node_client(&self) -> Option> { @@ -516,6 +526,10 @@ impl Node for XdgToplevel { // log::info!("xdg-toplevel focus"); seat.set_known_cursor(KnownCursor::Default); } + + fn node_into_toplevel(self: Rc) -> Option> { + Some(self) + } } impl ToplevelNodeBase for XdgToplevel { @@ -619,6 +633,10 @@ impl ToplevelNodeBase for XdgToplevel { fn tl_restack_popups(&self) { self.xdg.restack_popups(); } + + fn tl_admits_children(&self) -> bool { + false + } } impl XdgSurfaceExt for XdgToplevel { diff --git a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs index 17d0aa4c..ed231aa9 100644 --- a/src/ifs/wl_surface/zwlr_layer_surface_v1.rs +++ b/src/ifs/wl_surface/zwlr_layer_surface_v1.rs @@ -10,7 +10,7 @@ use { object::Object, rect::Rect, renderer::Renderer, - tree::{FindTreeResult, FoundNode, Node, NodeId, NodeVisitor, OutputNode}, + tree::{FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeVisitor, OutputNode}, utils::{ bitflags::BitflagsExt, cell_ext::CellExt, linkedlist::LinkedNode, numcell::NumCell, option_ext::OptionExt, @@ -424,7 +424,13 @@ impl Node for ZwlrLayerSurfaceV1 { self.pos.get() } - fn node_find_tree_at(&self, x: i32, y: i32, tree: &mut Vec) -> FindTreeResult { + fn node_find_tree_at( + &self, + x: i32, + y: i32, + tree: &mut Vec, + _usecase: FindTreeUsecase, + ) -> FindTreeResult { self.surface.find_tree_at_(x, y, tree) } diff --git a/src/it/test_ifs/test_jay_compositor.rs b/src/it/test_ifs/test_jay_compositor.rs index bd0570db..53887664 100644 --- a/src/it/test_ifs/test_jay_compositor.rs +++ b/src/it/test_ifs/test_jay_compositor.rs @@ -74,12 +74,24 @@ impl TestJayCompositor { self.tran.client_id.set(ClientId::from_raw(ev.client_id)); Ok(()) } + + fn handle_seat(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let _ev = Seat::parse_full(parser)?; + Ok(()) + } + + fn handle_capabilities(&self, parser: MsgParser<'_, '_>) -> Result<(), TestError> { + let _ev = Capabilities::parse_full(parser)?; + Ok(()) + } } test_object! { TestJayCompositor, JayCompositor; CLIENT_ID => handle_client_id, + SEAT => handle_seat, + CAPABILITIES => handle_capabilities, } impl TestObject for TestJayCompositor {} diff --git a/src/it/tests.rs b/src/it/tests.rs index acc40f31..c32fe489 100644 --- a/src/it/tests.rs +++ b/src/it/tests.rs @@ -73,6 +73,7 @@ mod t0038_subsurface_parent_state; mod t0039_alpha_modifier; mod t0040_virtual_keyboard; mod t0041_input_method; +mod t0042_toplevel_select; pub trait TestCase: Sync { fn name(&self) -> &'static str; @@ -133,5 +134,6 @@ pub fn tests() -> Vec<&'static dyn TestCase> { t0039_alpha_modifier, t0040_virtual_keyboard, t0041_input_method, + t0042_toplevel_select, } } diff --git a/src/it/tests/t0042_toplevel_select.rs b/src/it/tests/t0042_toplevel_select.rs new file mode 100644 index 00000000..332e60e4 --- /dev/null +++ b/src/it/tests/t0042_toplevel_select.rs @@ -0,0 +1,68 @@ +use { + crate::{ + ifs::wl_seat::{ToplevelSelector, BTN_LEFT}, + it::{test_error::TestResult, testrun::TestRun}, + tree::{Node, ToplevelNode}, + utils::clonecell::CloneCell, + }, + std::rc::Rc, +}; + +testcase!(); + +async fn test(run: Rc) -> TestResult { + let ds = run.create_default_setup().await?; + + let client = run.create_client().await?; + let win1 = client.create_window().await?; + win1.map2().await?; + let win2 = client.create_window().await?; + win2.map2().await?; + client.sync().await; + + let win1pos = win1.tl.server.node_absolute_position().position(); + let win2pos = win2.tl.server.node_absolute_position().position(); + ds.mouse.abs( + &ds.connector, + win1pos.0 as f64 + 2.0, + win1pos.1 as f64 + 2.0, + ); + run.sync().await; + + struct Selector(CloneCell>>); + impl ToplevelSelector for Rc { + fn set(&self, toplevel: Rc) { + self.0.set(Some(toplevel)); + } + } + let selector = Rc::new(Selector(Default::default())); + ds.seat.select_toplevel(selector.clone()); + + client.compare_screenshot("1", false).await?; + + ds.mouse.abs( + &ds.connector, + win2pos.0 as f64 + 2.0, + win2pos.1 as f64 + 2.0, + ); + run.sync().await; + + client.compare_screenshot("2", false).await?; + + ds.kb.press(1); + run.sync().await; + tassert!(selector.0.get().is_none()); + + ds.seat.select_toplevel(selector.clone()); + + client.compare_screenshot("3", false).await?; + + ds.mouse.click(BTN_LEFT); + + client.compare_screenshot("4", false).await?; + + let tl = selector.0.get().expect("no toplevel selected"); + tassert_eq!(tl.node_id(), win2.tl.server.node_id); + + Ok(()) +} diff --git a/src/it/tests/t0042_toplevel_select/screenshot_1.qoi b/src/it/tests/t0042_toplevel_select/screenshot_1.qoi new file mode 100644 index 00000000..05d1a952 Binary files /dev/null and b/src/it/tests/t0042_toplevel_select/screenshot_1.qoi differ diff --git a/src/it/tests/t0042_toplevel_select/screenshot_2.qoi b/src/it/tests/t0042_toplevel_select/screenshot_2.qoi new file mode 100644 index 00000000..61f8ac62 Binary files /dev/null and b/src/it/tests/t0042_toplevel_select/screenshot_2.qoi differ diff --git a/src/it/tests/t0042_toplevel_select/screenshot_3.qoi b/src/it/tests/t0042_toplevel_select/screenshot_3.qoi new file mode 100644 index 00000000..61f8ac62 Binary files /dev/null and b/src/it/tests/t0042_toplevel_select/screenshot_3.qoi differ diff --git a/src/it/tests/t0042_toplevel_select/screenshot_4.qoi b/src/it/tests/t0042_toplevel_select/screenshot_4.qoi new file mode 100644 index 00000000..714222f1 Binary files /dev/null and b/src/it/tests/t0042_toplevel_select/screenshot_4.qoi differ diff --git a/src/logger.rs b/src/logger.rs index 373e84d3..6e7b489e 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -76,12 +76,13 @@ impl Logger { self.path.lock().clone() } - pub fn redirect(&self, ty: &str) { + pub fn redirect(&self, ty: &str) -> Ustring { let (file, fd) = open_log_file(ty); log::info!("Redirecting logs to {}", file.display()); *self.path.lock() = Arc::new(file.as_bytes().into()); self.file_fd.store(fd.raw(), Relaxed); *self._file.lock() = fd; + file } pub fn write_raw(&self, buf: &[u8]) { diff --git a/src/pipewire/pw_con.rs b/src/pipewire/pw_con.rs index 8e71b325..6fe93ed5 100644 --- a/src/pipewire/pw_con.rs +++ b/src/pipewire/pw_con.rs @@ -189,7 +189,7 @@ impl PwCon { ); if log::log_enabled!(log::Level::Trace) { log::trace!("CALL {}@{}: `{:?}`:", interface, id, opcode); - let mut parser = PwParser::new(&buf[16..], &fds); + let mut parser = PwParser::new(&buf[16..buf.len()], &fds); while parser.len() > 0 { log::trace!("{:#?}", parser.read_pod().unwrap()); } diff --git a/src/pipewire/pw_pod.rs b/src/pipewire/pw_pod.rs index babfc843..9f63a548 100644 --- a/src/pipewire/pw_pod.rs +++ b/src/pipewire/pw_pod.rs @@ -5,7 +5,10 @@ mod pw_debug; use { crate::pipewire::pw_parser::{PwParser, PwParserError}, bstr::BStr, - std::fmt::{Debug, Formatter}, + std::{ + fmt::{Debug, Formatter}, + sync::atomic::AtomicU32, + }, uapi::{c, Pod}, }; @@ -1425,10 +1428,10 @@ bitflags! { } #[repr(C)] -#[derive(Debug, Copy, Clone)] +#[derive(Debug)] pub struct spa_io_buffers { - pub status: SpaStatus, - pub buffer_id: u32, + pub status: AtomicU32, + pub buffer_id: AtomicU32, } unsafe impl Pod for spa_io_buffers {} diff --git a/src/portal.rs b/src/portal.rs index e196c9fb..0caacc7f 100644 --- a/src/portal.rs +++ b/src/portal.rs @@ -38,11 +38,13 @@ use { }, log::Level, std::{ + os::unix::process::CommandExt, + process::Command, rc::{Rc, Weak}, sync::Arc, }, thiserror::Error, - uapi::{c, OwnedFd}, + uapi::{c, getpid, OwnedFd}, }; const PORTAL_SUCCESS: u32 = 0; @@ -53,7 +55,7 @@ const PORTAL_ENDED: u32 = 2; pub fn run_freestanding(global: GlobalArgs) { let logger = Logger::install_stderr(global.log_level.into()); - run(logger); + run(logger, true); } #[derive(Debug, Error)] @@ -137,13 +139,13 @@ pub fn run_from_compositor(level: Level) -> Result { Forked::Child { .. } => { drop(read); let logger = Logger::install_pipe(write, level); - run(logger); + run(logger, false); std::process::exit(0); } } } -fn run(logger: Arc) { +fn run(logger: Arc, freestanding: bool) { let eng = AsyncEngine::new(); let ring = match IoUring::new(&eng, 32) { Ok(r) => r, @@ -151,16 +153,21 @@ fn run(logger: Arc) { fatal!("Could not create an IO-uring: {}", ErrorFmt(e)); } }; - let _f = eng.spawn(run_async(eng.clone(), ring.clone(), logger)); + let _f = eng.spawn(run_async(eng.clone(), ring.clone(), logger, freestanding)); if let Err(e) = ring.run() { fatal!("The IO-uring returned an error: {}", ErrorFmt(e)); } } -async fn run_async(eng: Rc, ring: Rc, logger: Arc) { +async fn run_async( + eng: Rc, + ring: Rc, + logger: Arc, + freestanding: bool, +) { let (_rtl_future, rtl) = RunToplevel::install(&eng); let dbus = Dbus::new(&eng, &ring, &rtl); - let dbus = init_dbus_session(&dbus, logger).await; + let dbus = init_dbus_session(&dbus, logger, freestanding).await; let xrd = match xrd() { Some(xrd) => xrd, _ => { @@ -206,7 +213,7 @@ async fn run_async(eng: Rc, ring: Rc, logger: Arc) const UNIQUE_NAME: &str = "org.freedesktop.impl.portal.desktop.jay"; -async fn init_dbus_session(dbus: &Dbus, logger: Arc) -> Rc { +async fn init_dbus_session(dbus: &Dbus, logger: Arc, freestanding: bool) -> Rc { let session = match dbus.session().await { Ok(s) => s, Err(e) => { @@ -226,18 +233,31 @@ async fn init_dbus_session(dbus: &Dbus, logger: Arc) -> Rc { match rv { Ok(r) if r.get().rv == DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER => { log::info!("Acquired unique name {}", UNIQUE_NAME); - logger.redirect("portal"); + let log_file = logger.redirect("portal"); log::info!("version = {VERSION}"); let fork = match fork_with_pidfd(false) { Ok(f) => f, Err(e) => fatal!("Could not fork: {}", ErrorFmt(e)), }; match fork { - Forked::Parent { .. } => std::process::exit(0), + Forked::Parent { .. } => { + if freestanding { + let e = Command::new("tail") + .arg("-f") + .arg("-n") + .arg("+1") + .arg(&log_file) + .exec(); + eprintln!("Could not exec `tail`: {}", ErrorFmt(e)); + std::process::exit(1); + } + std::process::exit(0) + } Forked::Child { .. } => { if let Err(e) = uapi::setsid() { log::error!("setsid failed: {}", ErrorFmt(OsError::from(e))); } + log::info!("pid = {}", getpid()); } } set_process_name("jay portal"); 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 e1aa2b5c..cfc730f3 100644 --- a/src/portal/ptl_screencast.rs +++ b/src/portal/ptl_screencast.rs @@ -34,13 +34,18 @@ 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, cell::{Cell, RefCell}, ops::Deref, rc::Rc, + sync::atomic::Ordering::{Acquire, Relaxed, Release}, }, }; @@ -58,6 +63,7 @@ pub enum ScreencastPhase { Init, SourcesSelected, Selecting(Rc), + SelectingWindow(Rc), Starting(Rc), Started(Rc), Terminated, @@ -65,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 { @@ -79,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 { @@ -87,6 +108,7 @@ pub struct StartedScreencast { node: Rc, port: Rc, buffers: RefCell>, + buffers_valid: Cell, dpy: Rc, jay_screencast: Rc, } @@ -133,15 +155,22 @@ 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(), port, buffers: Default::default(), + buffers_valid: Cell::new(false), dpy: self.dpy.clone(), jay_screencast: jsc, }); @@ -161,6 +190,7 @@ impl PwClientNodeOwner for StartedScreencast { fn use_buffers(&self, port: &Rc) { self.node .send_port_output_buffers(port, &self.buffers.borrow_mut()); + self.buffers_valid.set(true); } fn start(self: Rc) { @@ -179,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); @@ -188,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()); @@ -266,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), }))); } } @@ -303,23 +367,30 @@ impl UsrJayScreencastOwner for StartedScreencast { self.node.send_port_update(&self.port, true); self.node.send_active(true); *self.buffers.borrow_mut() = buffers; + self.buffers_valid.set(false); } fn ready(&self, ev: &Ready) { let idx = ev.idx as usize; + if !self.buffers_valid.get() { + self.jay_screencast.release_buffer(idx); + return; + } unsafe { let mut used = false; if let Some(io) = self.port.io_buffers.lock().values().next() { let io = io.write(); - if io.status != SPA_STATUS_HAVE_DATA { + let status = io.status.load(Acquire); + if status != SPA_STATUS_HAVE_DATA.0 { used = true; - if io.buffer_id != ev.idx { - if (io.buffer_id as usize) < self.buffers.borrow_mut().len() { - self.jay_screencast.release_buffer(io.buffer_id as usize); + let buffer_id = io.buffer_id.load(Relaxed); + if buffer_id != ev.idx { + if (buffer_id as usize) < self.buffers.borrow_mut().len() { + self.jay_screencast.release_buffer(buffer_id as usize); } } - io.buffer_id = ev.idx; - io.status = SPA_STATUS_HAVE_DATA; + io.buffer_id.store(ev.idx, Relaxed); + io.status.store(SPA_STATUS_HAVE_DATA.0, Release); } } if !used { 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