atagen

monolith

public
Star0
Watch0

Commit e32c28d90a3d

tarball

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

4 files changed+140-10
input: grab window borders to resize with a resize cursor

Resizing needed Mod or a client that asked for it, so tiles and bare floats could only be resized from the keyboard. A ring just outside each window, wider than the drawn border, now picks the edges or corner under the pointer, shows the matching resize cursor, and starts the same drag as Mod+right-drag on a left press. Edges of a tile with no neighbour across them are not grabbable.
Modified file: crates/monolith/src/cursor.rs+8 additions
@@ -55,6 +55,14 @@ impl Cursor {
5555 CursorIcon::Progress,
5656 CursorIcon::EwResize,
5757 CursorIcon::NsResize,
58+ CursorIcon::NResize,
59+ CursorIcon::SResize,
60+ CursorIcon::EResize,
61+ CursorIcon::WResize,
62+ CursorIcon::NwResize,
63+ CursorIcon::NeResize,
64+ CursorIcon::SwResize,
65+ CursorIcon::SeResize,
5866 ] {
5967 cursor.image(icon);
6068 }
Modified file: crates/monolith/src/frame.rs+3 additions−5 deletions
@@ -43,11 +43,9 @@ pub fn compose(
4343 if area.contains(pos.x, pos.y) {
4444 let local = pos - Point::from((area.x, area.y));
4545 if cursor {
46- elements.extend(ctx.input.cursor.elements(
47- ctx.renderer,
48- &ctx.input.cursor_status,
49- local,
50- ));
46+ let resize = ctx.input.resize_icon().map(CursorImageStatus::Named);
47+ let status = resize.as_ref().unwrap_or(&ctx.input.cursor_status);
48+ elements.extend(ctx.input.cursor.elements(ctx.renderer, status, local));
5149 }
5250 if let Some(icon) = &ctx.input.dnd_icon {
5351 elements.extend(icon.elements(ctx.renderer, local));
Modified file: crates/monolith/src/input.rs+126 additions−5 deletions
@@ -1,12 +1,12 @@
11 //! Input handling shared by every backend. Backends translate their events
22 //! into these calls, with pointer positions in global logical coordinates.
33
4-use crate::state::State;
4+use crate::state::{Input, State};
55 use monolith_layout::{Axis, Dir, Layout, MIN_FLOAT_SIZE, Rect, WindowId, clamp_float};
66 use smithay::backend::input::{ButtonState, InputTime, KeyState};
77 use smithay::backend::session::Session;
88 use smithay::input::keyboard::{FilterResult, Keycode, xkb};
9-use smithay::input::pointer::{AxisFrame, ButtonEvent, CursorImageStatus, MotionEvent};
9+use smithay::input::pointer::{AxisFrame, ButtonEvent, CursorIcon, CursorImageStatus, MotionEvent};
1010 use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel;
1111 use smithay::reexports::wayland_server::Resource;
1212 use smithay::utils::{Logical, Point, SERIAL_COUNTER};
@@ -26,6 +26,10 @@ impl State {
2626 focus_on_hover(self, pos, self.input.warping);
2727 let under = self.surface_under(pos);
2828 let before = pointer.current_focus().and_then(|s| s.client());
29+ let hover = self.edge_at(pos, under.is_some()).map(|(_, e)| e);
30+ if std::mem::replace(&mut self.input.hover, hover) != hover {
31+ self.dirty.cursor = true;
32+ }
2933 pointer.motion(
3034 self,
3135 under,
@@ -49,6 +53,12 @@ impl State {
4953 .input
5054 .drag
5155 .take_if(|d| d.button.is_none_or(|b| b == button));
56+ if ended.is_some() {
57+ let pos = self.input.pointer_pos;
58+ let over = self.surface_under(pos).is_some();
59+ self.input.hover = self.edge_at(pos, over).map(|(_, e)| e);
60+ self.dirty.cursor = true;
61+ }
5262 if ended.is_some_and(|d| d.button.is_none()) {
5363 return;
5464 }
@@ -302,6 +312,42 @@ impl Edges {
302312 self.left || self.right || self.top || self.bottom
303313 }
304314
315+ /// The edges whose ring of width hw around r holds pos, a corner when
316+ /// pos is near the end of a side. None inside r or beyond the ring.
317+ fn in_ring(r: Rect, pos: Point<f64, Logical>, hw: f64) -> Option<Self> {
318+ let (left, right) = (pos.x < r.x, pos.x >= r.right());
319+ let (top, bottom) = (pos.y < r.y, pos.y >= r.bottom());
320+ let beyond = pos.x < r.x - hw
321+ || pos.x >= r.right() + hw
322+ || pos.y < r.y - hw
323+ || pos.y >= r.bottom() + hw;
324+ if beyond || !(left || right || top || bottom) {
325+ return None;
326+ }
327+ let (cx, cy) = ((2.0 * hw).min(r.w / 2.0), (2.0 * hw).min(r.h / 2.0));
328+ let sideways = (left || right) && !(top || bottom);
329+ let lengthwise = (top || bottom) && !(left || right);
330+ Some(Self {
331+ left: left || (lengthwise && pos.x < r.x + cx),
332+ right: right || (lengthwise && pos.x >= r.right() - cx),
333+ top: top || (sideways && pos.y < r.y + cy),
334+ bottom: bottom || (sideways && pos.y >= r.bottom() - cy),
335+ })
336+ }
337+
338+ pub fn icon(self) -> CursorIcon {
339+ match (self.left, self.right, self.top, self.bottom) {
340+ (true, _, true, _) => CursorIcon::NwResize,
341+ (_, true, true, _) => CursorIcon::NeResize,
342+ (true, _, _, true) => CursorIcon::SwResize,
343+ (_, true, _, true) => CursorIcon::SeResize,
344+ (true, ..) => CursorIcon::WResize,
345+ (_, true, ..) => CursorIcon::EResize,
346+ (_, _, true, _) => CursorIcon::NResize,
347+ _ => CursorIcon::SResize,
348+ }
349+ }
350+
305351 pub fn from_xdg(edge: xdg_toplevel::ResizeEdge) -> Option<Self> {
306352 use xdg_toplevel::ResizeEdge as E;
307353 let (left, right, top, bottom) = match edge {
@@ -358,6 +404,70 @@ pub struct Drag {
358404 button: Option<u32>,
359405 }
360406
407+impl Input {
408+ /// The resize cursor to show over the client's own, if any.
409+ pub fn resize_icon(&self) -> Option<CursorIcon> {
410+ let edges = match &self.drag {
411+ Some(d) => d.edges,
412+ None => self.hover,
413+ };
414+ edges.map(Edges::icon)
415+ }
416+}
417+
418+impl State {
419+ /// The window and edges of a resize grab at pos. Rings sit just outside
420+ /// windows, so the client's own surface is never taken over, except
421+ /// that a floating window's ring wins over a tile it overlaps.
422+ fn edge_at(&self, pos: Point<f64, Logical>, over_surface: bool) -> Option<(WindowId, Edges)> {
423+ if self.services.lock.active() {
424+ return None;
425+ }
426+ let layout = &self.wm.layout;
427+ let (o, out) = layout
428+ .outputs()
429+ .find(|(_, out)| out.area.contains(pos.x, pos.y))?;
430+ let floats = !out.floating.is_empty()
431+ || layout
432+ .scratchpads()
433+ .iter()
434+ .any(|s| s.shown.is_some() && s.output == Some(o));
435+ if over_surface && !floats {
436+ return None;
437+ }
438+ let arr = layout.arrange(o);
439+ if arr.fullscreen.is_some() {
440+ return None;
441+ }
442+ let hw = f64::from(self.settings.config.decoration.border.max(4) + 4);
443+ for &(w, r) in arr.floating.iter().rev() {
444+ if r.contains(pos.x, pos.y) {
445+ return None;
446+ }
447+ if let Some(e) = Edges::in_ring(r, pos, hw) {
448+ return Some((w, e));
449+ }
450+ }
451+ if over_surface {
452+ return None;
453+ }
454+ let distance = |r: Rect| {
455+ let dx = (r.x - pos.x).max(pos.x - r.right()).max(0.0);
456+ let dy = (r.y - pos.y).max(pos.y - r.bottom()).max(0.0);
457+ dx.hypot(dy)
458+ };
459+ arr.tiled
460+ .iter()
461+ .filter(|p| p.visible)
462+ .filter_map(|p| {
463+ let e = Edges::in_ring(p.rect, pos, hw)?.movable(layout, p.window);
464+ e.any().then_some((p.window, e, distance(p.rect)))
465+ })
466+ .min_by(|a, b| a.2.total_cmp(&b.2))
467+ .map(|(w, e, _)| (w, e))
468+ }
469+}
470+
361471 fn start_drag(state: &mut State, button: u32) -> bool {
362472 if button != BTN_LEFT && button != BTN_RIGHT {
363473 return false;
@@ -368,12 +478,23 @@ fn start_drag(state: &mut State, button: u32) -> bool {
368478 .get_keyboard()
369479 .map(|k| k.modifier_state())
370480 .is_some_and(|m| crate::commands::held(&m).has(state.settings.config.general.mod_key));
481+ if !held {
482+ let pos = state.input.pointer_pos;
483+ let over = state.surface_under(pos).is_some();
484+ let Some((id, edges)) = state.edge_at(pos, over).filter(|_| button == BTN_LEFT) else {
485+ return false;
486+ };
487+ if state.wm.layout.focused() != Some(id) {
488+ state.wm.layer_focus = None;
489+ state.input.followed = Some(id);
490+ state.wm.layout.focus(id);
491+ state.apply_layout();
492+ }
493+ return begin_drag(state, id, Some(edges), None);
494+ }
371495 let Some(id) = state.wm.layout.focused() else {
372496 return false;
373497 };
374- if !held {
375- return false;
376- }
377498 let edges = match button {
378499 BTN_RIGHT => {
379500 let Some(rect) = window_rect(&state.wm.layout, id) else {
Modified file: crates/monolith/src/state.rs+3 additions
@@ -60,6 +60,8 @@ pub struct Input {
6060 pub cursor: crate::cursor::Cursor,
6161 pub cursor_status: CursorImageStatus,
6262 pub dnd_icon: Option<crate::cursor::DndIcon>,
63+ /// The edges of the window border under the pointer, for the resize cursor.
64+ pub hover: Option<crate::input::Edges>,
6365 }
6466
6567 /// What needs doing after this batch of events.
@@ -191,6 +193,7 @@ impl State {
191193 cursor: crate::cursor::Cursor::load(),
192194 cursor_status: CursorImageStatus::default_named(),
193195 dnd_icon: None,
196+ hover: None,
194197 },
195198 dirty: Dirty {
196199 redraw: true,