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

commit264df9af0a47efa99a96ff92ae2540c20ca0d8b3
parent782ac1e895
authorLucas Galante <[email protected]>
date2026-08-28 15:20
fix: the well's bevel is the far side of nested-control seams

The textpick picker and the spinbox -/+ run had their trough rings
encapsulated within the well's bevel, doubling the valley on every
adjoining side. Now the face reaches exactly to the well wall's base
(inset = the carve depth) and those trough edges are suppressed — the
WELL'S OWN WALL is the seam's far side; only edges facing open floor
(each control's interior left, and the -/+ divider groove) carve their
own. troughs() carries per-edge flags; relief_parts returns them.

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

 src/widget/container/parameters_bg.rs | 54 ++++++++++++++++++---------------
 src/widget/input/spinbox.rs           | 57 +++++++++++++++++++++--------------
 2 files changed, 65 insertions(+), 46 deletions(-)

diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 596e50b..8b7d581 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -605,15 +605,13 @@ impl ParametersBg {
                 if self.display_params[i].2.starts_with("textpick") {
                     // The picker button nests INSIDE the text box's recessed
                     // well (the box spans the full row): below the detached
-                    // label band, inset from the well's walls by HALF the
-                    // well's carve depth — its trough ring
-                    // ([`Self::picker_troughs`]) straddles this outline by
-                    // ±depth/2, so the ring's outer edge lands exactly on the
-                    // well outline and the seam hugs the box's edges the way
-                    // the dropdown's groove hugs its face. (The full-depth
-                    // inset was the boss era's: a raised island needed a
-                    // strip of floor for its wall to abut the recess wall,
-                    // which read as a too-small button in a too-wide ring.)
+                    // label band, its face reaching exactly to the base of
+                    // the well's wall (inset = the carve depth) on the sides
+                    // it adjoins — there the WELL'S OWN BEVEL is the seam's
+                    // far side, and the trough ([`Self::troughs`]) carves
+                    // only the interior left edge. A ring encapsulated
+                    // within the bevel doubled the valley on the adjoining
+                    // sides.
                     let label_top = if crate::layout::control_label_layout() == "side" {
                         0.0
                     } else {
@@ -621,7 +619,7 @@ impl ParametersBg {
                             + crate::layout::control_label_margin()
                     };
                     let band_h = r.3 - label_top;
-                    let inset = 0.5 * crate::layout::bevel_width().min(band_h * 0.2);
+                    let inset = crate::layout::bevel_width().min(band_h * 0.2);
                     let by = r.1 + label_top + inset;
                     let bh = (band_h - 2.0 * inset).max(8.0);
                     d.set_rect(r.0 + r.2 - PICK_W - inset, by, PICK_W, bh);
@@ -1246,15 +1244,20 @@ impl ParametersBg {
     }
 
     /// The textpick picker buttons' trough rings — `(x, y, w, h, radii, depth)`
-    /// for [`crate::scene::paint::PaintCtx::trough`], drawn by the host AFTER
-    /// [`Self::reliefs`]. These are the rows' FLUSH inset controls — the
+    /// for [`crate::scene::paint::PaintCtx::trough_edges`], drawn by the host
+    /// AFTER [`Self::reliefs`]. These are the rows' FLUSH inset controls — the
     /// textpick picker button, and the spinbox's -/+ run: faces level with
-    /// their well floor, a valley seam straddling the outline — the dropdown
-    /// trigger's (and the breadcrumb run's) inset language. They cannot ride
-    /// in [`Self::reliefs`], whose tuple only speaks boss/recess. Radii are
-    /// the well radius's parallel curve at each control's inset; depths match
-    /// the well's carve, so seam and wall read as one family.
-    pub fn troughs(&self) -> Vec<(f32, f32, f32, f32, (f32, f32, f32, f32), f32)> {
+    /// their well floor. On the sides a control adjoins its well, the WELL'S
+    /// OWN WALL is the seam's far side (the face reaches the wall's base and
+    /// that trough edge is suppressed — a lip of its own there doubles the
+    /// valley); only edges facing open floor carve their own wall. They
+    /// cannot ride in [`Self::reliefs`], whose tuple only speaks boss/recess.
+    /// Radii are the well radius's parallel curve at each control's inset;
+    /// depths match the well's carve, so seam and wall read as one family.
+    #[allow(clippy::type_complexity)]
+    pub fn troughs(
+        &self,
+    ) -> Vec<(f32, f32, f32, f32, (f32, f32, f32, f32), f32, (bool, bool, bool, bool))> {
         if !self.visible || !crate::layout::control_relief() {
             return Vec::new();
         }
@@ -1272,7 +1275,8 @@ impl ParametersBg {
                     if bw > 0.0 && bh > 0.0 {
                         let depth = crate::layout::bevel_width().min((th - ty) * 0.2);
                         let r = (crate::layout::textbox_corner_radius() - depth).max(2.0);
-                        out.push((bx, by, bw, bh, (r, r, r, r), depth));
+                        // Left edge only: top, right and bottom adjoin the well.
+                        out.push((bx, by, bw, bh, (r, r, r, r), depth, (false, false, false, true)));
                     }
                 }
             } else if p.2.starts_with("spinbox") {
@@ -1280,8 +1284,10 @@ impl ParametersBg {
                     let (x, y, w, h) = sb.rect();
                     let ty = crate::widget::label_offset(sb);
                     let band = Rect { x, y: y + ty, width: w, height: h - ty };
-                    if let Some((_, Some(((run, radii, rd), _)))) = sb.inner().relief_parts(band) {
-                        out.push((run.x, run.y, run.width, run.height, radii, rd));
+                    if let Some((_, Some(((run, radii, rd, edges), _)))) =
+                        sb.inner().relief_parts(band)
+                    {
+                        out.push((run.x, run.y, run.width, run.height, radii, rd, edges));
                     }
                 }
             }
@@ -1309,7 +1315,7 @@ impl ParametersBg {
                 let (x, y, w, h) = sb.rect();
                 let ty = crate::widget::label_offset(sb);
                 let band = Rect { x, y: y + ty, width: w, height: h - ty };
-                if let Some((_, Some(((_, _, rd), (sa, sb2, sw, host))))) =
+                if let Some((_, Some(((_, _, rd, _), (sa, sb2, sw, host))))) =
                     sb.inner().relief_parts(band)
                 {
                     out.push((sa, sb2, sw, rd, host));
@@ -1465,8 +1471,8 @@ impl Paint for ParametersBg {
                 ctx.recess_edges(Rect { x: rx, y: ry, width: rw, height: rh }, radii, rd, edges);
             }
         }
-        for (tx2, ty2, tw2, th2, radii, td) in self.troughs() {
-            ctx.trough(Rect { x: tx2, y: ty2, width: tw2, height: th2 }, radii, td);
+        for (tx2, ty2, tw2, th2, radii, td, tedges) in self.troughs() {
+            ctx.trough_edges(Rect { x: tx2, y: ty2, width: tw2, height: th2 }, radii, td, tedges);
         }
         for (ga, gb, gw, gd, ghost) in self.grooves() {
             ctx.groove(ga, gb, gw, gd, ghost);
diff --git a/src/widget/input/spinbox.rs b/src/widget/input/spinbox.rs
index c724fef..3f8bf1b 100644
--- a/src/widget/input/spinbox.rs
+++ b/src/widget/input/spinbox.rs
@@ -105,15 +105,19 @@ impl Spinbox {
     /// relief IS the chrome:
     /// - the whole control is a recessed well (the TextBox language — the
     ///   value sits on the well floor),
-    /// - the -/+ pair is ONE flush inset run inside the well's right end,
-    ///   trough-ringed like the textpick picker and the dropdown trigger,
+    /// - the -/+ pair is ONE flush inset run in the well's right end. Where
+    ///   the run adjoins the well (top, right, bottom) the WELL'S OWN WALL is
+    ///   the seam's far side — the face reaches exactly to the wall's base
+    ///   (inset = the carve depth) and those trough edges are suppressed; a
+    ///   second lip inside the bevel would double the valley. Only the left
+    ///   edge, which faces open floor, carries its own trough wall,
     /// - the two buttons divide by an engraved seam, not a wall pair — the
     ///   breadcrumb run's segment language at miniature scale.
     ///
     /// Returns the well `(rect, radius, depth)`, plus the run
-    /// `(rect, radii, depth)` and seam `(top, bottom, width, host)` when the
-    /// button zone is non-degenerate. `None` when the control has no area.
-    /// The caller gates on `control_relief`.
+    /// `(rect, radii, depth, trough edges)` and seam `(top, bottom, width,
+    /// host)` when the button zone is non-degenerate. `None` when the control
+    /// has no area. The caller gates on `control_relief`.
     #[allow(clippy::type_complexity)]
     pub fn relief_parts(
         &self,
@@ -121,7 +125,7 @@ impl Spinbox {
     ) -> Option<(
         (Rect, f32, f32),
         Option<(
-            (Rect, (f32, f32, f32, f32), f32),
+            (Rect, (f32, f32, f32, f32), f32, (bool, bool, bool, bool)),
             ((f32, f32), (f32, f32), f32, Rect),
         )>,
     )> {
@@ -135,17 +139,22 @@ impl Spinbox {
         if g.btn_w <= 0.0 || g.btn_h <= 0.0 {
             return Some((well, None));
         }
-        // The run rides the existing button hit zones (inset `pad` from the
-        // control edges — within a hair of the trough's depth/2 straddle, so
-        // the ring's outer edge hugs the well outline; the picker lesson).
-        // Right corners follow the well radius's parallel curve at that
-        // inset; left corners stay tight — the run's left edge is interior.
-        let run_rect = Rect { x: g.split_dec + g.pad, y: g.btn_y, width: 2.0 * g.btn_w, height: g.btn_h };
-        let rr = (radius - g.pad).max(2.0);
-        let run = (run_rect, (2.0, rr, rr, 2.0), depth);
+        // Face flush against the wall base on the adjoining sides; the left
+        // edge starts at the button hit zone. Right corners follow the well
+        // radius's parallel curve at the wall base; left corners stay tight —
+        // the run's left edge is interior.
+        let run_rect = Rect {
+            x: g.split_dec + g.pad,
+            y: g.y + depth,
+            width: (g.x + g.w - depth) - (g.split_dec + g.pad),
+            height: g.h - 2.0 * depth,
+        };
+        let rr = (radius - depth).max(2.0);
+        let run = (run_rect, (2.0, rr, rr, 2.0), depth, (false, false, false, true));
         // Seam floor width: the breadcrumb's SEAM_WIDTH — a hair of flat
-        // floor so the crease doesn't alias into a dotted line.
-        let sx = run_rect.x + g.btn_w;
+        // floor so the crease doesn't alias into a dotted line. It sits on
+        // the -/+ hit boundary, not the painted run's midpoint.
+        let sx = g.split_dec + g.pad + g.btn_w;
         let seam = ((sx, run_rect.y), (sx, run_rect.y + run_rect.height), 0.75, run_rect);
         Some((well, Some((run, seam))))
     }
@@ -277,15 +286,19 @@ impl Paint for Spinbox {
             // washes and the editing cue survive a flat host's rounded-quad
             // bridge, the carves are re-emitted host-side.
             if let Some((well, buttons)) = self.relief_parts(rect) {
-                if let Some(((run, radii, _), _)) = buttons {
+                if let Some(((run, radii, _, _), (seam_top, _, _, _))) = buttons {
                     let wash = [1.0, 1.0, 1.0, 0.06];
-                    let half = Rect { x: run.x, y: run.y, width: run.width * 0.5, height: run.height };
                     if self.hover_dec {
-                        ctx.rounded_rect(half, radii.0, (true, false, false, true), wash);
+                        ctx.rounded_rect(
+                            Rect { x: run.x, y: run.y, width: seam_top.0 - run.x, height: run.height },
+                            radii.0,
+                            (true, false, false, true),
+                            wash,
+                        );
                     }
                     if self.hover_inc {
                         ctx.rounded_rect(
-                            Rect { x: run.x + run.width * 0.5, ..half },
+                            Rect { x: seam_top.0, y: run.y, width: run.x + run.width - seam_top.0, height: run.height },
                             radii.1,
                             (false, true, true, false),
                             wash,
@@ -309,8 +322,8 @@ impl Paint for Spinbox {
                 }
                 let (wr, wrad, wd) = well;
                 ctx.recess(wr, (wrad, wrad, wrad, wrad), wd);
-                if let Some(((run, radii, rd), (sa, sb, sw, host))) = buttons {
-                    ctx.trough(run, radii, rd);
+                if let Some(((run, radii, rd, edges), (sa, sb, sw, host))) = buttons {
+                    ctx.trough_edges(run, radii, rd, edges);
                     ctx.groove(sa, sb, sw, rd, host);
                 }
             }