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

commit4215d66519a821aa1626fa9cb675532182d79ad5
parentab7653e3cd
authorLucas Galante <[email protected]>
date2026-09-17 14:05
feat(borders): slower handle fade, and the ring fades OUT on release

`BORDER_FADE_STEP` 0.25 → 0.15 per 16 ms tick: the ring now eases in
over roughly half a second instead of a fifth.

Releasing Super (or leaving overview, or losing focus) cut the ring
away in one frame: `draw_borders` disabled every handle node outright
the moment the mode was off, while `step_border_fade` was still
easing the reveal toward zero on a ring nobody drew. The ring now
stays drawn while any reveal is above zero — a fading ring is
decoration — and only the invisible catcher rects are gated on the
mode being live, so a ring on its way out is never a grab. Measured
in a shadow through a virtual-keyboard Super hold: the ring pixel
ramps up over ~500 ms after the press and back down over ~500 ms
after the release.

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

 src/server/window.rs | 27 +++++++++++++++++----------
 1 file changed, 17 insertions(+), 10 deletions(-)

diff --git a/src/server/window.rs b/src/server/window.rs
index 20445f1..64eafb9 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -173,7 +173,7 @@ impl BorderElement {
 
 /// Per-frame step of the hover fade, as a fraction of the remaining distance
 /// to the target (the same exponential-approach shape the viewport pan uses).
-pub const BORDER_FADE_STEP: f32 = 0.25;
+pub const BORDER_FADE_STEP: f32 = 0.15;
 /// Below this the fade is treated as finished and snapped to its target.
 pub const BORDER_FADE_EPSILON: f32 = 0.004;
 
@@ -4033,12 +4033,19 @@ impl Window {
             let (cw, ch) = (content.width, content.height);
             // A window thinner than two bands has no interior left for a
             // ring; drawing one would be a solid block over the whole window.
-            let handles_on = in_overview
+            // Live handles: the mode is on and this is the focused window.
+            // Focused-only, like the reveal in step_border_fade: without
+            // this the invisible catcher rects would keep intercepting
+            // scene hits on windows whose ring is not even drawn.
+            let handles_live = in_overview && self.is_seat_focused();
+            // Drawn handles: live, OR still fading out — releasing Super (or
+            // leaving overview, or losing focus) eases the ring away instead
+            // of cutting it, so the ring stays drawn while any reveal is
+            // above zero. The catchers below are gated on `handles_live`
+            // alone: a fading ring is decoration, never a grab.
+            let fading_out = !handles_live && self.border_reveal.iter().any(|&a| a > 0.0);
+            let handles_on = (handles_live || fading_out)
                 && window_takes_handles(self_ptr)
-                // Focused-only, like the reveal in step_border_fade: without
-                // this the invisible catcher rects would keep intercepting
-                // scene hits on windows whose ring is not even drawn.
-                && self.is_seat_focused()
                 && !is_virtual_border
                 && bw > 0
                 && (cw as f64 * sc) >= 12.0
@@ -4084,13 +4091,13 @@ impl Window {
             // to split a gap SHARED with a neighbouring window, and an inside
             // ring shares nothing.
             let b = ffi::wlr_box { x: 0, y: 0, width: bw_u, height: ch };
-            apply(self.border.left, b, &transparent, true);
+            apply(self.border.left, b, &transparent, handles_live);
             let b = ffi::wlr_box { x: cw - bw_u, y: 0, width: bw_u, height: ch };
-            apply(self.border.right, b, &transparent, true);
+            apply(self.border.right, b, &transparent, handles_live);
             let b = ffi::wlr_box { x: bw_u, y: 0, width: cw - 2 * bw_u, height: bw_u };
-            apply(self.border.top, b, &transparent, true);
+            apply(self.border.top, b, &transparent, handles_live);
             let b = ffi::wlr_box { x: bw_u, y: ch - bw_u, width: cw - 2 * bw_u, height: bw_u };
-            apply(self.border.bottom, b, &transparent, true);
+            apply(self.border.bottom, b, &transparent, handles_live);
 
             let layout = &(*self.server).wm.layout;
             // The ring hugs the window's own silhouette, so its outer arc IS