git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commitdfc2439be06dd1db5148498098de7ac2cab47da1
parent51f0b5895a
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-28 17:37
fix: the params pane's hover cue is the row label, not a wash over the row

The row wash, moved after the carves in 51f0b58, still lit the hovered
well: 5% white composites in LINEAR light, which lifts a well's dark
shade line from 18 to about 67 while the pane face only moves 46 to 76,
so the hovered well's outline paled as the pointer crossed it. A
pointer sweep over the control column showed exactly that, one well at a
time, with the plate-grouping trace confirming every carve stayed
grouped — the wash itself was the flash.

The hovered row now lifts its LABEL to the dialog's selected-label
colour and paints nothing else: a hover adds no geometry, so it can
neither close the pane plate's carve-grouping window nor composite over a
well's relief. `hover_row` tracking and its scroll re-hover are unchanged.
`a_hover_lifts_the_label_and_adds_no_geometry` asserts the label colours
and that the emitted primitives are identical at rest and on hover.
Verified in a shadow session with the user's project: the well edge
pixel is constant across a 38-position sweep, and the hover diff is the
label's glyphs alone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

 src/widget/container/parameters_bg.rs | 109 +++++++++++++++++-----------------
 1 file changed, 53 insertions(+), 56 deletions(-)

diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 2ddbddf..b830bd4 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -79,15 +79,25 @@ pub struct ParametersBg {
     /// under that line while the value it was reported for still stands.
     code_error_line: Option<usize>,
     mouse_pos: Option<(f32, f32)>,
-    /// The parameter row under the pointer, tinted by `paint_ui` — the
-    /// pane's ONE hover rule, over every control kind. The controls keep
-    /// their own hovers (a spinbox's +/- buttons, a slider's band), but
-    /// under the DE relief style most of them draw none at all — the
-    /// spinbox's row border, the well frames and the dropdown border are
-    /// flat-style chrome the relief branches never paint — so which rows
-    /// answered the pointer depended on the control and the style
-    /// (2026-09-28: a slider lit, the spinbox beside it did not). Set with
-    /// the controls' hover in `hover_controls`, so a scroll moves it too.
+    /// The parameter row under the pointer: its LABEL lifts
+    /// (`own_text_labels`) — the pane's ONE hover rule, over every control
+    /// kind. The controls keep their own hovers (a spinbox's +/- buttons, a
+    /// slider's band), but under the DE relief style most of them draw none
+    /// at all — the spinbox's row border, the well frames and the dropdown
+    /// border are flat-style chrome the relief branches never paint — so
+    /// which rows answered the pointer depended on the control and the
+    /// style (2026-09-28: a slider lit, the spinbox beside it did not). Set
+    /// with the controls' hover in `hover_controls`, so a scroll moves it
+    /// too.
+    ///
+    /// A label, deliberately, and not a wash over the row. A row wash was
+    /// tried the same day and failed twice, both times through the relief:
+    /// painted before the wells it closed the pane plate's carve-grouping
+    /// window and every well in the pane flipped to the overlay shading on
+    /// any hover; painted after them, its 5% white composited in LINEAR
+    /// light, which lifts a well's dark shade line from 18 to ~67 while
+    /// the pane face moves 46 to 76 — the hovered well's outline paled and
+    /// read as the well lighting up. Text stands clear of the relief.
     hover_row: Option<usize>,
     pub sliders: Vec<Option<Adapted<Slider>>>,
     pub float3s: Vec<Option<Adapted<Float3>>>,
@@ -864,19 +874,6 @@ impl ParametersBg {
         })
     }
 
