From f988203a7c22d945383efa9609f5c8a09a9506d2 Mon Sep 17 00:00:00 2001 From: srdusr <99972264+srdusr@users.noreply.github.com> Date: Mon, 10 Nov 2025 09:31:00 +0200 Subject: Fix a maximized X11 window overhanging the screen by its own border Reported by the aegis-fc peer session testing srdwm's own layer-shell strut handling: a maximized X11 client sat 4-8px past the right and bottom screen edges whenever its border was nonzero. set_border_width sets the frame's native X11 border-width attribute, which the X server draws outside a window's own declared width/height on all four sides - unlike every other backend's own border in this compositor (rendered as ordinary pixels inside the allocated geometry rect). apply_geometry configured the frame at geometry's own x/y/width/ height verbatim, so a nonzero native border pushed the frame's true visible footprint 2*border_width past every edge of what geometry actually promised. Fixed by shifting the configured origin inward and the configured size down by border_width on both axes (frame_geometry_for, pulled out as a pure function so it's unit-tested without a real X11 connection) - the visible footprint, native border included, now lands exactly on geometry. border_width == 0 reduces to the prior behavior exactly. --- crates/x11/src/platform/mod.rs | 23 +++++++++++++++++++++++ crates/x11/src/platform/tests.rs | 32 ++++++++++++++++++++++++++++++++ crates/x11/src/platform/trait_impl.rs | 22 ++++++++++++++++------ 3 files changed, 71 insertions(+), 6 deletions(-) (limited to 'crates') diff --git a/crates/x11/src/platform/mod.rs b/crates/x11/src/platform/mod.rs index e22b3f7..0f097df 100644 --- a/crates/x11/src/platform/mod.rs +++ b/crates/x11/src/platform/mod.rs @@ -149,6 +149,29 @@ fn rgb_to_pixel((r, g, b): (u8, u8, u8)) -> u32 { ((r as u32) << 16) | ((g as u32) << 8) | (b as u32) } +/// The actual arithmetic behind `Platform::apply_geometry`'s frame +/// placement - pulled out so it's testable without a real X11 +/// connection, the same reasoning `modmask_for_keycode_in_mod_slots` +/// above already gets. +/// +/// X11's native `border_width` window attribute is drawn OUTSIDE a +/// window's own declared width/height, on all four sides, by the X +/// server itself - unlike every other backend's own border (rendered as +/// ordinary pixels *inside* the allocated geometry rect, Wayland's +/// `decoration.rs`, concretely). `geometry` is the true, already-decided +/// on-screen rect (`Window::maximize_geometry`'s own doc comment); this +/// shifts the *configured* origin inward and the *configured* size down +/// by `border_width` on both axes so the window's real, visible footprint +/// (native border included) still lands exactly on `geometry`, instead of +/// spilling `border_width` pixels past every edge of it. Returns +/// `(frame_x, frame_y, frame_width, frame_height)` - the caller applies +/// `band` (the titlebar reservation) on top of `frame_height` separately, +/// same as before this existed. +fn frame_geometry_for(geometry: Rect, border_width: u32) -> (i32, i32, u32, u32) { + let bw = border_width as i32; + (geometry.x + bw, geometry.y + bw, geometry.width.saturating_sub(2 * border_width), geometry.height.saturating_sub(2 * border_width)) +} + pub struct X11Platform { conn: RustConnection, root: XWindow, diff --git a/crates/x11/src/platform/tests.rs b/crates/x11/src/platform/tests.rs index 7af5455..fbae5ff 100644 --- a/crates/x11/src/platform/tests.rs +++ b/crates/x11/src/platform/tests.rs @@ -95,3 +95,35 @@ let usable = usable_rect(full, (1920, 1080), std::iter::empty()); assert_eq!(usable, full); } + + #[test] + fn zero_border_width_leaves_the_frame_geometry_unchanged() { + let geom = Rect::new(0, 0, 1920, 1080); + assert_eq!(frame_geometry_for(geom, 0), (0, 0, 1920, 1080)); + } + + #[test] + fn a_real_border_width_keeps_the_true_footprint_equal_to_the_requested_geometry() { + // Live-reported bug: a maximized window with a native X11 border + // sat 2*border_width pixels past the right/bottom edges, since the + // border draws outside the configured width/height. The true + // on-screen footprint - configured origin minus the border on the + // near side, configured size plus the border on both sides - must + // reproduce the original geometry exactly. + let geom = Rect::new(100, 50, 1920, 1080); + let (x, y, w, h) = frame_geometry_for(geom, 4); + assert_eq!((x - 4, y - 4, w + 8, h + 8), (geom.x, geom.y, geom.width, geom.height)); + } + + #[test] + fn a_maximized_window_no_longer_overhangs_the_monitor_with_a_real_border() { + // The exact live scenario: a window maximized to fill the whole + // monitor must not visually extend past it just because it also + // has a nonzero border. + let monitor = Rect::new(0, 0, 1920, 1080); + let (x, y, w, h) = frame_geometry_for(monitor, 4); + assert_eq!(x - 4, 0, "left edge (including border) must not sit left of the monitor"); + assert_eq!(y - 4, 0, "top edge (including border) must not sit above the monitor"); + assert_eq!(x + w as i32 + 4, 1920, "right edge (including border) must not overhang the monitor"); + assert_eq!(y + h as i32 + 4, 1080, "bottom edge (including border) must not overhang the monitor"); + } diff --git a/crates/x11/src/platform/trait_impl.rs b/crates/x11/src/platform/trait_impl.rs index b45a4aa..6c68e9f 100644 --- a/crates/x11/src/platform/trait_impl.rs +++ b/crates/x11/src/platform/trait_impl.rs @@ -103,18 +103,28 @@ impl Platform for X11Platform { // shrunk by, a titlebar that `redraw_decoration` (below) is // correctly not drawing at all, leaving a blank strip and the // frame visibly not matching what's inside it. - let decorated = self.wm.borrow().window(window).map(|w| w.decorated).unwrap_or(true); + let (decorated, border_width) = { + let wm = self.wm.borrow(); + let w = wm.window(window); + (w.map(|w| w.decorated).unwrap_or(true), w.map(|w| w.border_width).unwrap_or(0)) + }; let band = if decorated { TITLEBAR_HEIGHT } else { 0 }; + // See `frame_geometry_for`'s own doc comment: X11's native border + // is drawn outside a window's declared size, so configuring the + // frame at `geometry` verbatim with a nonzero border pushed the + // frame's true on-screen footprint past every edge of what + // `geometry` actually promised - reported live (found by the + // aegis peer session testing srdwm's own strut handling) as a + // maximized window sitting 4-8px past the right and bottom screen + // edges. + let (frame_x, frame_y, frame_w, frame_h) = frame_geometry_for(geometry, border_width); self.conn - .configure_window( - frame_id, - &ConfigureWindowAux::new().x(geometry.x).y(geometry.y).width(geometry.width).height(geometry.height), - ) + .configure_window(frame_id, &ConfigureWindowAux::new().x(frame_x).y(frame_y).width(frame_w).height(frame_h)) .map_err(err)?; self.conn .configure_window( client_id, - &ConfigureWindowAux::new().x(0).y(band as i32).width(geometry.width).height(geometry.height.saturating_sub(band)), + &ConfigureWindowAux::new().x(0).y(band as i32).width(frame_w).height(frame_h.saturating_sub(band)), ) .map_err(err)?; self.conn.flush().map_err(err)?; -- cgit v1.2.3