Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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,