GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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);