From 8d5ac9a2b71fac4e2587a2beafcff05cf5be60bd Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 16 Dec 2025 20:39:21 +0100 Subject: [PATCH 1/3] tree: make FindTreeUsecase handling more explicit --- src/ifs/wl_surface/x_surface/xwindow.rs | 6 +++-- src/ifs/wl_surface/xdg_surface/xdg_popup.rs | 6 +++-- .../wl_surface/xdg_surface/xdg_toplevel.rs | 6 +++-- src/tree.rs | 2 +- src/tree/output.rs | 22 ++++++++++++++----- 5 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index d4d44c0d..f156323e 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -396,8 +396,10 @@ impl Node for Xwindow { tree: &mut Vec, usecase: FindTreeUsecase, ) -> FindTreeResult { - if usecase == FindTreeUsecase::SelectToplevel { - return FindTreeResult::AcceptsInput; + match usecase { + FindTreeUsecase::None => {} + FindTreeUsecase::SelectToplevel => return FindTreeResult::AcceptsInput, + FindTreeUsecase::SelectWorkspace => return FindTreeResult::Other, } let rect = self.x.surface.buffer_abs_pos.get(); if x < rect.width() && y < rect.height() { diff --git a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs index 7758c138..c874122b 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs @@ -340,8 +340,10 @@ impl Node for XdgPopup { tree: &mut Vec, usecase: FindTreeUsecase, ) -> FindTreeResult { - if usecase == FindTreeUsecase::SelectToplevel { - return FindTreeResult::Other; + match usecase { + FindTreeUsecase::None => {} + FindTreeUsecase::SelectToplevel => return FindTreeResult::Other, + FindTreeUsecase::SelectWorkspace => 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 0f01ab95..e6e3e45d 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -598,8 +598,10 @@ impl Node for XdgToplevel { tree: &mut Vec, usecase: FindTreeUsecase, ) -> FindTreeResult { - if usecase == FindTreeUsecase::SelectToplevel { - return FindTreeResult::AcceptsInput; + match usecase { + FindTreeUsecase::None => {} + FindTreeUsecase::SelectToplevel => return FindTreeResult::AcceptsInput, + FindTreeUsecase::SelectWorkspace => return FindTreeResult::Other, } self.xdg.find_tree_at(x, y, tree) } diff --git a/src/tree.rs b/src/tree.rs index b2461344..40c0ae2d 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -109,7 +109,7 @@ impl FindTreeResult { } } -#[derive(Copy, Clone, Eq, PartialEq)] +#[derive(Copy, Clone)] pub enum FindTreeUsecase { None, SelectToplevel, diff --git a/src/tree/output.rs b/src/tree/output.rs index 6cdcc2e7..fa468976 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -1073,8 +1073,10 @@ impl OutputNode { tree: &mut Vec, usecase: FindTreeUsecase, ) -> FindTreeResult { - if usecase == FindTreeUsecase::SelectToplevel { - return FindTreeResult::Other; + match usecase { + FindTreeUsecase::None => {} + FindTreeUsecase::SelectToplevel => return FindTreeResult::Other, + FindTreeUsecase::SelectWorkspace => return FindTreeResult::Other, } let len = tree.len(); for layer in layers.iter().copied() { @@ -1634,9 +1636,12 @@ impl Node for OutputNode { usecase: FindTreeUsecase, ) -> FindTreeResult { if self.state.lock.locked.get() { - if usecase != FindTreeUsecase::SelectToplevel - && let Some(ls) = self.lock_surface.get() - { + let allow_surface = match usecase { + FindTreeUsecase::None => true, + FindTreeUsecase::SelectToplevel => false, + FindTreeUsecase::SelectWorkspace => false, + }; + if allow_surface && let Some(ls) = self.lock_surface.get() { tree.push(FoundNode { node: ls.clone(), x, @@ -1647,7 +1652,12 @@ impl Node for OutputNode { return FindTreeResult::AcceptsInput; } let ws_rect_rel = self.workspace_rect_rel.get(); - if usecase == FindTreeUsecase::SelectWorkspace && ws_rect_rel.contains(x, y) { + let select_workspace = match usecase { + FindTreeUsecase::None => false, + FindTreeUsecase::SelectToplevel => false, + FindTreeUsecase::SelectWorkspace => true, + }; + if select_workspace && ws_rect_rel.contains(x, y) { let (x, y) = ws_rect_rel.translate(x, y); if let Some(ws) = self.workspace.get() { tree.push(FoundNode { From 15e6ab2b8a1c133da6d9752b74f2c1f489a5edfb Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 16 Dec 2025 20:29:45 +0100 Subject: [PATCH 2/3] window-management: allow moving/resizing popups --- src/ifs/wl_seat.rs | 4 + src/ifs/wl_seat/pointer_owner.rs | 223 +++++++++++++++--- src/ifs/wl_surface/x_surface/xwindow.rs | 1 + src/ifs/wl_surface/xdg_surface/xdg_popup.rs | 56 ++++- .../wl_surface/xdg_surface/xdg_toplevel.rs | 9 + src/tree.rs | 10 +- src/tree/output.rs | 3 + 7 files changed, 268 insertions(+), 38 deletions(-) diff --git a/src/ifs/wl_seat.rs b/src/ifs/wl_seat.rs index 119d16d9..c0c06717 100644 --- a/src/ifs/wl_seat.rs +++ b/src/ifs/wl_seat.rs @@ -1146,6 +1146,10 @@ impl WlSeatGlobal { } } + pub fn cancel_popup_move(self: &Rc) { + self.pointer_owner.grab_node_removed(self); + } + pub fn cancel_dnd(self: &Rc) { self.pointer_owner.cancel_dnd(self); } diff --git a/src/ifs/wl_seat/pointer_owner.rs b/src/ifs/wl_seat/pointer_owner.rs index a4c879a9..07989d87 100644 --- a/src/ifs/wl_seat/pointer_owner.rs +++ b/src/ifs/wl_seat/pointer_owner.rs @@ -10,7 +10,11 @@ use { BTN_LEFT, BTN_RIGHT, CHANGE_CURSOR_MOVED, CHANGE_TREE, Dnd, DroppedDnd, NodeSeatState, WlSeatError, WlSeatGlobal, wl_pointer::PendingScroll, }, - wl_surface::{WlSurface, dnd_icon::DndIcon}, + wl_surface::{ + WlSurface, + dnd_icon::DndIcon, + xdg_surface::{xdg_popup::XdgPopup, xdg_toplevel::ResizeEdges}, + }, xdg_toplevel_drag_v1::XdgToplevelDragV1, }, rect::Rect, @@ -20,7 +24,7 @@ use { PlaceholderNode, TddType, ToplevelNode, WorkspaceDragDestination, WorkspaceNode, WsMoveConfig, move_ws_to_output, toplevel_set_workspace, }, - utils::{clonecell::CloneCell, smallmap::SmallMap}, + utils::{bitflags::BitflagsExt, clonecell::CloneCell, smallmap::SmallMap}, }, linearize::LinearizeExt, std::{ @@ -944,7 +948,7 @@ impl Drop for SelectWorkspaceUsecase { } impl SimplePointerOwnerUsecase for WindowManagementUsecase { - const FIND_TREE_USECASE: FindTreeUsecase = FindTreeUsecase::SelectToplevel; + const FIND_TREE_USECASE: FindTreeUsecase = FindTreeUsecase::SelectToplevelOrPopup; const IS_DEFAULT: bool = false; fn default_button( @@ -954,33 +958,42 @@ impl SimplePointerOwnerUsecase for WindowManagementUsecase { button: u32, pn: &Rc, ) -> bool { - let Some(tl) = pn.clone().node_into_toplevel() else { - return false; - }; - let pos = tl.node_absolute_position(); + let pos = pn.node_absolute_position(); let (x, y) = seat.pointer_cursor.position(); let (x, y) = (x.round_down(), y.round_down()); let (mut dx, mut dy) = pos.translate(x, y); let owner: Rc = if button == BTN_LEFT { - seat.pointer_cursor.set_known(KnownCursor::Move); - if tl.tl_data().is_fullscreen.get() { - Rc::new(ToplevelGrabPointerOwner { - tl, - usecase: MoveFullscreenToplevelGrabPointerOwner, - }) - } else if tl.tl_data().float.is_none() { - Rc::new(ToplevelGrabPointerOwner { - tl: tl.clone(), - usecase: TileDragUsecase { + if let Some(tl) = pn.clone().node_into_toplevel() { + seat.pointer_cursor.set_known(KnownCursor::Move); + if tl.tl_data().is_fullscreen.get() { + Rc::new(ToplevelGrabPointerOwner { tl, - destination: Default::default(), - }, + usecase: MoveFullscreenToplevelGrabPointerOwner, + }) + } else if tl.tl_data().float.is_none() { + Rc::new(ToplevelGrabPointerOwner { + tl: tl.clone(), + usecase: TileDragUsecase { + tl, + destination: Default::default(), + }, + }) + } else { + Rc::new(ToplevelGrabPointerOwner { + tl, + usecase: MoveToplevelGrabPointerOwner { dx, dy }, + }) + } + } else if let Some(popup) = pn.clone().node_into_popup() { + popup.add_interactive_move(seat); + seat.pointer_cursor.set_known(KnownCursor::Move); + Rc::new(PopupPointerOwner { + popup, + button, + usecase: PopupPointerOwnerMoveUsecase { dx, dy }, }) } else { - Rc::new(ToplevelGrabPointerOwner { - tl, - usecase: MoveToplevelGrabPointerOwner { dx, dy }, - }) + return false; } } else if button == BTN_RIGHT { let mut top = false; @@ -1006,18 +1019,39 @@ impl SimplePointerOwnerUsecase for WindowManagementUsecase { (true, false, false, true) => KnownCursor::NwResize, _ => KnownCursor::Move, }; - seat.pointer_cursor.set_known(cursor); - Rc::new(ToplevelGrabPointerOwner { - tl, - usecase: ResizeToplevelGrabPointerOwner { - top, - right, - bottom, - left, - dx, - dy, - }, - }) + if let Some(tl) = pn.clone().node_into_toplevel() { + seat.pointer_cursor.set_known(cursor); + Rc::new(ToplevelGrabPointerOwner { + tl, + usecase: ResizeToplevelGrabPointerOwner { + top, + right, + bottom, + left, + dx, + dy, + }, + }) + } else if let Some(popup) = pn.clone().node_into_popup() { + popup.add_interactive_move(seat); + seat.pointer_cursor.set_known(cursor); + Rc::new(PopupPointerOwner { + popup, + button, + usecase: PopupPointerOwnerResizeUsecase { + edges: ResizeEdges { + top, + left, + right, + bottom, + }, + dx, + dy, + }, + }) + } else { + return false; + } } else { return false; }; @@ -1458,3 +1492,122 @@ impl UiDragUsecase for WorkspaceDragUsecase { } } } + +struct PopupPointerOwner { + popup: Rc, + button: u32, + usecase: T, +} + +trait PopupPointerOwnerUsecase: Sized + 'static { + fn apply_changes(&self, popup: &Rc, seat: &Rc); +} + +impl PopupPointerOwner +where + T: PopupPointerOwnerUsecase, +{ + fn revert_to_window_management(&self, seat: &Rc) { + self.popup.remove_interactive_move(seat); + self.popup.node_seat_state().remove_pointer_grab(seat); + seat.pointer_cursor.set_known(KnownCursor::Default); + seat.pointer_owner.owner.set(Rc::new(SimplePointerOwner { + usecase: WindowManagementUsecase, + })); + seat.changes.or_assign(CHANGE_CURSOR_MOVED); + seat.apply_changes(); + } + + fn revert_to_previous(&self, seat: &Rc) { + self.revert_to_window_management(seat); + } +} + +impl PointerOwner for PopupPointerOwner +where + T: PopupPointerOwnerUsecase, +{ + fn button(&self, seat: &Rc, _time_usec: u64, button: u32, state: ButtonState) { + if button != self.button || state != ButtonState::Released { + return; + } + self.revert_to_previous(seat); + } + + fn apply_changes(&self, seat: &Rc) { + if seat.changes.get().not_contains(CHANGE_CURSOR_MOVED) { + return; + } + self.usecase.apply_changes(&self.popup, seat); + } + + fn revert_to_default(&self, seat: &Rc) { + self.popup.remove_interactive_move(seat); + self.popup.node_seat_state().remove_pointer_grab(seat); + seat.pointer_owner.set_default_pointer_owner(seat); + seat.tree_changed.trigger(); + } + + fn grab_node_removed(&self, seat: &Rc) { + self.revert_to_previous(seat); + } + + fn disable_window_management(&self, seat: &Rc) { + self.revert_to_default(seat); + } +} + +struct PopupPointerOwnerMoveUsecase { + dx: i32, + dy: i32, +} + +impl PopupPointerOwnerUsecase for PopupPointerOwnerMoveUsecase { + fn apply_changes(&self, popup: &Rc, seat: &Rc) { + let (x, y) = seat.pointer_cursor.position(); + let (x, y) = (x.round_down(), y.round_down()); + let pos = popup.node_absolute_position(); + let (x, y) = pos.translate(x, y); + if (x, y) != (self.dx, self.dy) { + popup.move_(x - self.dx, y - self.dy); + seat.tree_changed.trigger(); + } + } +} + +struct PopupPointerOwnerResizeUsecase { + edges: ResizeEdges, + dx: i32, + dy: i32, +} + +impl PopupPointerOwnerUsecase for PopupPointerOwnerResizeUsecase { + fn apply_changes(&self, popup: &Rc, seat: &Rc) { + let (x, y) = seat.pointer_cursor.position(); + let (x, y) = (x.round_down(), y.round_down()); + let pos = popup.node_absolute_position(); + let (x, y) = pos.translate(x, y); + let mut dx1 = 0; + let mut dx2 = 0; + let mut dy1 = 0; + let mut dy2 = 0; + if self.edges.left { + dx1 = x - self.dx; + dx1 = dx1.min(pos.width().saturating_sub(1)); + } else if self.edges.right { + dx2 = self.dx - (pos.width() - x); + dx2 = dx2.max(-pos.width().saturating_sub(1)); + } + if self.edges.top { + dy1 = y - self.dy; + dy1 = dy1.min(pos.height().saturating_sub(1)); + } else if self.edges.bottom { + dy2 = self.dy - (pos.height() - y); + dy2 = dy2.max(-pos.height().saturating_sub(1)); + } + if dx1 != 0 || dx2 != 0 || dy1 != 0 || dy2 != 0 { + popup.resize(dx1, dy1, dx2, dy2); + seat.tree_changed.trigger(); + } + } +} diff --git a/src/ifs/wl_surface/x_surface/xwindow.rs b/src/ifs/wl_surface/x_surface/xwindow.rs index f156323e..60e5e24e 100644 --- a/src/ifs/wl_surface/x_surface/xwindow.rs +++ b/src/ifs/wl_surface/x_surface/xwindow.rs @@ -399,6 +399,7 @@ impl Node for Xwindow { match usecase { FindTreeUsecase::None => {} FindTreeUsecase::SelectToplevel => return FindTreeResult::AcceptsInput, + FindTreeUsecase::SelectToplevelOrPopup => return FindTreeResult::AcceptsInput, FindTreeUsecase::SelectWorkspace => return FindTreeResult::Other, } let rect = self.x.surface.buffer_abs_pos.get(); diff --git a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs index c874122b..fa6cb961 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_popup.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_popup.rs @@ -4,7 +4,7 @@ use { cursor::KnownCursor, fixed::Fixed, ifs::{ - wl_seat::{NodeSeatState, WlSeatGlobal, tablet::TabletTool}, + wl_seat::{NodeSeatState, SeatId, WlSeatGlobal, tablet::TabletTool}, wl_surface::{ tray::TrayItemId, xdg_surface::{XdgSurface, XdgSurfaceExt}, @@ -22,7 +22,7 @@ use { Direction, FindTreeResult, FindTreeUsecase, FoundNode, Node, NodeId, NodeLayerLink, NodeLocation, NodeVisitor, OutputNode, StackedNode, }, - utils::clonecell::CloneCell, + utils::{clonecell::CloneCell, smallmap::SmallMap}, wire::{XdgPopupId, xdg_popup::*}, }, std::{ @@ -65,6 +65,7 @@ pub struct XdgPopup { pub tracker: Tracker, seat_state: NodeSeatState, set_visible_prepared: Cell, + interactive_moves: SmallMap, 1>, } impl Debug for XdgPopup { @@ -93,6 +94,7 @@ impl XdgPopup { tracker: Default::default(), seat_state: Default::default(), set_visible_prepared: Cell::new(false), + interactive_moves: Default::default(), }) } @@ -222,6 +224,43 @@ impl XdgPopup { .set_absolute_desired_extents(&rel.move_(parent.x1(), parent.y1())); } } + + fn set_relative_position(&self, rel: Rect) { + self.relative_position.set(rel); + self.update_absolute_position(); + self.send_configure(rel.x1(), rel.y1(), rel.width(), rel.height()); + self.xdg.schedule_configure(); + } + + pub fn move_(&self, dx: i32, dy: i32) { + let rel = self.relative_position.get().move_(dx, dy); + self.set_relative_position(rel); + } + + pub fn resize(&self, dx1: i32, dy1: i32, dx2: i32, dy2: i32) { + let rel = self.relative_position.get(); + let rel = Rect::new( + rel.x1() + dx1, + rel.y1() + dy1, + rel.x2() + dx2, + rel.y2() + dy2, + ); + let Some(rel) = rel else { + return; + }; + if rel.is_empty() { + return; + } + self.set_relative_position(rel); + } + + pub fn add_interactive_move(&self, seat: &Rc) { + self.interactive_moves.insert(seat.id(), seat.clone()); + } + + pub fn remove_interactive_move(&self, seat: &Rc) { + self.interactive_moves.remove(&seat.id()); + } } impl XdgPopupRequestHandler for XdgPopup { @@ -240,6 +279,9 @@ impl XdgPopupRequestHandler for XdgPopup { fn reposition(&self, req: Reposition, _slf: &Rc) -> Result<(), Self::Error> { *self.pos.borrow_mut() = self.xdg.surface.client.lookup(req.positioner)?.value(); + while let Some((_, seat)) = self.interactive_moves.pop() { + seat.cancel_popup_move(); + } if let Some(parent) = self.parent.get() { self.update_position(&*parent); let rel = self.relative_position.get(); @@ -343,6 +385,12 @@ impl Node for XdgPopup { match usecase { FindTreeUsecase::None => {} FindTreeUsecase::SelectToplevel => return FindTreeResult::Other, + FindTreeUsecase::SelectToplevelOrPopup => { + let len = tree.len(); + let res = self.xdg.find_tree_at(x, y, tree); + tree.truncate(len); + return res; + } FindTreeUsecase::SelectWorkspace => return FindTreeResult::Other, } self.xdg.find_tree_at(x, y, tree) @@ -380,6 +428,10 @@ impl Node for XdgPopup { ) { tool.cursor().set_known(KnownCursor::Default) } + + fn node_into_popup(self: Rc) -> Option> { + Some(self) + } } impl StackedNode for XdgPopup { diff --git a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs index e6e3e45d..4ecbfe81 100644 --- a/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs +++ b/src/ifs/wl_surface/xdg_surface/xdg_toplevel.rs @@ -601,6 +601,7 @@ impl Node for XdgToplevel { match usecase { FindTreeUsecase::None => {} FindTreeUsecase::SelectToplevel => return FindTreeResult::AcceptsInput, + FindTreeUsecase::SelectToplevelOrPopup => return FindTreeResult::AcceptsInput, FindTreeUsecase::SelectWorkspace => return FindTreeResult::Other, } self.xdg.find_tree_at(x, y, tree) @@ -832,3 +833,11 @@ pub enum XdgToplevelError { NonNegative, } efrom!(XdgToplevelError, ClientError); + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub struct ResizeEdges { + pub top: bool, + pub left: bool, + pub right: bool, + pub bottom: bool, +} diff --git a/src/tree.rs b/src/tree.rs index 40c0ae2d..0a696deb 100644 --- a/src/tree.rs +++ b/src/tree.rs @@ -13,7 +13,10 @@ use { }, wl_pointer::PendingScroll, }, - wl_surface::{WlSurface, tray::TrayItemId, zwlr_layer_surface_v1::ZwlrLayerSurfaceV1}, + wl_surface::{ + WlSurface, tray::TrayItemId, xdg_surface::xdg_popup::XdgPopup, + zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, + }, }, keyboard::KeyboardState, rect::Rect, @@ -113,6 +116,7 @@ impl FindTreeResult { pub enum FindTreeUsecase { None, SelectToplevel, + SelectToplevelOrPopup, SelectWorkspace, } @@ -642,6 +646,10 @@ pub trait Node: 'static { None } + fn node_into_popup(self: Rc) -> Option> { + None + } + // TYPE CHECKERS fn node_is_container(&self) -> bool { diff --git a/src/tree/output.rs b/src/tree/output.rs index fa468976..3455861b 100644 --- a/src/tree/output.rs +++ b/src/tree/output.rs @@ -1076,6 +1076,7 @@ impl OutputNode { match usecase { FindTreeUsecase::None => {} FindTreeUsecase::SelectToplevel => return FindTreeResult::Other, + FindTreeUsecase::SelectToplevelOrPopup => return FindTreeResult::Other, FindTreeUsecase::SelectWorkspace => return FindTreeResult::Other, } let len = tree.len(); @@ -1639,6 +1640,7 @@ impl Node for OutputNode { let allow_surface = match usecase { FindTreeUsecase::None => true, FindTreeUsecase::SelectToplevel => false, + FindTreeUsecase::SelectToplevelOrPopup => false, FindTreeUsecase::SelectWorkspace => false, }; if allow_surface && let Some(ls) = self.lock_surface.get() { @@ -1655,6 +1657,7 @@ impl Node for OutputNode { let select_workspace = match usecase { FindTreeUsecase::None => false, FindTreeUsecase::SelectToplevel => false, + FindTreeUsecase::SelectToplevelOrPopup => false, FindTreeUsecase::SelectWorkspace => true, }; if select_workspace && ws_rect_rel.contains(x, y) { From 1d3dfa8b3a611b96fae4ae42718854e91a7bba6c Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 16 Dec 2025 20:21:10 +0100 Subject: [PATCH 3/3] 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, +}