From 5ef39bd041c64c66f629f9fac53ecc0cdd7aff11 Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Mon, 13 Jul 2026 19:43:00 +0200 Subject: Give every window an outward resize band, so a borderless one can be grabbed Reported as resizing working "from one direction" and feeling "very cheap". The outward half of the grab zone was `border_width` alone. The inward half is deliberately narrow - 3px on an undecorated window, narrowed on purpose because a wider band was stealing clicks from Nemo's own tab-close and minimize buttons near the edges. So a borderless window's entire resize target was that 3px, which is missed far more often than hit and reads as an edge that only sometimes resizes. Turning borders off for the macOS look made it strictly worse: the border had been quietly providing the only outward reach. The fix belongs outside the frame, not inside it. Pixels beyond a window's own edge have no client content to steal a click from, so the band there can be generous regardless of border width. RESIZE_OUTSET is now a floor on the outward reach, with the drawn border used instead when it is wider. macOS and GNOME both let a pointer grab slightly outside a window's visible edge for the same reason. An existing test asserted the old behaviour - that with no border a point outside the frame is not a hit - so it was rewritten to exercise a border wider than the new band rather than have its assertion quietly flipped. Two tests added: that a borderless window is grabbable across the whole band and one pixel past it is not, and that the band reaches all four edges rather than just the left. 533 tests pass, clippy clean. --- crates/core/src/window.rs | 79 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 70 insertions(+), 9 deletions(-) (limited to 'crates') diff --git a/crates/core/src/window.rs b/crates/core/src/window.rs index 272d772..21b7ab4 100644 --- a/crates/core/src/window.rs +++ b/crates/core/src/window.rs @@ -454,6 +454,28 @@ pub const RESIZE_MARGIN: i32 = 6; /// diagonal drag easier to land on a window srdwm itself has no competing /// content in, which is never true here. pub const UNDECORATED_RESIZE_MARGIN: i32 = 3; + +/// How far *outside* a window's own frame the resize grab zone reaches, +/// regardless of border width. +/// +/// The inward margin has to stay narrow - see +/// [`UNDECORATED_RESIZE_MARGIN`] above for the clicks it was stealing from +/// Nemo's own buttons. Pixels outside the frame have no such problem: +/// there is no client content there to take a click from, so the band can +/// be as generous as it needs to be. +/// +/// This used to be `border_width` alone, which meant a borderless window +/// had no outward band at all and its entire resize target was the narrow +/// inward margin - 3px on an undecorated window. Reported as resizing +/// working "from one direction" and feeling "very cheap": a 3px target is +/// missed far more often than it is hit, so it reads as an edge that +/// sometimes resizes rather than one that always does. Turning borders off +/// for the macOS look made it strictly worse, since the border had been +/// quietly providing the only outward reach. +/// +/// macOS and GNOME both let a pointer grab slightly outside a window's +/// visible edge for the same reason. +pub const RESIZE_OUTSET: i32 = 5; /// Top-edge resize margin for a *decorated* window's own titlebar band -- /// unlike [`UNDECORATED_RESIZE_MARGIN`], this has no client content to /// avoid stealing a click from (the whole titlebar band is srdwm's own @@ -557,8 +579,11 @@ impl ResizeEdge { // itself needs no matching change - its margin comparisons // (`x <= frame.x + m`, etc.) already treat anything at or outside // `frame`'s own edge as maximally "near", border pixels included. - let bw = border_width as i32; - let outer = Rect::new(frame.x - bw, frame.y - bw, frame.width + 2 * border_width, frame.height + 2 * border_width); + // Whichever reaches further: the drawn border, or the fixed outward + // grab band. See `RESIZE_OUTSET`. + let bw = (border_width as i32).max(RESIZE_OUTSET); + let reach = bw as u32; + let outer = Rect::new(frame.x - bw, frame.y - bw, frame.width + 2 * reach, frame.height + 2 * reach); if !outer.contains_point(x, y) { return None; } @@ -1283,21 +1308,57 @@ mod tests { // though it's what visually reads as the window's edge. let f = frame(); let (_, cy) = f.center(); - let border_width = 2; - // One pixel into the border strip, past the left edge. + // A border wider than the fixed outward band, so this test is + // actually exercising the border and not `RESIZE_OUTSET`. + let border_width = (RESIZE_OUTSET + 3) as u32; let x = f.x - 1; - assert_eq!(ResizeEdge::hit_test(f, x, cy, true, 0, RESIZE_MARGIN, false, None, false, true), None, "sanity check: with no border, this point really is outside the window"); assert_eq!( ResizeEdge::hit_test(f, x, cy, true, border_width, RESIZE_MARGIN, false, None, false, true), Some(TitlebarHit::Resize(ResizeEdge::Left)), - "one pixel into the actual drawn border must still register as the left edge" + "one pixel into the actual drawn border must register as the left edge" + ); + assert_eq!( + ResizeEdge::hit_test(f, f.x - border_width as i32 + 1, cy, true, border_width, RESIZE_MARGIN, false, None, false, true), + Some(TitlebarHit::Resize(ResizeEdge::Left)), + "the far side of a wide border is still the window's edge" ); - // Just past the border entirely (border_width + 1 outside frame) is - // still nothing - the fix widens the dead zone's boundary, it - // doesn't remove it. + // Past the border entirely: the grab zone has a boundary, it is + // just further out than the frame. assert_eq!(ResizeEdge::hit_test(f, f.x - border_width as i32 - 1, cy, true, border_width, RESIZE_MARGIN, false, None, false, true), None); } + /// A borderless window still has somewhere to grab. Before + /// `RESIZE_OUTSET` the outward band was `border_width` alone, so + /// turning borders off left only the inward margin - 3px on an + /// undecorated window, which is missed more often than hit. + #[test] + fn a_borderless_window_can_still_be_grabbed_from_outside_its_edge() { + let f = frame(); + let (_, cy) = f.center(); + for dx in 1..=RESIZE_OUTSET { + assert_eq!( + ResizeEdge::hit_test(f, f.x - dx, cy, false, 0, RESIZE_MARGIN, false, None, false, true), + Some(TitlebarHit::Resize(ResizeEdge::Left)), + "{dx}px outside a borderless window's left edge must still resize" + ); + } + assert_eq!( + ResizeEdge::hit_test(f, f.x - RESIZE_OUTSET - 1, cy, false, 0, RESIZE_MARGIN, false, None, false, true), + None, + "one pixel past the band is not the window" + ); + } + + #[test] + fn the_outward_band_reaches_every_edge_not_just_the_left() { + let f = frame(); + let (cx, cy) = f.center(); + let probe = |x, y| ResizeEdge::hit_test(f, x, y, false, 0, RESIZE_MARGIN, false, None, false, true); + assert_eq!(probe(f.right() + RESIZE_OUTSET - 1, cy), Some(TitlebarHit::Resize(ResizeEdge::Right))); + assert_eq!(probe(cx, f.y - RESIZE_OUTSET + 1), Some(TitlebarHit::Resize(ResizeEdge::Top))); + assert_eq!(probe(cx, f.bottom() + RESIZE_OUTSET - 1), Some(TitlebarHit::Resize(ResizeEdge::Bottom))); + } + #[test] fn corner_resize_reaches_further_than_a_plain_edge_would() { // Reported live: corners felt like they had no priority over sides. -- cgit v1.2.3