git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit50a6036fcf3e305ae1ed3974725318bacf8b4652
parent1f923d9a64
authorLucas Galante <[email protected]>
date2026-07-28 10:00
feat: doubled border band, foam-clipped between neighbors, rounded-L corners

The interactive band now runs twice the configured border width
(border_band_width(), HOVER_BAND_MIN floor raised to 16) — one shared
formula for the visual segments, the hit catchers, and the pointer
zones (get_border_zone), so they can never drift. Where two windows'
bands would overlap across a gap, each stops at the gap's midline
(border_side_extents(), per side; stacked windows keep full bands) —
clipping the catchers too is what routes each half of a shared gap to
the nearer window in the scene hit-test. Corner zones are now one
rounded-L rect each (outer radius r+bw concentric with the window's
rounding, content quadrant clipped at r; the two-rect L's V slots are
retired), and corner lengths scale with the doubled band, keeping at
least half a band of straight arm.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/server/cursor.rs |  23 ++---
 src/server/window.rs | 240 +++++++++++++++++++++++++++++++++++++++++++++------
 2 files changed, 223 insertions(+), 40 deletions(-)

diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index bb6f055..a08faf7 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -2305,10 +2305,7 @@ pub enum BorderZone {
     Resize(crate::window::Edges),
 }
 
