atagen

monolith

public
Star0
Watch0

Commit b01f4e7ab7e6

tarball

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

3 files changed+80-25
input: keep dragged floating windows reachable

Mod+drag and client move or resize requests could push a floating window entirely off its output with no way back. Moves now go through layout clamp_float against the output's usable area, and resizes stop each dragged edge at that area.
Modified file: crates/layout/src/lib.rs+38 additions−8 deletions
@@ -91,6 +91,19 @@ pub struct Output {
9191 pub floating: Vec<WindowId>,
9292 }
9393
94+impl Output {
95+ /// The area left once bars and panels have taken their edges.
96+ pub fn usable(&self) -> Rect {
97+ let (a, r) = (self.area, self.reserved);
98+ Rect::new(
99+ a.x + r.left,
100+ a.y + r.top,
101+ (a.w - r.left - r.right).max(0.0),
102+ (a.h - r.top - r.bottom).max(0.0),
103+ )
104+ }
105+}
106+
94107 #[derive(Clone, Debug)]
95108 pub struct Scratchpad {
96109 pub name: String,
@@ -725,14 +738,7 @@ impl Layout {
725738 let Some(out) = self.outputs.get(o) else {
726739 return Arrangement::default();
727740 };
728- let r = out.reserved;
729- let usable = Rect::new(
730- out.area.x + r.left,
731- out.area.y + r.top,
732- (out.area.w - r.left - r.right).max(0.0),
733- (out.area.h - r.top - r.bottom).max(0.0),
734- );
735- let area = inset(usable, self.gaps.outer);
741+ let area = inset(out.usable(), self.gaps.outer);
736742 let shown = self.vis_on(o);
737743 let vis = &|w: WindowId| shown(w) && !hide.contains(&w);
738744 let tiled = out
@@ -918,6 +924,30 @@ fn centred(area: Rect, frac: f64) -> Rect {
918924 )
919925 }
920926
927+/// The smallest a floating window gets when dragged or rescaled.
928+pub const MIN_FLOAT_SIZE: f64 = 32.0;
929+
930+/// How much of a floating window stays reachable when it is larger than
931+/// the area it lives in.
932+const STRIP: f64 = 64.0;
933+
934+/// Moves a floating rect to keep it reachable inside bounds. A window that
935+/// fits stays whole inside, a larger one keeps its top edge and a strip.
936+pub fn clamp_float(r: Rect, bounds: Rect) -> Rect {
937+ let x = if r.w <= bounds.w {
938+ r.x.clamp(bounds.x, bounds.right() - r.w)
939+ } else {
940+ let strip = STRIP.min(bounds.w);
941+ r.x.clamp(bounds.x + strip - r.w, bounds.right() - strip)
942+ };
943+ let y = if r.h <= bounds.h {
944+ r.y.clamp(bounds.y, bounds.bottom() - r.h)
945+ } else {
946+ r.y.clamp(bounds.y, bounds.bottom() - STRIP.min(bounds.h))
947+ };
948+ Rect::new(x, y, r.w, r.h)
949+}
950+
921951 /// Keeps a rect at the same relative spot when it changes output.
922952 fn translate(r: Rect, from: Rect, to: Rect) -> Rect {
923953 let sx = if from.w > 0.0 { to.w / from.w } else { 1.0 };
Modified file: crates/layout/src/tests.rs+12 additions
@@ -629,3 +629,15 @@ fn urgency_skips_the_focused_window_and_ends_with_focus() {
629629 l.focus(b);
630630 assert!(!l.window(b).unwrap().urgent);
631631 }
632+
633+#[test]
634+fn oversized_floats_keep_their_top_edge_and_a_strip() {
635+ let bounds = Rect::new(0.0, 0.0, 1000.0, 600.0);
636+ let huge = Rect::new(-5000.0, -300.0, 3000.0, 2000.0);
637+ let r = clamp_float(huge, bounds);
638+ assert_eq!((r.x, r.y), (-2936.0, 0.0));
639+ let r = clamp_float(Rect::new(5000.0, 5000.0, 3000.0, 2000.0), bounds);
640+ assert_eq!((r.x, r.y), (936.0, 536.0));
641+ let fits = clamp_float(Rect::new(900.0, 500.0, 300.0, 200.0), bounds);
642+ assert_eq!((fits.x, fits.y), (700.0, 400.0));
643+}
Modified file: crates/monolith/src/input.rs+30 additions−17 deletions
@@ -2,7 +2,7 @@
22 //! into these calls, with pointer positions in global logical coordinates.
33
44 use crate::state::State;
5-use monolith_layout::{Rect, WindowId};
5+use monolith_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};
@@ -244,7 +244,6 @@ impl State {
244244
245245 const BTN_LEFT: u32 = 0x110;
246246 const BTN_RIGHT: u32 = 0x111;
247-const MIN_SIZE: f64 = 32.0;
248247
249248 /// The sides of a window a resize moves.
250249 #[derive(Clone, Copy, Default)]
@@ -310,6 +309,7 @@ pub struct Drag {
310309 window: WindowId,
311310 from: Point<f64, Logical>,
312311 rect: Rect,
312+ bounds: Rect,
313313 /// None moves the window.
314314 edges: Option<Edges>,
315315 /// The client's button, whose release is still the client's to see.
@@ -345,12 +345,11 @@ impl State {
345345 }
346346
347347 fn begin_drag(state: &mut State, id: WindowId, edges: Option<Edges>, button: Option<u32>) -> bool {
348- let Some(rect) = state
349- .wm
350- .layout
348+ let layout = &state.wm.layout;
349+ let Some((rect, bounds)) = layout
351350 .window(id)
352351 .filter(|w| w.floating && !w.fullscreen)
353- .and_then(|w| w.float_rect)
352+ .and_then(|w| Some((w.float_rect?, layout.output(w.output?)?.usable())))
354353 else {
355354 return false;
356355 };
@@ -358,20 +357,33 @@ fn begin_drag(state: &mut State, id: WindowId, edges: Option<Edges>, button: Opt
358357 window: id,
359358 from: state.input.pointer_pos,
360359 rect,
360+ bounds,
361361 edges,
362362 button,
363363 });
364364 true
365365 }
366366
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)
367+/// Moves the dragged edges of an axis, keeping them between lo and hi unless
368+/// the window started beyond them, and the size above the minimum.
369+fn resized(
370+ start: f64,
371+ len: f64,
372+ delta: f64,
373+ edges: (bool, bool),
374+ (lo, hi): (f64, f64),
375+) -> (f64, f64) {
376+ let end = start + len;
377+ match edges {
378+ (true, _) => {
379+ let edge = (start + delta).max(lo.min(start)).min(end - MIN_FLOAT_SIZE);
380+ (edge, end - edge)
381+ }
382+ (_, true) => {
383+ let edge = (end + delta).min(hi.max(end)).max(start + MIN_FLOAT_SIZE);
384+ (start, edge - start)
385+ }
386+ _ => (start, len),
375387 }
376388 }
377389
@@ -383,11 +395,12 @@ fn drag_to(state: &mut State, pos: Point<f64, Logical>) -> bool {
383395 let r = d.rect;
384396 let rect = match d.edges {
385397 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);
398+ let b = d.bounds;
399+ let (x, w) = resized(r.x, r.w, dx, (e.left, e.right), (b.x, b.right()));
400+ let (y, h) = resized(r.y, r.h, dy, (e.top, e.bottom), (b.y, b.bottom()));
388401 Rect::new(x, y, w, h)
389402 }
390- None => Rect::new(r.x + dx, r.y + dy, r.w, r.h),
403+ None => clamp_float(Rect::new(r.x + dx, r.y + dy, r.w, r.h), d.bounds),
391404 };
392405 let id = d.window;
393406 state.wm.layout.set_float_rect(id, rect);