atagen

monolith

public
Star0
Watch0

Commit 25731952c6b8

tarball

atagen <boss@atagen.co>2026-10-19 08:00 UTC

3 files changed+179-20
input: honour client move and resize requests on floating windows

xdg move_request and resize_request and their X11 stubs were no-ops, so clients with their own headerbars or resize handles could not drag a floating window. The Mod+drag state now takes any set of resize edges and a client button whose release is still forwarded, since the client saw the press.
Modified file: crates/monolith/src/input.rs+111 additions−12 deletions
@@ -7,9 +7,11 @@ use smithay::backend::input::{ButtonState, InputTime, KeyState};
77 use smithay::backend::session::Session;
88 use smithay::input::keyboard::{FilterResult, Keycode, xkb};
99 use smithay::input::pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent};
10+use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
1011 use smithay::reexports::wayland_server::Resource;
1112 use smithay::utils::{Logical, Point, SERIAL_COUNTER};
1213 use smithay::wayland::shell::wlr_layer::Layer;
14+use smithay::xwayland::xwm::ResizeEdge;
1315
1416 impl State {
1517 pub fn pointer_to(&mut self, pos: Point<f64, Logical>, time: InputTime) {
@@ -42,8 +44,14 @@ impl State {
4244
4345 pub fn pointer_button(&mut self, button: u32, pressed: ButtonState, time: InputTime) {
4446 self.activity();
45- if pressed == ButtonState::Released && self.input.drag.take().is_some() {
46- return;
47+ if pressed == ButtonState::Released {
48+ let ended = self
49+ .input
50+ .drag
51+ .take_if(|d| d.button.is_none_or(|b| b == button));
52+ if ended.is_some_and(|d| d.button.is_none()) {
53+ return;
54+ }
4755 }
4856 if pressed == ButtonState::Pressed && !self.services.lock.active() {
4957 focus_under_pointer(self);
@@ -236,13 +244,76 @@ impl State {
236244
237245 const BTN_LEFT: u32 = 0x110;
238246 const BTN_RIGHT: u32 = 0x111;
247+const MIN_SIZE: f64 = 32.0;
248+
249+/// The sides of a window a resize moves.
250+#[derive(Clone, Copy, Default)]
251+pub struct Edges {
252+ left: bool,
253+ right: bool,
254+ top: bool,
255+ bottom: bool,
256+}
239257
240-/// A floating window being moved or resized with Mod and a mouse button.
258+impl Edges {
259+ const BOTTOM_RIGHT: Self = Self {
260+ left: false,
261+ right: true,
262+ top: false,
263+ bottom: true,
264+ };
265+
266+ pub fn from_xdg(edge: xdg_toplevel::ResizeEdge) -> Option<Self> {
267+ use xdg_toplevel::ResizeEdge as E;
268+ let (left, right, top, bottom) = match edge {
269+ E::Top => (false, false, true, false),
270+ E::Bottom => (false, false, false, true),
271+ E::Left => (true, false, false, false),
272+ E::Right => (false, true, false, false),
273+ E::TopLeft => (true, false, true, false),
274+ E::TopRight => (false, true, true, false),
275+ E::BottomLeft => (true, false, false, true),
276+ E::BottomRight => (false, true, false, true),
277+ _ => return None,
278+ };
279+ Some(Self {
280+ left,
281+ right,
282+ top,
283+ bottom,
284+ })
285+ }
286+
287+ pub fn from_x11(edge: ResizeEdge) -> Self {
288+ let (left, right, top, bottom) = match edge {
289+ ResizeEdge::Top => (false, false, true, false),
290+ ResizeEdge::Bottom => (false, false, false, true),
291+ ResizeEdge::Left => (true, false, false, false),
292+ ResizeEdge::Right => (false, true, false, false),
293+ ResizeEdge::TopLeft => (true, false, true, false),
294+ ResizeEdge::TopRight => (false, true, true, false),
295+ ResizeEdge::BottomLeft => (true, false, false, true),
296+ ResizeEdge::BottomRight => (false, true, false, true),
297+ };
298+ Self {
299+ left,
300+ right,
301+ top,
302+ bottom,
303+ }
304+ }
305+}
306+
307+/// A floating window being moved or resized, either with Mod and a mouse
308+/// button or because its client asked while holding `button`.
241309 pub struct Drag {
242310 window: WindowId,
243311 from: Point<f64, Logical>,
244312 rect: Rect,
245- resize: bool,
313+ /// None moves the window.
314+ edges: Option<Edges>,
315+ /// The client's button, whose release is still the client's to see.
316+ button: Option<u32>,
246317 }
247318
248319 fn start_drag(state: &mut State, button: u32) -> bool {
@@ -258,6 +329,22 @@ fn start_drag(state: &mut State, button: u32) -> bool {
258329 let Some(id) = state.wm.layout.focused() else {
259330 return false;
260331 };
332+ if !held {
333+ return false;
334+ }
335+ let edges = (button == BTN_RIGHT).then_some(Edges::BOTTOM_RIGHT);
336+ begin_drag(state, id, edges, None)
337+}
338+
339+impl State {
340+ /// Starts moving, or with `edges` resizing, a floating window at a
341+ /// client's request. Tiled windows are left alone.
342+ pub fn client_drag(&mut self, id: WindowId, edges: Option<Edges>, button: u32) {
343+ begin_drag(self, id, edges, Some(button));
344+ }
345+}
346+
347+fn begin_drag(state: &mut State, id: WindowId, edges: Option<Edges>, button: Option<u32>) -> bool {
261348 let Some(rect) = state
262349 .wm
263350 .layout
@@ -267,28 +354,40 @@ fn start_drag(state: &mut State, button: u32) -> bool {
267354 else {
268355 return false;
269356 };
270- if !held {
271- return false;
272- }
273357 state.input.drag = Some(Drag {
274358 window: id,
275359 from: state.input.pointer_pos,
276360 rect,
277- resize: button == BTN_RIGHT,
361+ edges,
362+ button,
278363 });
279364 true
280365 }
281366
367+fn resized(start: f64, len: f64, delta: f64, low: bool, high: bool) -> (f64, f64) {
368+ if low {
369+ let len_now = (len - delta).max(MIN_SIZE);
370+ (start + len - len_now, len_now)
371+ } else if high {
372+ (start, (len + delta).max(MIN_SIZE))
373+ } else {
374+ (start, len)
375+ }
376+}
377+
282378 fn drag_to(state: &mut State, pos: Point<f64, Logical>) -> bool {
283379 let Some(d) = &state.input.drag else {
284380 return false;
285381 };
286382 let (dx, dy) = (pos.x - d.from.x, pos.y - d.from.y);
287383 let r = d.rect;
288- let rect = if d.resize {
289- Rect::new(r.x, r.y, (r.w + dx).max(32.0), (r.h + dy).max(32.0))
290- } else {
291- Rect::new(r.x + dx, r.y + dy, r.w, r.h)
384+ let rect = match d.edges {
385+ Some(e) => {
386+ let (x, w) = resized(r.x, r.w, dx, e.left, e.right);
387+ let (y, h) = resized(r.y, r.h, dy, e.top, e.bottom);
388+ Rect::new(x, y, w, h)
389+ }
390+ None => Rect::new(r.x + dx, r.y + dy, r.w, r.h),
292391 };
293392 let id = d.window;
294393 state.wm.layout.set_float_rect(id, rect);
Modified file: crates/monolith/src/xdg.rs+47 additions
@@ -1,10 +1,14 @@
1+use crate::input::Edges;
12 use crate::state::State;
23 use monolith_layout::OutputId;
34 use smithay::desktop::{PopupKind, Window};
5+use smithay::input::Seat;
6+use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
47 use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode;
58 use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as XdgState;
69 use smithay::reexports::wayland_server::protocol::wl_output::WlOutput;
710 use smithay::reexports::wayland_server::protocol::wl_seat::WlSeat;
11+use smithay::reexports::wayland_server::Resource;
812 use smithay::utils::{Logical, Serial, Size};
913 use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler;
1014 use smithay::wayland::compositor::with_states;
@@ -46,6 +50,22 @@ impl XdgShellHandler for State {
4650 self.unmap_window(surface.wl_surface());
4751 }
4852
53+ fn move_request(&mut self, surface: ToplevelSurface, seat: WlSeat, serial: Serial) {
54+ self.xdg_drag(&surface, &seat, serial, None);
55+ }
56+
57+ fn resize_request(
58+ &mut self,
59+ surface: ToplevelSurface,
60+ seat: WlSeat,
61+ serial: Serial,
62+ edges: xdg_toplevel::ResizeEdge,
63+ ) {
64+ if let Some(edges) = Edges::from_xdg(edges) {
65+ self.xdg_drag(&surface, &seat, serial, Some(edges));
66+ }
67+ }
68+
4969 fn fullscreen_request(&mut self, surface: ToplevelSurface, output: Option<WlOutput>) {
5070 self.set_fullscreen(&surface, true, output);
5171 }
@@ -102,6 +122,33 @@ fn server_side(toplevel: &ToplevelSurface) {
102122 }
103123
104124 impl State {
125+ /// Honours a move or resize only for a serial that is the press the
126+ /// pointer is holding on one of the requesting client's surfaces.
127+ fn xdg_drag(
128+ &mut self,
129+ surface: &ToplevelSurface,
130+ seat: &WlSeat,
131+ serial: Serial,
132+ edges: Option<Edges>,
133+ ) {
134+ let Some(pointer) = Seat::<State>::from_resource(seat).and_then(|s| s.get_pointer()) else {
135+ return;
136+ };
137+ let Some(start) = pointer.grab_start_data() else {
138+ return;
139+ };
140+ let held_by_client = start
141+ .focus
142+ .as_ref()
143+ .is_some_and(|(f, _)| f.id().same_client_as(&surface.wl_surface().id()));
144+ if !pointer.has_grab(serial) || !held_by_client {
145+ return;
146+ }
147+ if let Some(id) = self.window_id(surface.wl_surface()) {
148+ self.client_drag(id, edges, start.button);
149+ }
150+ }
151+
105152 /// A toplevel that has not mapped has no window to change, so its wish is
106153 /// kept in its pending state for `map_window` to find.
107154 fn set_fullscreen(&mut self, toplevel: &ToplevelSurface, on: bool, output: Option<WlOutput>) {
Modified file: crates/monolith/src/xwayland.rs+21 additions−8 deletions
@@ -4,6 +4,7 @@
44 //! leaves and the socket is watched again.
55
66 use crate::focus::Focus;
7+use crate::input::Edges;
78 use crate::state::State;
89 use monolith_config::SelectionAccess;
910 use smithay::desktop::Window;
@@ -374,16 +375,13 @@ impl XwmHandler for State {
374375 }
375376 }
376377
377- fn resize_request(
378- &mut self,
379- _xwm: XwmId,
380- _window: X11Surface,
381- _button: u32,
382- _edge: ResizeEdge,
383- ) {
378+ fn resize_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32, edge: ResizeEdge) {
379+ self.x11_drag(&window, Some(Edges::from_x11(edge)));
384380 }
385381
386- fn move_request(&mut self, _xwm: XwmId, _window: X11Surface, _button: u32) {}
382+ fn move_request(&mut self, _xwm: XwmId, window: X11Surface, _button: u32) {
383+ self.x11_drag(&window, None);
384+ }
387385
388386 fn fullscreen_request(&mut self, _xwm: XwmId, window: X11Surface) {
389387 self.x11_fullscreen(&window, true);
@@ -524,6 +522,21 @@ impl State {
524522 .map(|(id, _)| id)
525523 }
526524
525+ /// X11 requests carry no serial, so the pointer must be holding a press.
526+ fn x11_drag(&mut self, window: &X11Surface, edges: Option<Edges>) {
527+ let Some(start) = self
528+ .input
529+ .seat
530+ .get_pointer()
531+ .and_then(|p| p.grab_start_data())
532+ else {
533+ return;
534+ };
535+ if let Some(id) = self.x11_window_id(window) {
536+ self.client_drag(id, edges, start.button);
537+ }
538+ }
539+
527540 fn forget_x11(&mut self, window: &X11Surface) {
528541 if let Some(x) = self.services.xwayland.as_mut() {
529542 x.unmanaged.retain(|w| w != window);