-/// Floor on the border grab/reveal band, in layout pixels. Borders are
-/// invisible until hovered, so the band is the only thing to aim at; a 2px
-/// target would be unusable.
-pub const HOVER_BAND_MIN: f64 = 8.0;
+pub use crate::window::HOVER_BAND_MIN;
 
 pub unsafe fn get_border_zone(window: *mut crate::window::Window, lx: f64, ly: f64) -> BorderZone {
     if (*(*window).server).wm.mode == crate::window_manager::WindowManagerMode::Overview {
@@ -2325,18 +2322,12 @@ pub unsafe fn get_border_zone(window: *mut crate::window::Window, lx: f64, ly: f
         return BorderZone::None;
     }
 
-    // Width-only, matching draw_borders: visible borders are the zones at
-    // their real width regardless of the ssd flag.
-    //
-    // Borders rest invisible and are revealed by hovering this band, so it
-    // doubles as the reveal target and must stay comfortable to hit: a
-    // narrow configured border still gets a HOVER_BAND_MIN-wide grab zone.
-    let is_virtual_border = (*window).rendering_requested.border.width == 0;
-    let bw_unscaled = if is_virtual_border {
-        HOVER_BAND_MIN
-    } else {
-        ((*window).rendering_requested.border.width as f64).max(HOVER_BAND_MIN)
-    };
+    // The shared band formula (doubled width, floored) — matching
+    // draw_borders' catchers and segments, so the zones and the visuals
+    // cannot drift. Foam ownership between neighboring windows needs no
+    // handling here: the catchers are clipped at the walls, so the scene
+    // hit-test already hands each half of a shared gap to the nearer window.
+    let bw_unscaled = crate::window::border_band_width((*window).rendering_requested.border.width);
     if bw_unscaled <= 0.0 {
         return BorderZone::None;
     }
diff --git a/src/server/window.rs b/src/server/window.rs
index 3c3f442..80d89ca 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -160,12 +160,32 @@ pub const BORDER_FADE_EPSILON: f32 = 0.004;
 /// the band width, so a corner is at least its diagonal square.
 pub fn border_corner_len(bw: f64, configured: i32) -> f64 {
     if configured > 0 {
-        (configured as f64).max(bw)
+        // Configured lengths predate the band doubling — scale them the same
+        // way, and keep at least half a band of straight arm (arm = cl − bw)
+        // so a corner can never collapse to a bare square. (corner_length=16
+        // with the doubled 16px band used to yield arm = 0: corners vanished.)
+        (configured as f64 * 2.0).max(1.5 * bw)
     } else {
-        (2.0 * bw).max(16.0)
+        // 3× band: the corner arms reach well down each edge (they also
+        // carry the rounded-corner arc, which eats into the straight run).
+        (3.0 * bw).max(24.0)
     }
 }
 
+/// Floor on the border grab/reveal band, in unscaled layout pixels. Borders
+/// rest invisible until hovered, so the band is the only thing to aim at; a
+/// narrow target would be unusable.
+pub const HOVER_BAND_MIN: f64 = 16.0;
+
+/// Effective interactive border band width (unscaled): the configured border
+/// width DOUBLED — the hover/grab band runs twice the classic border — with
+/// the HOVER_BAND_MIN floor. Shared by the visual segments (draw_borders),
+/// the hit catchers, and the pointer zones (cursor::get_border_zone) so they
+/// can never drift apart.
+pub fn border_band_width(configured_width: u32) -> f64 {
+    (configured_width as f64 * 2.0).max(HOVER_BAND_MIN)
+}
+
 // Indices into BorderRects.segments: 4 edge bars + 2 L-arm rects per corner.
 const SEG_TOP: usize = 0;
 const SEG_BOTTOM: usize = 1;
@@ -2391,6 +2411,87 @@ impl Window {
         moving
     }
 
+    /// Outward extent (unscaled px) the interactive border may reach on each
+    /// side — `[left, right, top, bottom]` — after the foam rule against the
+    /// other windows: where two windows' bands would overlap across a gap,
+    /// each band stops at the gap's midline (the ramp key-ring behavior,
+    /// rectangular — the wall is equidistant from the two content edges).
+    /// Stacked windows (content rects overlapping) do not clip each other,
+    /// mirroring the rings' degenerate-distance guard. Per-side, not
+    /// per-span: one near neighbor claims the whole facing side.
+    pub unsafe fn border_side_extents(&self, band_unscaled: f64) -> [f64; 4] {
+        let scale = if self.scale > 0.0 { self.scale } else { 1.0 };
+        let band = band_unscaled * scale;
+        let ax0 = self.box_geom.x as f64;
+        let ay0 = self.box_geom.y as f64;
+        let ax1 = ax0 + self.box_geom.width as f64 * scale;
+        let ay1 = ay0 + self.box_geom.height as f64 * scale;
+        let mut ext = [band; 4]; // left, right, top, bottom (layout px)
+
+        let self_ptr = self as *const Window as *mut Window;
+        for &other in (*self.server).wm.windows.iter() {
+            if other.is_null() || other == self_ptr {
+                continue;
+            }
+            let o = &*other;
+            if o.closed
+                || o.minimized
+                || o.rendering_requested.hidden
+                || o.rendering_requested.circular
+                || matches!(
+                    o.tiling_mode,
+                    crate::tiling::TilingMode::Popup
+                        | crate::tiling::TilingMode::Fullscreen
+                        | crate::tiling::TilingMode::Status
+                )
+                || o.is_status_bar()
+                || o.is_wallpaper()
+            {
+                continue;
+            }
+            let os = if o.scale > 0.0 { o.scale } else { 1.0 };
+            let bx0 = o.box_geom.x as f64;
+            let by0 = o.box_geom.y as f64;
+            let bx1 = bx0 + o.box_geom.width as f64 * os;
+            let by1 = by0 + o.box_geom.height as f64 * os;
+            // Stacked: keep the full band.
+            if bx0 < ax1 && bx1 > ax0 && by0 < ay1 && by1 > ay0 {
+                continue;
+            }
+            let ob = border_band_width(o.rendering_requested.border.width) * os;
+            // Spans (including bands) must overlap for a wall to exist.
+            let v_overlap = by0 - ob < ay1 + band && by1 + ob > ay0 - band;
+            let h_overlap = bx0 - ob < ax1 + band && bx1 + ob > ax0 - band;
+            if v_overlap {
+                if bx0 >= ax1 {
+                    let gap = bx0 - ax1;
+                    if gap < band + ob {
+                        ext[1] = ext[1].min((gap / 2.0).max(0.0));
+                    }
+                } else if bx1 <= ax0 {
+                    let gap = ax0 - bx1;
+                    if gap < band + ob {
+                        ext[0] = ext[0].min((gap / 2.0).max(0.0));
+                    }
+                }
+            }
+            if h_overlap {
+                if by0 >= ay1 {
+                    let gap = by0 - ay1;
+                    if gap < band + ob {
+                        ext[3] = ext[3].min((gap / 2.0).max(0.0));
+                    }
+                } else if by1 <= ay0 {
+                    let gap = ay0 - by1;
+                    if gap < band + ob {
+                        ext[2] = ext[2].min((gap / 2.0).max(0.0));
+                    }
+                }
+            }
+        }
+        [ext[0] / scale, ext[1] / scale, ext[2] / scale, ext[3] / scale]
+    }
+
     pub unsafe fn draw_borders(&mut self) {
         let requested = &self.rendering_requested;
 
@@ -2454,7 +2555,15 @@ impl Window {
         let clip_empty = requested.content_clip.width == 0 && requested.content_clip.height == 0;
         if clip_empty || ffi::wlr_box_intersection(&mut intersect, &content, &requested.content_clip) {
             let border = &requested.border;
-            let band = if is_virtual_border { 8 } else { border.width as i32 };
+            // The interactive band (doubled configured width, floored), foam-
+            // clipped per side against neighboring windows' bands. Clipping
+            // the CATCHERS too is what routes the shared half of a gap to the
+            // nearer window in the scene hit-test.
+            let band_f = border_band_width(border.width);
+            let band = band_f as i32;
+            let ext = self.border_side_extents(band_f);
+            let (ext_l, ext_r, ext_t, ext_b) =
+                (ext[0] as i32, ext[1] as i32, ext[2] as i32, ext[3] as i32);
             let transparent = [0.0f32; 4];
 
             // The rounded-frame path used to leave radius/clip state on the
@@ -2487,14 +2596,15 @@ impl Window {
                 ffi::wlr_scene_rect_set_color(rect, color.as_ptr());
             };
 
-            // Full-band hit catchers: sides span the corners vertically.
-            let b = ffi::wlr_box { x: -band, y: -band, width: band, height: content.height + 2 * band };
+            // Full-band hit catchers (foam-clipped): sides span the corners
+            // vertically.
+            let b = ffi::wlr_box { x: -ext_l, y: -ext_t, width: ext_l, height: content.height + ext_t + ext_b };
             apply(self.border.left, b, &transparent, true);
-            let b = ffi::wlr_box { x: content.width, y: -band, width: band, height: content.height + 2 * band };
+            let b = ffi::wlr_box { x: content.width, y: -ext_t, width: ext_r, height: content.height + ext_t + ext_b };
             apply(self.border.right, b, &transparent, true);
-            let b = ffi::wlr_box { x: 0, y: -band, width: content.width, height: band };
+            let b = ffi::wlr_box { x: 0, y: -ext_t, width: content.width, height: ext_t };
             apply(self.border.top, b, &transparent, true);
-            let b = ffi::wlr_box { x: 0, y: content.height, width: content.width, height: band };
+            let b = ffi::wlr_box { x: 0, y: content.height, width: content.width, height: ext_b };
             apply(self.border.bottom, b, &transparent, true);
 
             if is_virtual_border {
@@ -2504,7 +2614,7 @@ impl Window {
                 return;
             }
 
-            let bw = border.width as i32;
+            let bw = band;
             let layout = &(*self.server).wm.layout;
             let cl = border_corner_len(bw as f64, layout.border_corner_length) as i32;
             let g = layout.border_segment_gap;
@@ -2515,6 +2625,30 @@ impl Window {
             let bar_y = cl - bw + g;
             let bar_h = content.height + 2 * bw - 2 * cl - 2 * g;
 
+            // Foam clip for a visible segment: shave each outward flank the
+            // segment occupies back to that side's allowed extent. (l/r shift
+            // or shrink horizontally, t/b vertically; apply() drops boxes
+            // that clip away entirely.)
+            let clip_seg = |mut bx: ffi::wlr_box, l: bool, r: bool, t: bool, b: bool| -> ffi::wlr_box {
+                if l {
+                    let d = bw - ext_l;
+                    bx.x += d;
+                    bx.width -= d;
+                }
+                if r {
+                    bx.width -= bw - ext_r;
+                }
+                if t {
+                    let d = bw - ext_t;
+                    bx.y += d;
+                    bx.height -= d;
+                }
+                if b {
+                    bx.height -= bw - ext_b;
+                }
+                bx
+            };
+
             // Borders rest invisible; a zone is drawn only as far as its
             // reveal factor has faded in. Channels are premultiplied alpha, so
             // scaling all four by the factor is the correct fade.
@@ -2530,25 +2664,83 @@ impl Window {
             let e = &border.edges;
             use BorderElement::*;
 
-            let segs: [(usize, ffi::wlr_box, BorderElement, bool); 12] = [
-                (SEG_TOP, ffi::wlr_box { x: bar_x, y: -bw, width: bar_w, height: bw }, Top, e.top),
-                (SEG_BOTTOM, ffi::wlr_box { x: bar_x, y: content.height, width: bar_w, height: bw }, Bottom, e.bottom),
-                (SEG_LEFT, ffi::wlr_box { x: -bw, y: bar_y, width: bw, height: bar_h }, Left, e.left),
-                (SEG_RIGHT, ffi::wlr_box { x: content.width, y: bar_y, width: bw, height: bar_h }, Right, e.right),
-                (SEG_TL_H, ffi::wlr_box { x: -bw, y: -bw, width: cl, height: bw }, TopLeft, e.top && e.left),
-                (SEG_TL_V, ffi::wlr_box { x: -bw, y: 0, width: bw, height: arm }, TopLeft, e.top && e.left),
-                (SEG_TR_H, ffi::wlr_box { x: content.width + bw - cl, y: -bw, width: cl, height: bw }, TopRight, e.top && e.right),
-                (SEG_TR_V, ffi::wlr_box { x: content.width, y: 0, width: bw, height: arm }, TopRight, e.top && e.right),
-                (SEG_BL_H, ffi::wlr_box { x: -bw, y: content.height, width: cl, height: bw }, BottomLeft, e.bottom && e.left),
-                (SEG_BL_V, ffi::wlr_box { x: -bw, y: content.height - arm, width: bw, height: arm }, BottomLeft, e.bottom && e.left),
-                (SEG_BR_H, ffi::wlr_box { x: content.width + bw - cl, y: content.height, width: cl, height: bw }, BottomRight, e.bottom && e.right),
-                (SEG_BR_V, ffi::wlr_box { x: content.width, y: content.height - arm, width: bw, height: arm }, BottomRight, e.bottom && e.right),
+            // Edge bars: box, element, edge-enable, then which outward flank
+            // each occupies (left, right, top, bottom) for the foam clip.
+            #[rustfmt::skip]
+            let bars: [(usize, ffi::wlr_box, BorderElement, bool, (bool, bool, bool, bool)); 4] = [
+                (SEG_TOP, ffi::wlr_box { x: bar_x, y: -bw, width: bar_w, height: bw }, Top, e.top, (false, false, true, false)),
+                (SEG_BOTTOM, ffi::wlr_box { x: bar_x, y: content.height, width: bar_w, height: bw }, Bottom, e.bottom, (false, false, false, true)),
+                (SEG_LEFT, ffi::wlr_box { x: -bw, y: bar_y, width: bw, height: bar_h }, Left, e.left, (true, false, false, false)),
+                (SEG_RIGHT, ffi::wlr_box { x: content.width, y: bar_y, width: bw, height: bar_h }, Right, e.right, (false, true, false, false)),
             ];
-            for (idx, bx, elem, enabled) in segs {
+            for (idx, bx, elem, enabled, (fl, fr, ft, fb)) in bars {
                 // A fully-faded-out zone is disabled outright rather than
                 // drawn transparent, so it costs nothing while at rest.
                 let enabled = enabled && self.border_reveal[elem.index()] > 0.0;
-                apply(self.border.segments[idx], bx, &color_for(elem), enabled);
+                apply(self.border.segments[idx], clip_seg(bx, fl, fr, ft, fb), &color_for(elem), enabled);
+            }
+
+            // Corner zones: one rounded-L rect per corner (the legacy
+            // two-rect L's V slots are retired). The outer corner carries
+            // radius r_in + bw — concentric with the window's own rounding —
+            // and the content quadrant is clipped away at r_in, so the band
+            // follows the rounded window corner. Radii and clip are set in
+            // PHYSICAL px on the final (foam-clipped) box; r_in == 0
+            // degenerates to the old square L.
+            let rr = |v: f64| -> u16 { (v.max(0.0) as i32).clamp(0, u16::MAX as i32) as u16 };
+            let r_in = border.corner_radius as i32;
+            let r_out = if r_in > 0 { r_in + bw } else { 0 };
+            let s = self.scale;
+            let ri = rr(r_in as f64 * s);
+            let ro = rr(r_out as f64 * s);
+            let no_radii = ffi::fx_corner_radii { top_left: 0, top_right: 0, bottom_right: 0, bottom_left: 0 };
+
+            // (visible slot, retired slot, element, enabled, box,
+            //  corner index: 0=TL 1=TR 2=BR 3=BL)
+            #[rustfmt::skip]
+            let corners: [(usize, usize, BorderElement, bool, ffi::wlr_box, usize); 4] = [
+                (SEG_TL_H, SEG_TL_V, TopLeft, e.top && e.left,
+                 ffi::wlr_box { x: -ext_l, y: -ext_t, width: cl - bw + ext_l, height: cl - bw + ext_t }, 0),
+                (SEG_TR_H, SEG_TR_V, TopRight, e.top && e.right,
+                 ffi::wlr_box { x: content.width + bw - cl, y: -ext_t, width: cl - bw + ext_r, height: cl - bw + ext_t }, 1),
+                (SEG_BR_H, SEG_BR_V, BottomRight, e.bottom && e.right,
+                 ffi::wlr_box { x: content.width + bw - cl, y: content.height - arm, width: cl - bw + ext_r, height: arm + ext_b }, 2),
+                (SEG_BL_H, SEG_BL_V, BottomLeft, e.bottom && e.left,
+                 ffi::wlr_box { x: -ext_l, y: content.height - arm, width: cl - bw + ext_l, height: arm + ext_b }, 3),
+            ];
+            for (slot, retired, elem, enabled, bx, which) in corners {
+                ffi::wlr_scene_node_set_enabled(self.border.segments[retired] as *mut ffi::wlr_scene_node, false);
+                let rect = self.border.segments[slot];
+                let enabled = enabled && self.border_reveal[elem.index()] > 0.0;
+                apply(rect, bx, &color_for(elem), enabled);
+                if !enabled || bx.width <= 0 || bx.height <= 0 {
+                    continue;
+                }
+                let (pw, ph) = ((bx.width as f64 * s) as i32, (bx.height as f64 * s) as i32);
+                // The content quadrant, rect-local physical px: from the
+                // content edges to the box's far sides.
+                let (cx, cy) = match which {
+                    0 => (ext_l, ext_t), // content corner sits ext in from the box origin
+                    1 => (0, ext_t),
+                    2 => (0, 0),
+                    _ => (ext_l, 0),
+                };
+                let (cx, cy) = ((cx as f64 * s) as i32, (cy as f64 * s) as i32);
+                let sub_w = match which { 0 | 3 => pw - cx, _ => pw - ((ext_r as f64 * s) as i32) };
+                let sub_h = match which { 0 | 1 => ph - cy, _ => ph - ((ext_b as f64 * s) as i32) };
+                let mut outer = no_radii;
+                let mut inner = no_radii;
+                match which {
+                    0 => { outer.top_left = ro; inner.top_left = ri; }
+                    1 => { outer.top_right = ro; inner.top_right = ri; }
+                    2 => { outer.bottom_right = ro; inner.bottom_right = ri; }
+                    _ => { outer.bottom_left = ro; inner.bottom_left = ri; }
+                }
+                ffi::wlr_scene_rect_set_corner_radii(rect, outer);
+                ffi::wlr_scene_rect_set_clipped_region(rect, ffi::clipped_region {
+                    area: ffi::wlr_box { x: cx, y: cy, width: sub_w.max(0), height: sub_h.max(0) },
+                    corners: inner,
+                });
             }
         }
     }