From 1d3dfa8b3a611b96fae4ae42718854e91a7bba6c Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 16 Dec 2025 20:21:10 +0100 Subject: [PATCH] xdg-popup: implement jay-popup-ext-v1 --- docs/features.md | 1 + src/globals.rs | 2 + src/ifs.rs | 1 + src/ifs/jay_popup_ext_manager_v1.rs | 107 ++++++++++ src/ifs/wl_seat.rs | 16 +- src/ifs/wl_seat/event_handling.rs | 8 + src/ifs/wl_seat/pointer_owner.rs | 194 +++++++++++++++++- src/ifs/wl_surface/xdg_surface/xdg_popup.rs | 14 +- .../xdg_surface/xdg_popup/jay_popup_ext_v1.rs | 101 +++++++++ .../wl_surface/xdg_surface/xdg_toplevel.rs | 17 ++ wire/jay_popup_ext_manager_v1.txt | 7 + wire/jay_popup_ext_v1.txt | 13 ++ 12 files changed, 473 insertions(+), 8 deletions(-) create mode 100644 src/ifs/jay_popup_ext_manager_v1.rs create mode 100644 src/ifs/wl_surface/xdg_surface/xdg_popup/jay_popup_ext_v1.rs create mode 100644 wire/jay_popup_ext_manager_v1.txt create mode 100644 wire/jay_popup_ext_v1.txt diff --git a/docs/features.md b/docs/features.md index 93e1f21f..a4fe2966 100644 --- a/docs/features.md +++ b/docs/features.md @@ -163,6 +163,7 @@ Jay supports the following wayland protocols: | ext_session_lock_manager_v1 | 1 | Yes | | ext_transient_seat_manager_v1 | 1[^ts_rejected] | Yes | | ext_workspace_manager_v1 | 1 | Yes | +| jay_popup_ext_manager_v1 | 1 | | | jay_tray_v1 | 1 | | | org_kde_kwin_server_decoration_manager | 1 | | | wl_compositor | 6 | | diff --git a/src/globals.rs b/src/globals.rs index 59ca1a3c..8741895c 100644 --- a/src/globals.rs +++ b/src/globals.rs @@ -21,6 +21,7 @@ use { }, jay_compositor::JayCompositorGlobal, jay_damage_tracking::JayDamageTrackingGlobal, + jay_popup_ext_manager_v1::JayPopupExtManagerV1Global, org_kde_kwin_server_decoration_manager::OrgKdeKwinServerDecorationManagerGlobal, wl_compositor::WlCompositorGlobal, wl_fixes::WlFixesGlobal, @@ -231,6 +232,7 @@ impl Globals { add_singleton!(XdgToplevelTagManagerV1Global); add_singleton!(JayHeadManagerV1Global); add_singleton!(WpPointerWarpV1Global); + add_singleton!(JayPopupExtManagerV1Global); } pub fn add_backend_singletons(&self, backend: &Rc) { diff --git a/src/ifs.rs b/src/ifs.rs index 74b2fca6..5d142c48 100644 --- a/src/ifs.rs +++ b/src/ifs.rs @@ -22,6 +22,7 @@ pub mod jay_input; pub mod jay_log_file; pub mod jay_output; pub mod jay_pointer; +pub mod jay_popup_ext_manager_v1; pub mod jay_randr; pub mod jay_reexec; pub mod jay_render_ctx; diff --git a/src/ifs/jay_popup_ext_manager_v1.rs b/src/ifs/jay_popup_ext_manager_v1.rs new file mode 100644 index 00000000..9bcce725 --- /dev/null +++ b/src/ifs/jay_popup_ext_manager_v1.rs @@ -0,0 +1,107 @@ +use { + crate::{ + client::{Client, ClientError}, + globals::{Global, GlobalName}, + ifs::wl_surface::xdg_surface::xdg_popup::jay_popup_ext_v1::{ + JayPopupExtV1, JayPopupExtV1Error, + }, + leaks::Tracker, + object::{Object, Version}, + wire::{JayPopupExtManagerV1Id, jay_popup_ext_manager_v1::*}, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct JayPopupExtManagerV1Global { + pub name: GlobalName, +} + +pub struct JayPopupExtManagerV1 { + pub id: JayPopupExtManagerV1Id, + pub client: Rc, + pub tracker: Tracker, + pub version: Version, +} + +impl JayPopupExtManagerV1Global { + pub fn new(name: GlobalName) -> Self { + Self { name } + } + + fn bind_( + self: Rc, + id: JayPopupExtManagerV1Id, + client: &Rc, + version: Version, + ) -> Result<(), JayPopupExtManagerV1Error> { + let obj = Rc::new(JayPopupExtManagerV1 { + id, + client: client.clone(), + tracker: Default::default(), + version, + }); + track!(client, obj); + client.add_client_obj(&obj)?; + Ok(()) + } +} + +global_base!( + JayPopupExtManagerV1Global, + JayPopupExtManagerV1, + JayPopupExtManagerV1Error +); + +impl Global for JayPopupExtManagerV1Global { + fn singleton(&self) -> bool { + true + } + + fn version(&self) -> u32 { + 1 + } +} + +simple_add_global!(JayPopupExtManagerV1Global); + +impl JayPopupExtManagerV1RequestHandler for JayPopupExtManagerV1 { + type Error = JayPopupExtManagerV1Error; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.client.remove_obj(self)?; + Ok(()) + } + + fn get_ext(&self, req: GetExt, _slf: &Rc) -> Result<(), Self::Error> { + let popup = self.client.lookup(req.popup)?; + let obj = Rc::new(JayPopupExtV1::new( + req.id, + &self.client, + self.version, + &popup, + )); + track!(self.client, obj); + self.client.add_client_obj(&obj)?; + obj.install()?; + Ok(()) + } +} + +object_base! { + self = JayPopupExtManagerV1; + version = self.version; +} + +impl Object for JayPopupExtManagerV1 {} + +simple_add_obj!(JayPopupExtManagerV1); + +#[derive(Debug, Error)] +pub enum JayPopupExtManagerV1Error { + #[error(transparent)] + ClientError(Box), + #[error(transparent)] + JayPopupExtV1Error(#[from] JayPopupExtV1Error), +} +efrom!(JayPopupExtManagerV1Error, ClientError); diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index c0c06717..b746e3dc 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -69,7 +69,7 @@ use { WlSurface, dnd_icon::DndIcon, tray::{DynTrayItem, TrayItemId}, - xdg_surface::xdg_popup::XdgPopup, + xdg_surface::{xdg_popup::XdgPopup, xdg_toplevel::ResizeEdges}, zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, }, xdg_toplevel_drag_v1::XdgToplevelDragV1, @@ -1146,6 +1146,20 @@ impl WlSeatGlobal { } } + pub fn start_popup_move(self: &Rc, popup: &Rc, serial: u64) { + self.pointer_owner.start_popup_move(self, popup, serial); + } + + pub fn start_popup_resize( + self: &Rc, + popup: &Rc, + edges: ResizeEdges, + serial: u64, + ) { + self.pointer_owner + .start_popup_resize(self, popup, edges, serial); + } + pub fn cancel_popup_move(self: &Rc) { self.pointer_owner.grab_node_removed(self); } diff --git a/src/ifs/wl_seat/event_handling.rs b/src/ifs/wl_seat/event_handling.rs index 04eea58c..6eda45e5 100644 --- a/src/ifs/wl_seat/event_handling.rs +++ b/src/ifs/wl_seat/event_handling.rs @@ -103,6 +103,14 @@ impl NodeSeatState { self.pointer_foci.remove(&seat.id); } + pub fn pointer_inside(&self, seat: &WlSeatGlobal) -> bool { + self.pointer_foci.contains(&seat.id) + } + + pub fn pointer_not_inside(&self, seat: &WlSeatGlobal) -> bool { + !self.pointer_inside(seat) + } + pub fn disable_focus_history(&self) { self.no_focus_history.set(true); } diff --git a/src/ifs/wl_seat/pointer_owner.rs b/src/ifs/wl_seat/pointer_owner.rs index 07989d87..5cd3c800 100644 --- a/src/ifs/wl_seat/pointer_owner.rs +++ b/src/ifs/wl_seat/pointer_owner.rs @@ -222,6 +222,22 @@ impl PointerOwnerHolder { pub fn start_workspace_drag(&self, seat: &Rc, ws: &Rc) { self.owner.get().start_workspace_drag(seat, ws); } + + pub fn start_popup_move(&self, seat: &Rc, popup: &Rc, serial: u64) { + self.owner.get().start_popup_move(seat, popup, serial); + } + + pub fn start_popup_resize( + &self, + seat: &Rc, + popup: &Rc, + edges: ResizeEdges, + serial: u64, + ) { + self.owner + .get() + .start_popup_resize(seat, popup, edges, serial); + } } trait PointerOwner { @@ -281,6 +297,25 @@ trait PointerOwner { let _ = seat; let _ = ws; } + + fn start_popup_move(&self, seat: &Rc, popup: &Rc, serial: u64) { + let _ = seat; + let _ = popup; + let _ = serial; + } + + fn start_popup_resize( + &self, + seat: &Rc, + popup: &Rc, + edges: ResizeEdges, + serial: u64, + ) { + let _ = seat; + let _ = popup; + let _ = edges; + let _ = serial; + } } struct SimplePointerOwner { @@ -289,7 +324,7 @@ struct SimplePointerOwner { struct SimpleGrabPointerOwner { usecase: T, - buttons: SmallMap, + buttons: SmallMap, node: Rc, serial: u64, } @@ -338,7 +373,7 @@ impl PointerOwner for SimplePointerOwner { .owner .set(Rc::new(SimpleGrabPointerOwner { usecase: self.usecase.clone(), - buttons: SmallMap::new_with(button, ()), + buttons: SmallMap::new_with(button, serial), node: pn.clone(), serial, })); @@ -439,8 +474,20 @@ impl PointerOwner for SimplePointerOwner { } } +impl SimpleGrabPointerOwner { + fn find_button(&self, serial: u64) -> Option { + for (button, s) in self.buttons.iter() { + if s == serial { + return Some(button); + } + } + None + } +} + impl PointerOwner for SimpleGrabPointerOwner { fn button(&self, seat: &Rc, time_usec: u64, button: u32, state: ButtonState) { + let serial = seat.state.next_serial(self.node.node_client().as_deref()); match state { ButtonState::Released => { if self.buttons.remove(&button).is_none() { @@ -454,12 +501,11 @@ impl PointerOwner for SimpleGrabPointerOwner { } } ButtonState::Pressed => { - if self.buttons.insert(button, ()).is_some() { + if self.buttons.insert(button, serial).is_some() { return; } } } - let serial = seat.state.next_serial(self.node.node_client().as_deref()); seat.handle_node_button(self.node.clone(), time_usec, button, state, serial); } @@ -512,6 +558,27 @@ impl PointerOwner for SimpleGrabPointerOwner { fn start_workspace_drag(&self, seat: &Rc, ws: &Rc) { self.usecase.start_workspace_drag(self, seat, ws); } + + fn start_popup_move(&self, seat: &Rc, popup: &Rc, serial: u64) { + let Some(button) = self.find_button(serial) else { + return; + }; + self.usecase.start_popup_move(self, seat, popup, button); + } + + fn start_popup_resize( + &self, + seat: &Rc, + popup: &Rc, + edges: ResizeEdges, + serial: u64, + ) { + let Some(button) = self.find_button(serial) else { + return; + }; + self.usecase + .start_popup_resize(self, seat, popup, edges, button); + } } impl PointerOwner for DndPointerOwner { @@ -680,6 +747,34 @@ trait SimplePointerOwnerUsecase: Sized + Clone + 'static { let _ = seat; let _ = ws; } + + fn start_popup_move( + &self, + grab: &SimpleGrabPointerOwner, + seat: &Rc, + popup: &Rc, + button: u32, + ) { + let _ = grab; + let _ = seat; + let _ = popup; + let _ = button; + } + + fn start_popup_resize( + &self, + grab: &SimpleGrabPointerOwner, + seat: &Rc, + popup: &Rc, + edges: ResizeEdges, + button: u32, + ) { + let _ = grab; + let _ = seat; + let _ = popup; + let _ = edges; + let _ = button; + } } impl DefaultPointerUsecase { @@ -706,6 +801,25 @@ impl DefaultPointerUsecase { seat.pointer_owner.owner.set(pointer_owner.clone()); pointer_owner.apply_changes(seat); } + + fn start_popup_usecase( + &self, + grab: &SimpleGrabPointerOwner, + seat: &Rc, + popup: &Rc, + button: u32, + usecase: U, + ) { + self.prepare_new_usecase(grab, seat); + seat.pointer_owner.owner.set(Rc::new(PopupPointerOwner { + popup: popup.clone(), + window_management: false, + button, + usecase, + })); + popup.node_seat_state().add_pointer_grab(seat); + popup.add_interactive_move(seat); + } } impl SimplePointerOwnerUsecase for DefaultPointerUsecase { @@ -812,6 +926,65 @@ impl SimplePointerOwnerUsecase for DefaultPointerUsecase { }, ); } + + fn start_popup_move( + &self, + grab: &SimpleGrabPointerOwner, + seat: &Rc, + popup: &Rc, + button: u32, + ) { + let (x, y) = seat.pointer_cursor.position(); + let (dx, dy) = popup + .node_absolute_position() + .translate(x.round_down(), y.round_down()); + self.start_popup_usecase( + grab, + seat, + popup, + button, + PopupPointerOwnerMoveUsecase { dx, dy }, + ); + seat.pointer_cursor.set_known(KnownCursor::Move); + } + + fn start_popup_resize( + &self, + grab: &SimpleGrabPointerOwner, + seat: &Rc, + popup: &Rc, + edges: ResizeEdges, + button: u32, + ) { + let cursor = match (edges.top, edges.left, edges.right, edges.bottom) { + (true, false, false, false) => KnownCursor::NsResize, + (false, false, false, true) => KnownCursor::NsResize, + (false, true, false, false) => KnownCursor::EwResize, + (false, false, true, false) => KnownCursor::EwResize, + (true, true, false, false) => KnownCursor::NwseResize, + (false, false, true, true) => KnownCursor::NwseResize, + (false, true, false, true) => KnownCursor::NeswResize, + (true, false, true, false) => KnownCursor::NeswResize, + _ => return, + }; + let (x, y) = seat.pointer_cursor.position(); + let pos = popup.node_absolute_position(); + let (mut dx, mut dy) = pos.translate(x.round_down(), y.round_down()); + if edges.right { + dx = pos.width() - dx; + } + if edges.bottom { + dy = pos.height() - dy; + } + self.start_popup_usecase( + grab, + seat, + popup, + button, + PopupPointerOwnerResizeUsecase { edges, dx, dy }, + ); + seat.pointer_cursor.set_known(cursor); + } } trait NodeSelectorUsecase: Sized + 'static { @@ -989,6 +1162,7 @@ impl SimplePointerOwnerUsecase for WindowManagementUsecase { seat.pointer_cursor.set_known(KnownCursor::Move); Rc::new(PopupPointerOwner { popup, + window_management: true, button, usecase: PopupPointerOwnerMoveUsecase { dx, dy }, }) @@ -1037,6 +1211,7 @@ impl SimplePointerOwnerUsecase for WindowManagementUsecase { seat.pointer_cursor.set_known(cursor); Rc::new(PopupPointerOwner { popup, + window_management: true, button, usecase: PopupPointerOwnerResizeUsecase { edges: ResizeEdges { @@ -1495,6 +1670,7 @@ impl UiDragUsecase for WorkspaceDragUsecase { struct PopupPointerOwner { popup: Rc, + window_management: bool, button: u32, usecase: T, } @@ -1519,7 +1695,11 @@ where } fn revert_to_previous(&self, seat: &Rc) { - self.revert_to_window_management(seat); + if self.window_management { + self.revert_to_window_management(seat); + } else { + self.revert_to_default(seat); + } } } @@ -1553,7 +1733,9 @@ where } fn disable_window_management(&self, seat: &Rc) { - self.revert_to_default(seat); + if self.window_management { + self.revert_to_default(seat); + } } } diff --git a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs index fa6cb961..0710a5ad 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs @@ -1,3 +1,5 @@ +pub mod jay_popup_ext_v1; + use { crate::{ client::{Client, ClientError}, @@ -7,7 +9,9 @@ use { wl_seat::{NodeSeatState, SeatId, WlSeatGlobal, tablet::TabletTool}, wl_surface::{ tray::TrayItemId, - xdg_surface::{XdgSurface, XdgSurfaceExt}, + xdg_surface::{ + XdgSurface, XdgSurfaceExt, xdg_popup::jay_popup_ext_v1::JayPopupExtV1, + }, }, xdg_positioner::{ CA_FLIP_X, CA_FLIP_Y, CA_RESIZE_X, CA_RESIZE_Y, CA_SLIDE_X, CA_SLIDE_Y, @@ -65,6 +69,7 @@ pub struct XdgPopup { pub tracker: Tracker, seat_state: NodeSeatState, set_visible_prepared: Cell, + jay_popup_ext: CloneCell>>, interactive_moves: SmallMap, 1>, } @@ -94,6 +99,7 @@ impl XdgPopup { tracker: Default::default(), seat_state: Default::default(), set_visible_prepared: Cell::new(false), + jay_popup_ext: Default::default(), interactive_moves: Default::default(), }) } @@ -267,6 +273,9 @@ impl XdgPopupRequestHandler for XdgPopup { type Error = XdgPopupError; fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + if self.jay_popup_ext.is_some() { + return Err(XdgPopupError::HasJayPopupExt); + } self.destroy_node(); self.xdg.unset_ext(); self.xdg.surface.client.remove_obj(self)?; @@ -328,6 +337,7 @@ object_base! { impl Object for XdgPopup { fn break_loops(&self) { + self.jay_popup_ext.take(); self.destroy_node(); } } @@ -518,5 +528,7 @@ pub enum XdgPopupError { Incomplete, #[error(transparent)] ClientError(Box), + #[error("The popup still has a jay_popup_ext_v1 extension object")] + HasJayPopupExt, } efrom!(XdgPopupError, ClientError); diff --git a/src/ifs/wl_surface/xdg_surface/xdg_popup/jay_popup_ext_v1.rs b/src/ifs/wl_surface/xdg_surface/xdg_popup/jay_popup_ext_v1.rs new file mode 100644 index 00000000..aeb847ce --- /dev/null +++ b/src/ifs/wl_surface/xdg_surface/xdg_popup/jay_popup_ext_v1.rs @@ -0,0 +1,101 @@ +use { + crate::{ + client::{Client, ClientError}, + ifs::wl_surface::xdg_surface::{xdg_popup::XdgPopup, xdg_toplevel::map_resize_edges}, + leaks::Tracker, + object::{Object, Version}, + wire::{JayPopupExtV1Id, jay_popup_ext_v1::*}, + }, + std::rc::Rc, + thiserror::Error, +}; + +pub struct JayPopupExtV1 { + id: JayPopupExtV1Id, + client: Rc, + pub tracker: Tracker, + version: Version, + popup: Rc, +} + +impl JayPopupExtV1 { + pub fn new( + id: JayPopupExtV1Id, + client: &Rc, + version: Version, + popup: &Rc, + ) -> Self { + Self { + id, + tracker: Default::default(), + version, + client: client.clone(), + popup: popup.clone(), + } + } + + pub fn install(self: &Rc) -> Result<(), JayPopupExtV1Error> { + if self.popup.jay_popup_ext.is_some() { + return Err(JayPopupExtV1Error::HasExt); + } + self.popup.jay_popup_ext.set(Some(self.clone())); + Ok(()) + } +} + +impl JayPopupExtV1RequestHandler for JayPopupExtV1 { + type Error = JayPopupExtV1Error; + + fn destroy(&self, _req: Destroy, _slf: &Rc) -> Result<(), Self::Error> { + self.popup.jay_popup_ext.take(); + self.client.remove_obj(self)?; + Ok(()) + } + + fn move_(&self, req: Move, _slf: &Rc) -> Result<(), Self::Error> { + let seat = self.client.lookup(req.seat)?; + let Some(serial) = self.client.map_serial(req.serial) else { + return Ok(()); + }; + if self.popup.seat_state.pointer_not_inside(&seat.global) { + return Ok(()); + } + seat.global.start_popup_move(&self.popup, serial); + Ok(()) + } + + fn resize(&self, req: Resize, _slf: &Rc) -> Result<(), Self::Error> { + let Some(edges) = map_resize_edges(req.edges) else { + return Err(JayPopupExtV1Error::UnknownResizeEdges(req.edges)); + }; + let seat = self.client.lookup(req.seat)?; + let Some(serial) = self.client.map_serial(req.serial) else { + return Ok(()); + }; + if self.popup.seat_state.pointer_not_inside(&seat.global) { + return Ok(()); + } + seat.global.start_popup_resize(&self.popup, edges, serial); + Ok(()) + } +} + +object_base! { + self = JayPopupExtV1; + version = self.version; +} + +impl Object for JayPopupExtV1 {} + +simple_add_obj!(JayPopupExtV1); + +#[derive(Debug, Error)] +pub enum JayPopupExtV1Error { + #[error(transparent)] + ClientError(Box), + #[error("The xdg_popup already has a jay_popup_ext_v1 extension")] + HasExt, + #[error("The resize edge {0} is unknown")] + UnknownResizeEdges(u32), +} +efrom!(JayPopupExtV1Error, ClientError); diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index 4ecbfe81..8a2808e7 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -79,6 +79,11 @@ pub const SUSPENDED_SINCE: Version = Version(6); pub const TILED_SINCE: Version = Version(2); pub const CONSTRAINTS_SINCE: Version = Version(7); +const RESIZE_EDGE_TOP: u32 = 1; +const RESIZE_EDGE_BOTTOM: u32 = 2; +const RESIZE_EDGE_LEFT: u32 = 4; +const RESIZE_EDGE_RIGHT: u32 = 8; + #[derive(Copy, Clone, Eq, PartialEq, Debug)] pub enum Decoration { #[expect(dead_code)] @@ -834,6 +839,18 @@ pub enum XdgToplevelError { } efrom!(XdgToplevelError, ClientError); +pub fn map_resize_edges(edge: u32) -> Option { + if !matches!(edge, 0 | 1 | 2 | 4 | 5 | 6 | 8 | 9 | 10) { + return None; + } + Some(ResizeEdges { + top: edge.contains(RESIZE_EDGE_TOP), + left: edge.contains(RESIZE_EDGE_LEFT), + right: edge.contains(RESIZE_EDGE_RIGHT), + bottom: edge.contains(RESIZE_EDGE_BOTTOM), + }) +} + #[derive(Copy, Clone, Debug, Eq, PartialEq)] pub struct ResizeEdges { pub top: bool, diff --git a/wire/jay_popup_ext_manager_v1.txt b/wire/jay_popup_ext_manager_v1.txt new file mode 100644 index 00000000..8cbd7857 --- /dev/null +++ b/wire/jay_popup_ext_manager_v1.txt @@ -0,0 +1,7 @@ +request destroy (destructor) { +} + +request get_ext { + id: id(jay_popup_ext_v1) (new), + popup: id(xdg_popup), +} diff --git a/wire/jay_popup_ext_v1.txt b/wire/jay_popup_ext_v1.txt new file mode 100644 index 00000000..cdd8d205 --- /dev/null +++ b/wire/jay_popup_ext_v1.txt @@ -0,0 +1,13 @@ +request destroy (destructor) { +} + +request move { + seat: id(wl_seat), + serial: u32, +} + +request resize { + seat: id(wl_seat), + serial: u32, + edges: u32, +}