-    /// The hovered row's tint: the dialog's hover-row wash, at the control
-    /// corner radius, laid over the row's carves (see `paint_ui` for why it
-    /// must come after them).
-    fn paint_hover_row(&self, ctx: &mut PaintCtx) {
-        let Some(i) = self.hover_row else { return };
-        let Some(&(x, y, w, h)) = self.get_param_rects().get(i) else { return };
-        if h <= 0.0 {
-            return;
-        }
-        let r = crate::layout::control_corner_radius();
-        ctx.rounded_rect(Rect { x, y, width: w, height: h }, r, (true, true, true, true), [1.0, 1.0, 1.0, 0.05]);
-    }
-
     /// The rows moved under a still pointer: re-hover from where it was.
     fn rehover_after_scroll(&mut self, ui: &mut UiContext) -> bool {
         match self.mouse_pos {
@@ -885,6 +882,12 @@ impl ParametersBg {
         }
     }
 
+    /// The pane's own row label, and the same label under the pointer: the
+    /// dialog's row and selected-row label colours, so a hovered parameter
+    /// reads as the dialog's hovered command does.
+    const LABEL: [u8; 3] = [0xaa, 0xaa, 0xbb];
+    const LABEL_HOVER: [u8; 3] = [0xf4, 0xf4, 0xfa];
+
     fn update_slider_rects(&mut self) {
         // Inline rows hand their control the row less the label column; the
         // row rects themselves (`get_param_rects`) stay the full row, which is
@@ -1012,7 +1015,7 @@ impl ParametersBg {
                     x: r.0,
                     y: r.1 + (r.3 - size) * 0.5,
                     font_size: size,
-                    color: [0xaa, 0xaa, 0xbb],
+                    color: if self.hover_row == Some(i) { Self::LABEL_HOVER } else { Self::LABEL },
                 });
             }
             if ptype.starts_with("slider") {
@@ -1907,16 +1910,6 @@ impl Paint for ParametersBg {
         for (fcx, fcy, fr, fd, fs) in self.section_fillets() {
             ctx.concave_fillet(fcx, fcy, fr, fd, fs, false);
         }
-        // AFTER every carve, never before: the wash is ordinary geometry,
-        // and ordinary geometry painted after the pane plate ends its
-        // carve-grouping window (`window_runner`'s tessellation) — every
-        // recess emitted after it loses the plate as its host and shades
-        // through the overlay fallback instead of the plate's own draw. For
-        // the day it sat above the carves (2026-09-28) hovering ANY row
-        // re-lit every well in the pane: the grouped and fallback shadings
-        // differ visibly (a crisp shade line against a soft lit crest), and
-        // the flip read as "all the wells brighten on hover".
-        self.paint_hover_row(ctx);
         for (scx, scy, sr, sc) in self.spheres() {
             ctx.sphere(scx, scy, sr, &crate::scene::material::Material::from_fill(sc));
         }
@@ -4080,7 +4073,7 @@ mod tests {
         let (px, py) = (250.0, r0.1 + r0.3 * 0.5);
         p.on_cursor_moved(px, py, &mut ctx);
         assert_eq!(hovered(&p), vec![0], "the slider under the pointer hovers");
-        assert_eq!(p.hover_row, Some(0), "and the pane tints its row");
+        assert_eq!(p.hover_row, Some(0), "and the pane lifts its label");
 
         // A wheel over the label column scrolls the pane: a notch moves the
         // TARGET and the rows glide there over the following ticks, so the
@@ -4100,7 +4093,7 @@ mod tests {
             .expect("a row under the pointer after the scroll");
         assert_ne!(now, 0, "a different row is under the pointer");
         assert_eq!(hovered(&p), vec![now], "the hover followed the rows, without a motion");
-        assert_eq!(p.hover_row, Some(now), "the row tint followed too");
+        assert_eq!(p.hover_row, Some(now), "the label lift followed too");
 
         // Off the pane, nothing is hovered.
         p.on_cursor_moved(-9999.0, -9999.0, &mut ctx);
@@ -4108,36 +4101,40 @@ mod tests {
         assert_eq!(p.hover_row, None);
     }
 
-    /// The hover wash is painted after every carve in the pane. Ordinary
-    /// geometry ends the pane plate's carve-grouping window, so a wash
-    /// emitted BEFORE the wells would drop every one of them to the overlay
-    /// shading — which is what happened for a day, and read as every well
-    /// re-lighting whenever any row was hovered.
+    /// The hover cue is the row's LABEL, and nothing else in the pane's
+    /// paint changes with it: a hover adds no geometry at all, so it can
+    /// neither close the pane plate's carve-grouping window (a wash before
+    /// the wells did, and every well flipped shading on any hover) nor
+    /// composite over a well's shade lines (a wash after them did, and the
+    /// hovered well's outline paled). Both shipped for part of 2026-09-28.
     #[test]
-    fn the_hover_wash_comes_after_every_carve() {
-        use crate::scene::paint::Prim;
+    fn a_hover_lifts_the_label_and_adds_no_geometry() {
         let mut ctx = UiContext::new();
         let mut p = panel_with(&[("Name", "x", "text"), ("Count", "3", "spinbox:0:10"), ("Size", "0.5", "slider:0:1")]);
         WidgetHost::set_rect(&mut p, 0.0, 0.0, 300.0, 400.0);
+        let rect = Rect { x: 0.0, y: 0.0, width: 300.0, height: 400.0 };
+        let prims = |p: &Adapted<ParametersBg>, ctx: &UiContext| {
+            let mut pc = crate::scene::paint::PaintCtx::new();
+            Paint::paint_ui(&**p, ctx, rect, &mut pc);
+            pc.finish().items.into_iter().map(|i| i.prim).collect::<Vec<_>>()
+        };
+        let label_of = |p: &Adapted<ParametersBg>, name: &str| {
+            p.own_text_labels().into_iter().find(|l| l.text == name).map(|l| l.color).expect("a row label")
+        };
+        let at_rest = prims(&p, &ctx);
+        assert_eq!(label_of(&p, "Count"), ParametersBg::LABEL);
+
         let (x, y, w, h) = p.get_param_rects()[1];
         p.on_cursor_moved(x + w * 0.5, y + h * 0.5, &mut ctx);
         assert_eq!(p.hover_row, Some(1));
-
-        let mut pc = crate::scene::paint::PaintCtx::new();
-        Paint::paint_ui(&*p, &ctx, Rect { x: 0.0, y: 0.0, width: 300.0, height: 400.0 }, &mut pc);
-        let items = pc.finish().items;
-        let is_carve = |prim: &Prim| matches!(prim, Prim::Recess { .. } | Prim::Boss { .. } | Prim::Trough { .. } | Prim::Ridge { .. });
-        let last_carve = items.iter().rposition(|i| is_carve(&i.prim)).expect("the rows carve at least one well");
-        let wash = items
-            .iter()
-            .position(|i| matches!(&i.prim, Prim::RoundedRect { color, .. } if *color == [1.0, 1.0, 1.0, 0.05]))
-            .expect("the hovered row's wash is painted");
-        assert!(wash > last_carve, "wash at {wash} must follow the last carve at {last_carve}");
+        assert_eq!(label_of(&p, "Count"), ParametersBg::LABEL_HOVER, "the hovered row's label lifts");
+        assert_eq!(label_of(&p, "Name"), ParametersBg::LABEL, "and only that row's");
+        assert_eq!(prims(&p, &ctx), at_rest, "a hover paints no geometry");
     }
 
-    /// The row tint is the pane's rule, not the control's: a spinbox row,
-    /// which draws no hover of its own under the relief style, is tinted
-    /// exactly as a slider row is, and a section header never is.
+    /// The label lift is the pane's rule, not the control's: a spinbox row,
+    /// which draws no hover of its own under the relief style, lifts exactly
+    /// as a slider row does, and a section header never does.
     #[test]
     fn every_control_kind_hovers_by_its_row() {
         let mut ctx = UiContext::new();