From 15e6ab2b8a1c133da6d9752b74f2c1f489a5edfb Mon Sep 17 00:00:00 2001 From: Julian Orth Date: Tue, 16 Dec 2025 20:29:45 +0100 Subject: [PATCH] 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) {