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

commit259410a8641e60c3a56c15c5a76a6db295c1f6c3
parentd1212969de
authorLucas Galante <[email protected]>
date2026-09-04 12:37
fix(handles): highlight the zone under the pointer, not a mirrored or stale one

Two bugs stacked up so that hovering a resize handle in overview lit the
wrong piece of the ring.

The shader mirrored the zones vertically. frame.frag flipped its box-local
coordinate to y-up "to match corner_dist", then read that y as distance
from the top — so ZONE_TOP was painted along the bottom edge and each top
corner along the corner below it. The SDF never showed it: one radius on
all four corners is symmetric under the flip. gl_FragCoord minus the box
position is already top-down here (the pass renders under FLIPPED_180, as
droplet.frag notes), so the zone logic now uses it unflipped, and the
popover exclusion compares its top-left-origin rect directly instead of
converting.

The ring did not repaint on a hover change. step_border_fade called
draw_borders only when a reveal value moved, and in overview every zone
already sits at full reveal, so a hover swap changed nothing it looked at.
The frame's `hovered` uniform was only refreshed when an unrelated commit
ran draw_borders, leaving the highlight one hover behind. The window now
remembers the zone the ring was last drawn with (border_hover_drawn) and
the fade step treats a mismatch as a change.

Verified in headless shadows at output scale 1 and 2: hovering the top,
bottom and left edges and the top-left and bottom-right corners lights
exactly that zone in color_hover.

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

 CLAUDE.md                                     | 10 ++++++++-
 scenefx/render/fx_renderer/shaders/frame.frag | 29 ++++++++++++++-------------
 src/server/window.rs                          | 17 ++++++++++++++++
 3 files changed, 41 insertions(+), 15 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 1297dbf..c7eea99 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -359,7 +359,15 @@ where the old band sat outside them.
   Each gap therefore separates two thin tips: the frame pinches at every
   join and thickens toward every piece's middle.
   The gap notches sever only the band; a groove across a pad would read as
-  damage. Both `get_border_zone` and the drawing treat a pad as its corner's
+  damage. The shader's zone logic works in TOP-DOWN box-local coordinates
+  (`gl_FragCoord` minus the box position, unflipped): the `corner_dist` SDF
+  flips its own copy, and mirroring the zone coordinate the same way once
+  swapped every zone label vertically — the top edge lit the bottom. And a
+  hover swap must repaint even when no reveal value moves: in overview the
+  ring is already fully revealed, so `step_border_fade` compares the hovered
+  zone against the one last drawn (`border_hover_drawn`), or the shader
+  keeps showing the previous zone until an unrelated commit repaints.
+  Both `get_border_zone` and the drawing treat a pad as its corner's
   zone — its tip reaches past the band, so the hit test carries a matching
   corner-disc check. The corner run (`corner_length`) clamps PER SIDE at 0.45
   of that side's length, in the shader and the hit test alike: two corner
diff --git a/scenefx/render/fx_renderer/shaders/frame.frag b/scenefx/render/fx_renderer/shaders/frame.frag
index 618a430..5d063ee 100644
--- a/scenefx/render/fx_renderer/shaders/frame.frag
+++ b/scenefx/render/fx_renderer/shaders/frame.frag
@@ -82,23 +82,24 @@ void main() {
     }
     float depth = -dist; // 0 at the silhouette, growing inward
 
-    // Rect-local position, in the SAME y-flipped space corner_dist works in
-    // (see corner_alpha.frag: it flips y after subtracting `position`).
-    // Without the flip the zone logic is upside down relative to the SDF.
+    // Rect-local position, TOP-DOWN: gl_FragCoord minus the box position is
+    // already y-down box-local here (the pass renders under a FLIPPED_180
+    // projection, so fragment row 0 is the top of the buffer — see the same
+    // note in droplet.frag). Everything below that names a side — dt/db,
+    // the corner pads, the zones — reads y as "distance from the top", so
+    // this must NOT be flipped the way corner_dist flips its own copy: the
+    // SDF is symmetric under that flip (one radius for all four corners),
+    // but the zone labels are not, and flipping here mirrored them
+    // vertically — hovering the top edge lit the bottom one, and each top
+    // corner lit the corner below it.
     vec2 p = gl_FragCoord.xy - position;
