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

commited5a688bc46a0a79d385ea56a9c0efcba5dee544
parent14f32a2400
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-01 21:02
feat: a spinbox's -/+ run is the control's right end

The -/+ run was nested inside a full-width well. It is a flush plate
reaching the control's outer edge now, as a dropdown trigger's does, with
the value's well square at the seam beside it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md                             |  7 ++++
 src/widget/container/parameters_bg.rs | 27 +++++++++++----
 src/widget/input/spinbox.rs           | 64 ++++++++++++++++++-----------------
 3 files changed, 61 insertions(+), 37 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index da4b7b5..ca60e6d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -329,6 +329,13 @@ and the pane's `reliefs` both read). Until then the picker was a button nested i
 box's well, its face stopping at the base of the well's wall, so it never reached the edge
 a dropdown's ▼ does. `textpick_rows_carry_a_picker` holds the layout.
 
+**A spinbox's -/+ run is its right end the same way** (the same day):
+`Spinbox::relief_parts` makes the run a flush plate over the whole band, out to the
+outer edge, its ring all round, square at the seam and the spinbox radius outside, and
+the value's well ends there square (`relief_parts` returns its radii, which the widget's
+paint and the pane's `reliefs` both use). The seam between - and + crosses the run's face
+between its ring's walls. It was nested inside a full-width well, as the picker was.
+
 ### A row can open a submenu (since 2026-09-29)
 
 `context_menu::set_row_submenu(idx, SubmenuSpec { options, header_count, sliders })`,
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index a7ce5bb..4cca83f 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -1764,12 +1764,27 @@ impl ParametersBg {
             } else if p.2.starts_with("spinbox") {
                 // The well recess only — the -/+ run's trough and its seam
                 // travel through [`Self::troughs`] / [`Self::grooves`] (this
-                // tuple speaks boss/recess). The generic push below matches
-                // the widget's own `relief_parts` well exactly: same side-
-                // label inset, same content band, same depth cap.
-                self.spinboxes[i]
-                    .as_ref()
-                    .map(|w| (w as &dyn WidgetHost, crate::layout::spinbox_corner_radius(), false))
+                // tuple speaks boss/recess). The widget's own `relief_parts`
+                // decides where the well ends: at the run, square there, the
+                // run being the control's right end. Same side-label inset,
+                // same content band, same depth cap as its paint.
+                if let Some(sb) = &self.spinboxes[i] {
+                    let (x, y, w, h) = sb.rect();
+                    let ty = sb.label_strip();
+                    let band = Rect { x, y: y + ty, width: w, height: h - ty };
+                    if w > 0.0 && h > 0.0 {
+                        let r = crate::layout::spinbox_corner_radius();
+                        let depth = crate::layout::bevel_width().min((h - ty) * 0.2);
+                        match sb.inner().relief_parts(band) {
+                            Some((_, Some(((run, _, _, _), _)))) => {
+                                out.push((x, y + ty, run.x - x, h - ty, (r, 0.0, 0.0, r), depth, false, all));
+                            }
+                            Some((_, None)) => out.push((x, y + ty, w, h - ty, r4(r), depth, false, all)),
+                            None => {}
+                        }
+                    }
+                }
+                None
             } else if p.2.starts_with("color") || p.2 == "rgb" || p.2 == "rgba" {
                 // The control's one well (`ColorSelector::field_relief`, the
                 // same geometry its paint carves); the swatch is a fill on its
diff --git a/src/widget/input/spinbox.rs b/src/widget/input/spinbox.rs
index cfdc2c9..ff4083f 100644
--- a/src/widget/input/spinbox.rs
+++ b/src/widget/input/spinbox.rs
@@ -132,18 +132,21 @@ impl Spinbox {
     /// (their rounded-quad bridge keeps only flat prims — the slider's
     /// `track_relief` precedent). Faces are transparent in this style; the
     /// 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 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 value sits in a recessed well (the TextBox language) that ends
+    ///   at the -/+ run, square there,
+    /// - the -/+ pair is ONE flush control plate, the control's right end:
+    ///   it reaches the control's outer edge on the top, right and bottom as
+    ///   a dropdown trigger's plate does, its ring all round, square where it
+    ///   meets the well and the control's radius outside — so the two read as
+    ///   one field with a seam between them, as a parameter pane's text row
+    ///   and its picker do. Until 2026-10-01 the run was nested INSIDE a
+    ///   full-width well, its face stopping at the base of the well's wall,
+    ///   so it never reached the edge a dropdown's does,
     /// - 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
+    /// Returns the well `(rect, radii, depth)` — carved, radii top-left
+    /// clockwise — plus the run
     /// `(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`.
@@ -152,7 +155,7 @@ impl Spinbox {
         &self,
         rect: Rect,
     ) -> Option<(
-        (Rect, f32, f32),
+        (Rect, (f32, f32, f32, f32), f32),
         Option<(
             (Rect, (f32, f32, f32, f32), f32, (bool, bool, bool, bool)),
             ((f32, f32), (f32, f32), f32, Rect),
@@ -164,29 +167,28 @@ impl Spinbox {
         }
         let radius = crate::layout::spinbox_corner_radius();
         let depth = crate::layout::bevel_width().min(g.h * 0.2);
-        let (well_rect, well_radii) =
-            crate::layout::carve_inside(Rect { x: g.x, y: g.y, width: g.w, height: g.h }, (radius, radius, radius, radius), depth);
-        let well = (well_rect, well_radii.0, depth);
+        let whole = Rect { x: g.x, y: g.y, width: g.w, height: g.h };
         if g.btn_w <= 0.0 || g.btn_h <= 0.0 {
-            return Some((well, None));
+            let (well_rect, well_radii) = crate::layout::carve_inside(whole, (radius, radius, radius, radius), depth);
+            return Some(((well_rect, well_radii, depth), None));
         }
-        // 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));
+        // The run begins at the button hit zone and spans the whole band to
+        // the control's right edge; the well is what is left of it.
+        let split = g.split_dec + g.pad;
+        let (well_rect, well_radii) = crate::layout::carve_inside(
+            Rect { x: g.x, y: g.y, width: split - g.x, height: g.h },
+            (radius, 0.0, 0.0, radius),
+            depth,
+        );
+        let well = (well_rect, well_radii, depth);
+        let run_rect = Rect { x: split, y: g.y, width: g.x + g.w - split, height: g.h };
+        let run = (run_rect, (0.0, radius, radius, 0.0), depth, (true, true, true, true));
         // Seam floor width: the breadcrumb's SEAM_WIDTH — a hair of flat
         // 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);
+        // the -/+ hit boundary, not the painted run's midpoint, and crosses
+        // the run's face between its ring's walls.
+        let sx = split + g.btn_w;
+        let seam = ((sx, run_rect.y + depth), (sx, run_rect.y + run_rect.height - depth), 0.75, run_rect);
         Some((well, Some((run, seam))))
     }
 
@@ -368,8 +370,8 @@ impl Paint for Spinbox {
                     let cursor_y = g.y + (g.h - 14.0) / 2.0;
                     ctx.quad(Rect { x: cursor_x, y: cursor_y, width: 1.5, height: 14.0 }, [0.80, 0.80, 0.85, 1.0]);
                 }
-                let (wr, wrad, wd) = well;
-                ctx.recess(wr, (wrad, wrad, wrad, wrad), wd);
+                let (wr, wradii, wd) = well;
+                ctx.recess(wr, wradii, wd);
                 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);