-    p.y = size.y - p.y;
 
-    // A client popover owns this rect; the ring yields to it wholesale.
-    // The rect arrives top-left-origin (y down); p here is y-UP — the flip
-    // above mirrors gl_FragCoord's orientation rather than cancelling it —
-    // so the rect's y converts. (Found empirically: an un-flipped compare
-    // cut the bottom-left when the menu was top-left, with x exact.)
+    // A client popover owns this rect; the ring yields to it wholesale. The
+    // rect arrives top-left-origin (y down), the same space as p.
     if (exclusion.z > 0.0 && exclusion.w > 0.0
-            && p.x >= exclusion.x && p.x < exclusion.x + exclusion.z) {
-        float ey = size.y - exclusion.y - exclusion.w;
-        if (p.y >= ey && p.y < ey + exclusion.w) {
-            discard;
-        }
+            && p.x >= exclusion.x && p.x < exclusion.x + exclusion.z
+            && p.y >= exclusion.y && p.y < exclusion.y + exclusion.w) {
+        discard;
     }
 
     // Which side owns this fragment: whichever edge it sits nearer. The
diff --git a/src/server/window.rs b/src/server/window.rs
index 0785f31..04add1a 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -398,6 +398,13 @@ pub struct Window {
     /// The border zone the pointer is over (set by cursor.rs); that segment
     /// draws in `hover_color` while set.
     pub hovered_border_element: Option<BorderElement>,
+    /// The zone the ring was last DRAWN with, so `step_border_fade` can tell
+    /// a hover change from a settled ring. In overview every zone already
+    /// sits at full reveal, so a hover swap moves no reveal value at all —
+    /// and a step keyed on reveal alone never repainted, leaving the shader
+    /// on whatever zone the last unrelated commit happened to push. The
+    /// highlight lagged one hover behind: "the wrong handle lights up".
+    pub border_hover_drawn: Option<BorderElement>,
     /// Per-zone reveal factor, 0.0 (fully hidden) to 1.0 (fully drawn),
     /// indexed by `BorderElement::index`. Borders rest invisible and only the
     /// zone under the pointer fades in. Deliberately NOT part of
@@ -706,6 +713,7 @@ impl Window {
                 tree: border_tree,
             },
             hovered_border_element: None,
+            border_hover_drawn: None,
             border_reveal: [0.0; 8],
             decorations_above: std::mem::zeroed(),
             decorations_above_tree,
@@ -3458,6 +3466,11 @@ impl Window {
             moving = true;
             changed = true;
         }
+        // A hover swap on a fully revealed ring moves nothing above, but the
+        // shader still has to be told which zone to paint.
+        if self.hovered_border_element != self.border_hover_drawn {
+            changed = true;
+        }
         if changed {
             self.draw_borders();
         }
@@ -3806,6 +3819,9 @@ impl Window {
                 && (cw as f64 * sc) >= 12.0
                 && (ch as f64 * sc) >= 12.0;
             if !handles_on {
+                // Nothing is drawn, so nothing is stale: without this the
+                // fade step would see a mismatch and repaint every tick.
+                self.border_hover_drawn = self.hovered_border_element;
                 for r in [self.border.left, self.border.right, self.border.top, self.border.bottom] {
                     ffi::wlr_scene_node_set_enabled(r as *mut ffi::wlr_scene_node, false);
                 }
@@ -3904,6 +3920,7 @@ impl Window {
                 .hovered_border_element
                 .map(|e| e.index() as f32)
                 .unwrap_or(-1.0);
+            self.border_hover_drawn = self.hovered_border_element;
             ffi::wlr_scene_frame_set_hover(
                 self.border.frame,
                 hovered,