GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: a toggle is a field whose run glides
A dropdown, a button, a text row with its picker, a spinbox and a toggle
are one object: a well in a plate with a flush run standing in it, one
outline round both. They differ only in where the run is. Prim::Field's
run is now a span, split to end (PaintCtx::field_run), and where it stops
short of the right end MODE_FIELD mirrors the left: a well on that side,
the face inset half a wall from the seam with the run's left corners, the
seam the well's left wall. A run reaching the right end takes the same
path as before, term for term.
The Toggle is drawn so: a run half the field wide at the left end off,
the right end on, gliding between with a well either side. It was a
recess with a raised boss on its floor, the one control whose nested
plate stood above the surface instead of flush with it. Focus lights the
field's rim; ParametersBg draws it among its fields.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 36 ++++-
src/backend/window_runner.rs | 46 +++---
src/layout.rs | 30 ++--
src/scene/paint.rs | 42 +++--
src/vk/shader2d.wgsl | 42 ++++-
src/widget/container/parameters_bg.rs | 79 +++++-----
src/widget/input/checkbox.rs | 280 ++++++++++++++--------------------
7 files changed, 309 insertions(+), 246 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index af2e0b5..56a00ab 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -400,6 +400,40 @@ are the tests. Until the same day the picker and the run were nested INSIDE a fu
well, their faces stopping at the base of its wall, so they never reached the edge a
dropdown's ▼ does.
+### A field is one object, and a toggle is a field whose run glides (since 2026-10-02)
+
+A dropdown, a button, a text row with its picker, a spinbox and a toggle are ONE
+object: a well cut into a plate with a flush plate, the RUN, standing in it, one
+outline round both (`Prim::Field`). They differ only in where the run is:
+
+| form | run | well |
+| --- | --- | --- |
+| flush control plate (dropdown trigger, button, breadcrumb run, …) | the whole field | none |
+| text row with its picker, spinbox with its -/+ run | the right end | left of it |
+| toggle | half the field: the left end off, the right end on | the other half; both sides mid-glide |
+| plain text box | none (a `Prim::Recess`, for grouping — see below) | the whole field |
+
+So the run is a span, `split` to `end` (`PaintCtx::field_run`; `PaintCtx::field`
+is the run reaching the right end, `end` `FIELD_RUN_ONLY` past it). Where the run
+stops short of the right end, a well lies to its right too, and `MODE_FIELD`
+mirrors everything it does on the left: the face is inset half a wall from that
+seam with the corners of the run's LEFT end, and the seam is that well's left
+wall's inner half. A run reaching the right end takes the same path through the
+shader as before, term for term, so every field that was drawn is drawn as it was.
+The host-box slot carries both ends (`p_host.x`, `.y`); the legacy banded path and
+the flat-host bridge draw a well box either side of the run.
+
+**The Toggle is drawn so** (`Toggle::field` the outline, `Toggle::run` the span,
+`Toggle::face` what the relief-off path lights; `ParametersBg::fields` asks for
+the same, and its `reliefs` no longer carries toggles). Until this it was a recess
+with a raised `Boss` on its floor: the one control whose nested plate stood ABOVE
+the surface where every other stood flush with it, and whose outline turned its
+own corner round the plate instead of running round the whole control. The run
+is laid out half the field and a wall wider than its face, as a picker is, so the
+face reaches the well; it glides by `slide_t` as the boss did. Focus lights the
+field's rim (`ControlPlate::focus_tint`), where it lit the boss's.
+`a_toggle_is_a_field_whose_run_glides` is the test.
+
### A grouped carve shades as its overlay does (since 2026-10-02)
A full-ring, untinted `Prim::Recess` / `Boss` emitted while a plate's grouping window is
@@ -585,7 +619,7 @@ What this buys, and where the code is heading:
instead of black (`FOCUS_SHADOW`, both at `FOCUS_GAIN`, in
`shader2d.wgsl`), so a focused plate still reads which edges face the lamp.
A Checkbox lights the ring its mark already draws; a Toggle lights the
- rim of the plate that slides in its well.
+ rim of its field, as every field is lit.
Roles today: Button, Checkbox, Toggle, Dropdown, FontSelector, ButtonStrip
(arrows move the selection between its segment plates) and Breadcrumb
(arrows walk its visible segments, Enter navigates) are plates; TextBox,
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 9fd99c5..f914646 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -2173,9 +2173,9 @@ pub fn tessellate_display_list(
// A sunken well ending in a flush run (see `Prim::Field`): one
// outline, one overlay — never grouped, its profile is not a
// monotonic step. The cover quad inflates by half the wall, as a
- // free carve's does; the host-box slot carries the seam's x
- // (physical px), since nothing fades against a host here.
- Prim::Field { rect, radii, depth, split, tint } if shader_plates => {
+ // free carve's does; the host-box slot carries the run's two
+ // ends (physical px), since nothing fades against a host here.
+ Prim::Field { rect, radii, depth, split, end, tint } if shader_plates => {
let infl = *depth * 0.5 + 2.0;
verts.extend(quad_vertices(
rect.x - infl, rect.y - infl,
@@ -2187,7 +2187,7 @@ pub fn tessellate_display_list(
if let Some(t) = tint {
p.specular_tint = [t[0], t[1], t[2], 1.0];
}
- p.host = [*split * scale, 0.0, 0.0, 0.0];
+ p.host = [*split * scale, *end * scale, 0.0, 0.0];
plate = Some(p);
}
Prim::Recess { rect, radii, depth, edges, .. }
@@ -2542,32 +2542,42 @@ pub fn tessellate_display_list(
EdgeKind::Step, &mut verts,
);
}
- Prim::Field { rect, radii, depth, split, .. } => {
+ Prim::Field { rect, radii, depth, split, end, .. } => {
// Legacy approximation: the two-box form `Prim::Field`
- // replaced — the well to the seam a step down, the run past it
- // the Trough arm's down-then-up stack. The banded machinery
- // has no blended outline, and the legacy path exists only for
- // A/B comparison.
+ // replaced — a well either side of the run a step down, the
+ // run the Trough arm's down-then-up stack. The banded
+ // machinery has no blended outline, and the legacy path
+ // exists only for A/B comparison.
let all = (true, true, true, true);
- if *split > rect.x {
+ let (fl, fr) = (rect.x, rect.x + rect.width);
+ let (well_l, well_r) = (*split > fl, *end < fr);
+ let rx = split.max(fl);
+ let rw = (end.min(fr) - rx).max(0.0);
+ if well_l {
push_bevel_edge_vertices_banded(
- rect.x, rect.y, *split - rect.x, rect.height, (radii.0, 0.0, 0.0, radii.3), *depth,
+ fl, rect.y, rx - fl, rect.height, (radii.0, 0.0, 0.0, radii.3), *depth,
sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(*depth), all,
EdgeKind::Step, &mut verts,
);
}
- let rx = split.max(rect.x);
- let rw = (rect.x + rect.width - rx).max(0.0);
- // All run (`PaintCtx::inset_plate`'s edge): its own left corners.
- let (l0, l3) = if *split > rect.x { (0.0, 0.0) } else { (radii.0, radii.3) };
+ if well_r {
+ push_bevel_edge_vertices_banded(
+ rx + rw, rect.y, fr - rx - rw, rect.height, (0.0, radii.1, radii.2, 0.0), *depth,
+ sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(*depth), all,
+ EdgeKind::Step, &mut verts,
+ );
+ }
+ // A run's own corners where it reaches the outline; square at a seam.
+ let (l0, l3) = if well_l { (0.0, 0.0) } else { (radii.0, radii.3) };
+ let (r1, r2) = if well_r { (0.0, 0.0) } else { (radii.1, radii.2) };
let half = *depth * 0.5;
push_bevel_edge_vertices_banded(
- rx, rect.y, rw, rect.height, (l0, radii.1, radii.2, l3), half,
+ rx, rect.y, rw, rect.height, (l0, r1, r2, l3), half,
sw, sh, [0.0; 4], no, -1.0, default_bevel_bands(half), all,
EdgeKind::Step, &mut verts,
);
- let ir = (radii.1 - half).max(0.0);
- let il = if *split > rect.x { 0.0 } else { (radii.0 - half).max(0.0) };
+ let ir = if well_r { 0.0 } else { (radii.1 - half).max(0.0) };
+ let il = if well_l { 0.0 } else { (radii.0 - half).max(0.0) };
push_bevel_edge_vertices_banded(
rx + half, rect.y + half, rw - *depth, rect.height - *depth, (il, ir, ir, il), half,
sw, sh, [0.0; 4], no, 1.0, default_bevel_bands(half), all,
diff --git a/src/layout.rs b/src/layout.rs
index 72c3bae..2da665e 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -4272,7 +4272,9 @@ pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut
let trough_for_face = match (&item.prim, &pending_face) {
(Prim::Trough { rect, .. }, Some((face_rect, _, _))) => same_rect(*rect, *face_rect),
// `inset_plate`'s edge: a field that is all run.
- (Prim::Field { rect, split, .. }, Some((face_rect, _, _))) => *split <= rect.x && same_rect(*rect, *face_rect),
+ (Prim::Field { rect, split, end, .. }, Some((face_rect, _, _))) => {
+ *split <= rect.x && *end >= rect.x + rect.width && same_rect(*rect, *face_rect)
+ }
_ => false,
};
if !trough_for_face {
@@ -4347,25 +4349,33 @@ pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut
// A flat host has no blended outline: the two-box form
// `Prim::Field` replaced — the well to the seam, the run's flush
// plate past it.
- Prim::Field { rect, radii, depth, split, tint } => {
+ Prim::Field { rect, radii, depth, split, end, tint } => {
+ let (fl, fr) = (rect.x, rect.x + rect.width);
+ let (well_l, well_r) = (split > fl, end < fr);
// All run and no well (`PaintCtx::inset_plate`'s edge): the
// plate alone, with the face that came before it.
- let face = if split <= rect.x { pending_face.take().map(|(_, _, fill)| fill) } else { None }.unwrap_or([0.0; 4]);
- if split > rect.x {
+ let face = if !well_l && !well_r { pending_face.take().map(|(_, _, fill)| fill) } else { None }.unwrap_or([0.0; 4]);
+ let rx = split.max(fl);
+ let rw = (end.min(fr) - rx).max(0.0);
+ let mut well = |x: f32, w: f32, radii: (f32, f32, f32, f32)| {
pc.relief_carve(&ReliefCarve {
kind: CarveKind::Recess { tint },
- x: rect.x,
+ x,
y: rect.y,
- w: split - rect.x,
+ w,
h: rect.height,
- radii: (radii.0, 0.0, 0.0, radii.3),
+ radii,
depth,
edges: (true, true, true, true),
});
+ };
+ if well_l {
+ well(fl, rx - fl, (radii.0, 0.0, 0.0, radii.3));
+ }
+ if well_r {
+ well(rx + rw, fr - rx - rw, (0.0, radii.1, radii.2, 0.0));
}
- let r = radii.1.max(radii.2);
- let rx = split.max(rect.x);
- let rw = (rect.x + rect.width - rx).max(0.0);
+ let r = if well_r { radii.0.max(radii.3) } else { radii.1.max(radii.2) };
match tint {
Some(t) => pc.inset_plate_tinted(face, rx, rect.y, rw, rect.height, r, depth, t),
None => pc.inset_plate(face, rx, rect.y, rw, rect.height, r, depth),
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index c709792..cd21398 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -219,7 +219,9 @@ pub enum PlateStance {
/// How far left of a [`Prim::Field`] its seam is put to make it all run and
/// no well ([`PaintCtx::inset_plate`]): far enough that the blend across the
-/// seam and the seam's own wall land nowhere near the field.
+/// seam and the seam's own wall land nowhere near the field. A run's `end`
+/// is put as far past the field's right edge to reach that edge
+/// ([`PaintCtx::field`]).
pub const FIELD_RUN_ONLY: f32 = 1.0e4;
/// A control plate: the thing you press, at the control rung of the plate
@@ -586,12 +588,17 @@ pub enum Prim {
/// `tint` lights the rim like [`Prim::Recess`]'s — the focus treatment of a
/// flush control plate (`PaintCtx::control_plate`).
Trough { rect: Rect, radii: Radii, depth: f32, edges: (bool, bool, bool, bool), tint: Option<[f32; 3]> },
- /// A sunken well that ends in a FLUSH run: one field, one outer contour.
- /// Left of `split` (an x in the same space as `rect`) it is a
- /// [`Prim::Recess`] — the interior one step down; right of it, a
- /// [`Prim::Trough`] — a valley on the outline and the interior back at
- /// the surface's level, as a flush control plate's. A parameter pane's
- /// text row with its completion picker, a spinbox's value and its -/+ run.
+ /// A sunken well holding a FLUSH run: one field, one outer contour.
+ /// Between `split` and `end` (xs in the same space as `rect`) the
+ /// interior is back at the surface's level, as a flush control plate's
+ /// face, inside a valley on the outline; on either side of that it is a
+ /// [`Prim::Recess`] — the interior one step down. A run that reaches an
+ /// end of the field (`split` left of it, or `end` right of it — by
+ /// [`FIELD_RUN_ONLY`]) has no well on that side. The forms in use: a
+ /// parameter pane's text row with its completion picker and a spinbox's
+ /// value with its -/+ run (the run at the right end), a flush control
+ /// plate (all run), a toggle (a run half the field wide, at the left end
+ /// off and the right end on, gliding between).
///
/// Why one prim and not the two it replaces, side by side: each of those
/// shades its OWN box, so at the seam the outline breaks — each box
@@ -607,7 +614,7 @@ pub enum Prim {
///
/// SDF path only; the legacy banded tessellation and flat hosts draw the
/// two-box form it replaced. `tint` lights it like a recess's.
- Field { rect: Rect, radii: Radii, depth: f32, split: f32, tint: Option<[f32; 3]> },
+ Field { rect: Rect, radii: Radii, depth: f32, split: f32, end: f32, tint: Option<[f32; 3]> },
/// The window's glass slab: a rounded fill plus a rolled, lit edge around its whole
/// perimeter, drawn at full size. Distinct from `Bevel`, which insets its fill by
/// `depth` — a plate must fill the window exactly, or the compositor's rounded window
@@ -1114,7 +1121,7 @@ impl PaintCtx {
Some(t) => self.trough_tinted(rect, radii, depth, t),
None => self.trough_edges(rect, radii, depth, edges),
},
- Prim::Field { rect, radii, depth, split, tint } => self.field(rect, radii, depth, split, tint),
+ Prim::Field { rect, radii, depth, split, end, tint } => self.field_run(rect, radii, depth, (split, end), tint),
Prim::Plate { rect, radii, material, depth, shape } => {
self.plate_shaped(rect, radii, &material, depth, shape)
}
@@ -1566,12 +1573,21 @@ impl PaintCtx {
self.push(Prim::Trough { rect, radii, depth, edges, tint: Some(tint) });
}
- /// A sunken well ending in a flush run at `split` — see [`Prim::Field`].
- /// `tint` lights its rim (the focus and hover treatment).
+ /// A sunken well ending in a flush run at `split`, the run reaching the
+ /// field's right end — see [`Prim::Field`]. `tint` lights its rim (the
+ /// focus and hover treatment).
pub fn field(&mut self, rect: Rect, radii: Radii, depth: f32, split: f32, tint: Option<[f32; 3]>) {
- let split = split + self.offset.0;
+ let end = rect.x + rect.width + FIELD_RUN_ONLY;
+ self.field_run(rect, radii, depth, (split, end), tint);
+ }
+
+ /// A sunken well with a flush run from `run.0` to `run.1` — see
+ /// [`Prim::Field`]: the general form, a well on whichever sides of the
+ /// run the field extends past it.
+ pub fn field_run(&mut self, rect: Rect, radii: Radii, depth: f32, run: (f32, f32), tint: Option<[f32; 3]>) {
+ let (split, end) = (run.0 + self.offset.0, run.1 + self.offset.0);
let rect = self.apply_offset(rect);
- self.push(Prim::Field { rect, radii, depth, split, tint });
+ self.push(Prim::Field { rect, radii, depth, split, end, tint });
}
/// Raise a rim along `rect`'s boundary — see `Prim::Ridge`. `depth` is the
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index b4230ba..77a9423 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -955,24 +955,47 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
// A field with no well (split left of it — a dropdown trigger, a
// button) is all run: the face inset from the outline all round,
// its own corners.
+ //
+ // The run ends at p_host.y. Past the field's right end (every form
+ // above) it reaches the outline there; short of it, a well lies to
+ // its right too, mirroring everything said of the left: the face
+ // inset half a wall from that seam with the corners of its LEFT end,
+ // and the seam the well's left wall's inner half. A toggle's run is
+ // half the field and glides between its ends, so mid-glide it has a
+ // well on both sides.
let split = rrect_clip.p_host.x;
+ let run_end = rrect_clip.p_host.y;
let hw = 0.5 * t;
let pr = rrect_clip.p_rect;
let fl = pr.x - pr.z;
+ let fr = pr.x + pr.z;
let has_well = split > fl + 0.5;
+ let has_well_r = run_end < fr - 0.5;
let run_l = select(fl, split, has_well);
+ let run_r = select(fr, run_end, has_well_r);
let fx0 = run_l + hw;
- let fx1 = pr.x + pr.z - hw;
+ let fx1 = run_r - hw;
let fy0 = pr.y - pr.w + hw;
let fy1 = pr.y + pr.w - hw;
let face = vec4f(0.5 * (fx0 + fx1), 0.5 * (fy0 + fy1), max(0.5 * (fx1 - fx0), 0.0), max(0.5 * (fy1 - fy0), 0.0));
let rr = rrect_clip.p_radii;
+ let rtl = max(rr.x - hw, 0.0);
let rtr = max(rr.y - hw, 0.0);
let rbr = max(rr.z - hw, 0.0);
- let face_r = select(vec4f(max(rr.x - hw, 0.0), rtr, rbr, max(rr.w - hw, 0.0)), vec4f(rtr, rtr, rbr, rbr), has_well);
+ let rbl = max(rr.w - hw, 0.0);
+ let face_r = vec4f(
+ select(rtl, rtr, has_well),
+ select(rtr, rtl, has_well_r),
+ select(rbr, rbl, has_well_r),
+ select(rbl, rbr, has_well),
+ );
// The outline: its outer half everywhere, its inner half over the
- // well alone (`over_well`, antialiased across the seam's pixel).
- let over_well = 1.0 - smoothstep(split - 0.5, split + 0.5, frag.x);
+ // wells alone (`over_well`, antialiased across each seam's pixel).
+ let over_well = clamp(
+ 1.0 - smoothstep(split - 0.5, split + 0.5, frag.x) + smoothstep(run_end - 0.5, run_end + 0.5, frag.x),
+ 0.0,
+ 1.0,
+ );
let inner = select(1.0, over_well, u > 0.5);
slope = -cd * carve_slope(u) * inner;
curv = rrect_clip.p_mat.w * sin(u * TAU) * inner;
@@ -995,6 +1018,17 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
curv_seam = curv_seam + rrect_clip.p_mat.w * sin(usc * TAU) * fade;
}
}
+ // The right-hand seam: the right well's LEFT wall, its inner half,
+ // falling the other way.
+ if (has_well_r) {
+ let us = (frag.x - run_end) / t + 0.5;
+ if (us >= 0.5) {
+ let usc = min(us, 1.0);
+ let fade = clamp(d / hw, 0.0, 1.0);
+ sv_seam = sv_seam + vec2f(-1.0, 0.0) * (-cd * carve_slope(usc)) * fade;
+ curv_seam = curv_seam + rrect_clip.p_mat.w * sin(usc * TAU) * fade;
+ }
+ }
} else if (eff == MODE_RIDGE || eff == MODE_TROUGH) {
// Ridge bump: the carve profile mirrored about the boundary (rising
// outer half, falling inner half), amplitude halved so the wall tilt
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 36cb9ef..86eb249 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -1746,26 +1746,8 @@ impl ParametersBg {
// island the button itself never drew.
None
} else if p.2 == "toggle" || p.2 == "checkbox" {
- // A toggle paints no fill at all, so these carves — the
- // widget's own (`Toggle::flat_carves`, exactly what its paint
- // emits, in the order it emits them) — ARE the control: the
- // track's well (a recess) and the plate gliding on its floor
- // (a boss). Neither is flush.
- if let Some(t) = &self.toggles[i] {
- let (x, y, w, h) = t.rect();
- if w > 0.0 && h > 0.0 {
- let ty = t.label_strip();
- let rect = Rect { x, y: y + ty, width: w, height: h - ty };
- for c in t.inner().flat_carves(rect) {
- let raised = match c.kind {
- crate::layout::CarveKind::Boss { .. } => true,
- crate::layout::CarveKind::Recess { .. } => false,
- crate::layout::CarveKind::Trough => continue,
- };
- out.push((c.x, c.y, c.w, c.h, c.radii, c.depth, raised, c.edges));
- }
- }
- }
+ // A toggle is a field ([`Self::fields`]): its well and the
+ // flush run gliding in it, one outline round both.
None
} else if p.2.starts_with("spinbox") {
// A plain well only when there is no -/+ run; with one the
@@ -1829,16 +1811,20 @@ impl ParametersBg {
}
/// The rows that are ONE field ([`crate::scene::paint::Prim::Field`]):
- /// a sunken well ending in a flush run, one outline round both —
- /// `(x, y, w, h, radii, depth, split)`, drawn AFTER [`Self::reliefs`].
- /// A textpick row is its text box's well and its picker; a spinbox, its
- /// value's well and its -/+ run; a dropdown row, a field that is all run
- /// (its seam [`crate::scene::paint::FIELD_RUN_ONLY`] to the left), so
- /// every flush control in the pane has the same edge. They replaced
+ /// a sunken well holding a flush run, one outline round both —
+ /// `(x, y, w, h, radii, depth, split, end)`, the run from `split` to
+ /// `end`, drawn AFTER [`Self::reliefs`]. A textpick row is its text box's
+ /// well and its picker; a spinbox, its value's well and its -/+ run (both
+ /// runs reaching the right end, `end` [`crate::scene::paint::FIELD_RUN_ONLY`]
+ /// past it); a dropdown or button row, a field that is all run (its seam
+ /// as far to the left); a toggle, a well with its run at the end its
+ /// state says (`Toggle::field` and `Toggle::run`). So every flush control
+ /// in the pane has the same edge. They replaced
/// `troughs()` on 2026-10-01. On the pane's own rule for wells and
/// troughs: the content band as the outline, the wall straddling it.
#[allow(clippy::type_complexity)]
- pub fn fields(&self) -> Vec<(f32, f32, f32, f32, (f32, f32, f32, f32), f32, f32)> {
+ pub fn fields(&self) -> Vec<(f32, f32, f32, f32, (f32, f32, f32, f32), f32, f32, f32)> {
+ use crate::scene::paint::FIELD_RUN_ONLY;
if !self.visible || !crate::layout::control_relief() {
return Vec::new();
}
@@ -1859,7 +1845,7 @@ impl ParametersBg {
if w > 0.0 && h - ty > 0.0 && dw > 0.0 {
let depth = crate::layout::bevel_width().min((h - ty) * 0.2);
let r = crate::layout::textbox_corner_radius();
- out.push((x, y + ty, dx + dw - x, h - ty, (r, r, r, r), depth, dx));
+ out.push((x, y + ty, dx + dw - x, h - ty, (r, r, r, r), depth, dx, dx + dw + FIELD_RUN_ONLY));
}
}
} else if p.2 == "button" {
@@ -1872,7 +1858,7 @@ impl ParametersBg {
if w > 0.0 && h - ty > 0.0 {
let depth = crate::layout::bevel_width().min((h - ty) * 0.2);
let r = crate::layout::button_corner_radius();
- out.push((x, y + ty, w, h - ty, (r, r, r, r), depth, x - crate::scene::paint::FIELD_RUN_ONLY));
+ out.push((x, y + ty, w, h - ty, (r, r, r, r), depth, x - FIELD_RUN_ONLY, x + w + FIELD_RUN_ONLY));
}
}
} else if p.2.starts_with("choice") {
@@ -1887,7 +1873,7 @@ impl ParametersBg {
if w > 0.0 && h - ty > 0.0 {
let depth = crate::layout::bevel_width().min((h - ty) * 0.2);
let r = crate::layout::dropdown_corner_radius();
- out.push((x, y + ty, w, h - ty, (r, r, r, r), depth, x - crate::scene::paint::FIELD_RUN_ONLY));
+ out.push((x, y + ty, w, h - ty, (r, r, r, r), depth, x - FIELD_RUN_ONLY, x + w + FIELD_RUN_ONLY));
}
}
} else if p.2.starts_with("spinbox") {
@@ -1898,10 +1884,33 @@ impl ParametersBg {
if let Some(rel) = sb.inner().relief_parts(band) {
if let Some((split, _)) = rel.run {
let r = rel.radius;
- out.push((band.x, band.y, band.width, band.height, (r, r, r, r), rel.depth, split));
+ out.push((
+ band.x,
+ band.y,
+ band.width,
+ band.height,
+ (r, r, r, r),
+ rel.depth,
+ split,
+ band.x + band.width + FIELD_RUN_ONLY,
+ ));
}
}
}
+ } else if p.2 == "toggle" || p.2 == "checkbox" {
+ // The toggle's own field (`Toggle::field`, `Toggle::run`) —
+ // exactly what its paint draws, on its band. It paints no
+ // fill, so this IS the control.
+ if let Some(t) = &self.toggles[i] {
+ let (x, y, w, h) = t.rect();
+ let ty = t.label_strip();
+ if w > 0.0 && h - ty > 0.0 {
+ let band = Rect { x, y: y + ty, width: w, height: h - ty };
+ let (f, radii, depth) = t.inner().field(band);
+ let (a, b) = t.inner().run(band);
+ out.push((f.x, f.y, f.width, f.height, radii, depth, a, b));
+ }
+ }
}
}
out
@@ -2114,10 +2123,10 @@ impl Paint for ParametersBg {
(false, false) => ctx.recess_edges(rect, radii, rd, edges),
}
}
- for (fx, fy, fw, fh, radii, fd, split) in self.fields() {
+ for (fx, fy, fw, fh, radii, fd, split, end) in self.fields() {
let rect = Rect { x: fx, y: fy, width: fw, height: fh };
let tint = hovered(fx, fy, fw, fh).then_some(Self::HOVER_TINT);
- ctx.field(rect, radii, fd, split, tint);
+ ctx.field_run(rect, radii, fd, (split, end), tint);
}
for (ga, gb, gw, gd, ghost) in self.grooves() {
ctx.groove(ga, gb, gw, gd, ghost);
@@ -3911,7 +3920,7 @@ mod tests {
let rr = crate::layout::textbox_corner_radius();
let fields = p.inner().fields();
assert_eq!(fields.len(), 1, "{fields:?}");
- let (fx, fy, fw, fh, fradii, _, split) = fields[0];
+ let (fx, fy, fw, fh, fradii, _, split, _) = fields[0];
assert_eq!((fx, fy, fx + fw, fy + fh), (tx, dy, dx + dw, dy + dh), "the field spans box and picker");
assert_eq!(fradii, (rr, rr, rr, rr), "its own radius at all four corners");
assert_eq!(split, dx, "the seam is where the picker begins");
@@ -3950,7 +3959,7 @@ mod tests {
let ty = d.label_strip();
let fields = p.inner().fields();
assert_eq!(fields.len(), 1, "{fields:?}");
- let (fx, fy, fw, fh, _, _, split) = fields[0];
+ let (fx, fy, fw, fh, _, _, split, _) = fields[0];
assert_eq!((fx, fy, fw, fh), (x, y + ty, w, h - ty), "on the trigger's band");
assert_eq!(split, x - FIELD_RUN_ONLY, "all run, no well");
let mut pc = PaintCtx::new();
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index 49de12e..20cca87 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -237,9 +237,19 @@ impl Input for Checkbox {
}
}
-/// A plate that slides in a well: the toggle's footprint is a track carved
-/// into the plate it sits on, and a half-width control plate stands on that
-/// well's floor at the left (off) or right (on) end, gliding between them.
+/// A field whose run glides: the toggle's footprint is ONE field
+/// ([`crate::scene::paint::Prim::Field`]) — a well carved into the plate it
+/// sits on, holding a flush run half its width, the run at the left end
+/// (off) or the right end (on) and gliding between them.
+///
+/// It is the same object as a text row with its picker, a spinbox and its
+/// -/+ run, and a dropdown trigger: a well in a plate with a flush plate in
+/// it, one outline round both. Those hold their run still at the right end
+/// (or are all run); a toggle's moves, and where it stands is the state.
+/// Until 2026-10-02 it was a recess with a raised boss standing on its
+/// floor, the one control whose nested plate stood ABOVE the surface where
+/// every other stood flush with it, and whose outline turned its own corner
+/// round the plate rather than running round the whole control.
///
/// That is the ONE toggle style. The rocker — two flat half faces with a
/// hinge between them, the state half tipped out toward the light — is gone,
@@ -255,11 +265,11 @@ pub struct Toggle {
/// Where the label sits across the track. Mirrors `Button::justify` — same enum, same
/// 8px edge inset — so the two read as one control set wherever they share a column.
justify: Justification,
- /// Relief style: the track is a real carved well and the glider a raised
- /// plate standing in it. Without it the track falls back to the hairline
- /// frame every well shares and the plate to a lit face — see `paint`.
+ /// Relief style: the toggle is a field, a carved well with a flush run in
+ /// it. Without it the well falls back to the hairline frame every well
+ /// shares and the run to a lit face — see `paint`.
raised: Option<bool>,
- /// The glider's animated position along its well, 0 (left/off) → 1 (right/on).
+ /// The run's animated position along its field, 0 (left/off) → 1 (right/on).
/// Chases `toggled` in `tick` after a click; programmatic state syncs
/// (`set_toggled`, `set_value_string`) snap it, so only user interaction
/// animates.
@@ -307,89 +317,44 @@ impl Toggle {
self.toggled
}
- /// The well the glider lives in: the toggle's whole footprint carved one
- /// step down. Taken through [`crate::layout::carve_inside`], so the walls
- /// stay inside the rect and the gap beside a toggle is the gap, exactly as
- /// a TextBox's well is taken. `(rect, per-corner radii, wall width)`.
+ /// The field's outline: the toggle's whole footprint, carved one step
+ /// down. Taken through [`crate::layout::carve_inside`], so the walls stay
+ /// inside the rect and the gap beside a toggle is the gap, exactly as a
+ /// TextBox's well and a spinbox's field are taken. `(rect, per-corner
+ /// radii, wall width)`.
///
- /// The SINGLE source for the track geometry: `paint` carves it here and
- /// [`Toggle::slide_plate`] measures the floor from it.
- pub fn well(&self, rect: Rect) -> (Rect, crate::scene::paint::Radii, f32) {
+ /// The SINGLE source for the toggle's geometry: `paint` draws it here,
+ /// [`Toggle::run`] places the run in it, and a host that draws the relief
+ /// itself (`ParametersBg::fields`) asks for both.
+ pub fn field(&self, rect: Rect) -> (Rect, crate::scene::paint::Radii, f32) {
let r = crate::layout::toggle_corner_radius();
let depth = crate::layout::bevel_width().min(rect.height * 0.2);
- let (well, radii) = crate::layout::carve_inside(rect, (r, r, r, r), depth);
- (well, radii, depth)
- }
-
- /// The sliding plate, as `(footprint, corner radius, wall width)`: half the
- /// well's floor wide, gliding between the floor's ends by the animated
- /// `slide_t` — left is off, right is on.
- ///
- /// It stands ON the floor — the flat region inside the well's walls, which
- /// start half a wall in from the well's own boundary — and its roll abuts
- /// that floor's edge instead of shading over the well's wall. Its corners
- /// run concentric with the well's.
- ///
- /// Its wall is HALF the well's. A plate in a well is the shallower part of
- /// the pair, and at a control's height it has to be: a toggle is 24px, a
- /// well wall 4.8, and two full-depth walls stacked leave the boss no flat
- /// top at all — its own walls meet in the middle and the plate reads as a
- /// ridge drawn across the track rather than a thing standing in it.
- pub fn slide_plate(&self, rect: Rect) -> (Rect, f32, f32) {
- let (well, radii, wd) = self.well(rect);
- let (floor, floor_r) = inset(well, radii.0, wd * 0.5);
- let pd = (wd * 0.5).max(1.0);
- let (travel, pr) = inset(floor, floor_r, pd * 0.5);
- let pw = travel.width * 0.5;
- let plate = Rect {
- x: travel.x + self.slide_t * (travel.width - pw),
- y: travel.y,
- width: pw,
- height: travel.height,
- };
- (plate, pr, pd)
- }
-
- /// The carves this Toggle paints: the track's well (a `Recess`) and the
- /// glider standing in it (a `Boss`, the faceless raised plate
- /// [`crate::scene::paint::PaintCtx::control_plate`] emits) — in that order,
- /// the order `paint` emits them.
- ///
- /// Exists because a Toggle paints NO fill in any state: on a legacy-view
- /// host (`ParametersBg::reliefs`) these carves are the ENTIRE control, and
- /// without them the row is a bare label. Empty without `raised` styling,
- /// where the frame and the lit face stand in for them.
- pub fn flat_carves(&self, rect: Rect) -> Vec<crate::layout::ReliefCarve> {
- use crate::layout::{CarveKind, ReliefCarve};
- if !self.raised() {
- return Vec::new();
- }
- let all = (true, true, true, true);
- let (well, well_radii, depth) = self.well(rect);
- let (plate, pr, pd) = self.slide_plate(rect);
- let (boss, boss_radii) = crate::layout::carve_inside(plate, (pr, pr, pr, pr), pd);
- vec![
- ReliefCarve {
- kind: CarveKind::Recess { tint: None },
- x: well.x,
- y: well.y,
- w: well.width,
- h: well.height,
- radii: well_radii,
- depth,
- edges: all,
- },
- ReliefCarve {
- kind: CarveKind::Boss { tint: None },
- x: boss.x,
- y: boss.y,
- w: boss.width,
- h: boss.height,
- radii: boss_radii,
- depth: pd,
- edges: all,
- },
- ]
+ let (field, radii) = crate::layout::carve_inside(rect, (r, r, r, r), depth);
+ (field, radii, depth)
+ }
+
+ /// The flush run, as the xs it spans in [`Toggle::field`]: half the field
+ /// wide, placed by the animated `slide_t` — flush with the field's left
+ /// end off, its right end on, a well on both sides between. As a text
+ /// row's picker is, the run is laid out a wall wider than the face it
+ /// carries (the field insets the face half a wall from the outline and
+ /// from the seam), so the face reaches the well.
+ pub fn run(&self, rect: Rect) -> (f32, f32) {
+ let (field, _, _) = self.field(rect);
+ let w = field.width * 0.5;
+ let x = field.x + self.slide_t * (field.width - w);
+ (x, x + w)
+ }
+
+ /// The run's FACE — what stands at the surface's level inside the
+ /// field's valley: the run inset half a wall on every side, its corners
+ /// the field's less that. `(rect, corner radius)`. The relief-off paint
+ /// lights this rect, where the field cannot be carved.
+ pub fn face(&self, rect: Rect) -> (Rect, f32) {
+ let (field, radii, depth) = self.field(rect);
+ let (a, b) = self.run(rect);
+ let run = Rect { x: a, y: field.y, width: b - a, height: field.height };
+ inset(run, radii.0, depth * 0.5)
}
}
@@ -448,47 +413,41 @@ impl Paint for Toggle {
}
fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
- use crate::scene::paint::{ControlPlate, PlateStance};
+ use crate::scene::paint::ControlPlate;
let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
// A toggle paints NO fill of its own: it is worked out of the plate it
// sits on, so the plate's own material (tint, blur, whatever it is)
- // shows through both the well's floor and the glider's face, and the
+ // shows through both the well's floor and the run's face, and the
// state reads from light and relief alone — the DE's transparent-face
// convention (closed dropdowns, inset troughs). The state colors this
// used to tint with (enabled/disabled/background) are retired with the
// rest of the toggle's palette.
- let (well, well_radii, depth) = self.well(rect);
- let (plate, plate_r, plate_depth) = self.slide_plate(rect);
+ let (field, field_radii, depth) = self.field(rect);
if self.raised() {
- // The track is a WELL — the same recess a TextBox carves — and the
- // glider is a control plate standing on its floor, raised faceless
- // (a `Boss`, the surface below as its face). Its position IS the
- // read: left off, right on, animated in `tick`. Both also reach
- // legacy-view hosts through `flat_carves` (see
- // `ParametersBg::reliefs`), which must emit them in this order.
- ctx.recess(well, well_radii, depth);
- let focus = if self.focused { Some(ControlPlate::focus_tint()) } else { None };
- ctx.control_plate(
- &ControlPlate::control(plate, plate_r, PlateStance::Raised, None)
- .with_depth(plate_depth)
- .with_tint(focus),
- );
+ // ONE field — the well and the flush run in it under one outline,
+ // as a text row's picker and a spinbox's -/+ run are drawn. The
+ // run's position IS the read: left off, right on, animated in
+ // `tick`. A host that draws the relief itself
+ // (`ParametersBg::fields`) asks for the same `field` and `run`.
+ // Focus lights the field's rim, the ring every field wears.
+ let focus = self.focused.then(ControlPlate::focus_tint);
+ ctx.field_run(field, field_radii, depth, self.run(rect), focus);
} else {
// Relief off: a well is its frame — the one hairline every well
// falls back to, lit while focused, exactly `well_rim`'s flat arm —
- // and the plate standing in it is a lit face, the neutral overlay
- // the DE gives a surface it cannot carve (what the rocker's halves
- // wore). Scaled by the relief strength, as that lighting was.
+ // and the run's face is a lit face, the neutral overlay the DE
+ // gives a surface it cannot carve (what the rocker's halves wore).
+ // Scaled by the relief strength, as that lighting was.
//
- // The plate's overlay is a ROUNDED RECT on purpose: the legacy
+ // The face's overlay is a ROUNDED RECT on purpose: the legacy
// reverse bridge reads only those, never a `Border`, so a
- // legacy-view host (`ParametersBg`, whose carves are gated on
- // relief) still shows which end the plate is at.
+ // legacy-view host still shows which end the run is at.
let bw = crate::layout::toggle_border_width().max(1.0);
- ctx.border(well, well_radii, [0.0; 4], colors::well_frame_color(self.hovered, self.focused), bw);
+ ctx.border(field, field_radii, [0.0; 4], colors::well_frame_color(self.hovered, self.focused), bw);
+ let (face, face_r) = self.face(rect);
let lit = (0.16 * (crate::layout::bevel_depth() / 0.15)).clamp(0.0, 0.5);
- ctx.rounded_rect(plate, plate_r, (true, true, true, true), [1.0, 1.0, 1.0, lit]);
+ ctx.rounded_rect(face, face_r, (true, true, true, true), [1.0, 1.0, 1.0, lit]);
}
if let Some(ref label) = self.label {
@@ -499,13 +458,13 @@ impl Paint for Toggle {
Justification::Right => x + w - est_w - crate::layout::CONTROL_TEXT_INSET,
Justification::Center => x + (w - est_w) / 2.0,
};
- // The label never moves with the state. When the glider covers it
- // the text shows through: the plate carries no face of its own, and
+ // The label never moves with the state. When the run covers it
+ // the text shows through: the run carries no face of its own, and
// the glyphs land in the engine's later text pass either way.
//
- // Focus is the glider plate's own lit rim (`ControlPlate::with_tint`)
- // — the ring every other plate wears, which the rocker's partial
- // carves could not — so the label stays the label.
+ // Focus is the field's own lit rim — the ring every other field
+ // wears, which the rocker's partial carves could not — so the
+ // label stays the label.
ctx.text_with(
label.clone(),
tx,
@@ -685,7 +644,7 @@ mod tests {
}
#[test]
- fn toggle_click_glides_the_plate_across_its_well() {
+ fn toggle_click_glides_the_run_across_its_field() {
let mut ctx = UiContext::new();
let mut t = Toggle::new();
let (id, ptr) = (t.id(), t.as_ptr_mut());
@@ -699,7 +658,7 @@ mod tests {
pc.finish().items.into_iter().map(|i| format!("{:?}", i.prim)).collect::<Vec<_>>()
};
let before = painted(&t);
- let plate_x = |t: &Adapted<Toggle>| t.inner().slide_plate(rect).0.x;
+ let plate_x = |t: &Adapted<Toggle>| t.inner().run(rect).0;
let left = plate_x(&t);
assert!(ctx.propagate_event(&click_at(30.0, 15.0), id), "toggle consumed the click");
@@ -720,57 +679,48 @@ mod tests {
assert_eq!(WidgetHost::preferred_height(&t), Some(crate::layout::toggle_height()));
}
- /// The plate stands ON the well's floor, clear of its walls by one wall
- /// width on every side, and travels between the floor's ends: off is flush
- /// left, on is flush right, and it never reaches outside the toggle's rect.
+ /// The toggle is ONE field, the form a text row's picker and a spinbox's
+ /// -/+ run take: off, its run is the left half and the well the right;
+ /// on, the other way round; between, a well either side. The run's face
+ /// stands half a wall inside the run, so the run reaches the well.
#[test]
- fn toggle_plate_lives_inside_the_well() {
+ fn a_toggle_is_a_field_whose_run_glides() {
+ use crate::scene::paint::{Prim, FIELD_RUN_ONLY};
let rect = Rect { x: 10.0, y: 4.0, width: 120.0, height: 24.0 };
- let mut t = Toggle::new();
+ let mut t = Toggle::new().with_raised(true);
+ let (field, radii, depth) = t.inner().field(rect);
+ assert!(field.x >= rect.x && field.y >= rect.y, "the field carves inside the rect");
+ let (fl, fr) = (field.x, field.x + field.width);
- let (well, _, depth) = t.inner().well(rect);
- assert!(well.x >= rect.x && well.y >= rect.y, "the well carves inside the rect");
-
- // The floor is the flat region inside the well's walls; the plate's own
- // (half-depth) roll abuts its edge, so the plate is inset one more
- // half-wall of its own from there.
- let (off, _, pd) = t.inner().slide_plate(rect);
- assert!((pd - depth * 0.5).abs() < 1e-4, "the plate's wall is half the well's");
- let edge = depth * 0.5 + pd * 0.5;
- assert!((off.x - (well.x + edge)).abs() < 1e-4, "off sits at the travel's left end");
- assert!((off.y - (well.y + edge)).abs() < 1e-4, "and clear of the floor's top");
- assert!(
- off.height > pd * 2.0,
- "the plate keeps a flat top: a boss no taller than its own wall is a ridge",
- );
+ let fields = |t: &Adapted<Toggle>| -> Vec<Prim> {
+ let mut pc = crate::scene::paint::PaintCtx::new();
+ crate::widget::Paint::paint(t.inner(), rect, &mut pc);
+ pc.finish().items.into_iter().map(|i| i.prim).filter(|p| !matches!(p, Prim::Text { .. })).collect()
+ };
+ let off = fields(&t);
+ assert_eq!(off.len(), 1, "one prim, the field: {off:?}");
+ let Prim::Field { rect: r, radii: rr, depth: d, split, end, tint } = off[0] else { panic!("{off:?}") };
+ assert_eq!((r, rr, d, tint), (field, radii, depth, None));
+ assert_eq!(split, fl, "off: the run reaches the field's left end — no well there");
+ assert!((end - (fl + field.width * 0.5)).abs() < 1e-4, "half the field, the well beyond it");
t.set_toggled(true); // programmatic syncs snap, so this is the on-end geometry
- let (on, _, _) = t.inner().slide_plate(rect);
- assert!(on.x > off.x, "on is to the right of off");
- assert!(
- (on.x + on.width - (well.x + well.width - edge)).abs() < 1e-4,
- "on sits flush against the travel's right end",
- );
- assert!(on.x + on.width <= rect.x + rect.width, "and never leaves the toggle's rect");
- assert!((off.width * 2.0 - (well.width - 2.0 * edge)).abs() < 1e-4, "half the travel wide");
- }
-
- /// The carves a legacy-view host re-emits (`ParametersBg::reliefs`) are the
- /// well then the plate, in the order `paint` emits them — and nothing at
- /// all with relief off, where the flat frames stand in.
- #[test]
- fn toggle_flat_carves_are_the_well_then_the_plate() {
- use crate::layout::CarveKind;
- let rect = Rect { x: 0.0, y: 0.0, width: 120.0, height: 24.0 };
- let t = Toggle::new().with_raised(true);
- let carves = t.inner().flat_carves(rect);
- assert_eq!(carves.len(), 2);
- assert!(matches!(carves[0].kind, CarveKind::Recess { .. }), "the track's well first");
- assert!(matches!(carves[1].kind, CarveKind::Boss { .. }), "then the plate standing in it");
- assert!(carves.iter().all(|c| c.edges == (true, true, true, true)), "both are full rings");
-
- let flat = Toggle::new().with_raised(false);
- assert!(flat.inner().flat_carves(rect).is_empty(), "relief off carves nothing");
+ let Prim::Field { split, end, .. } = fields(&t)[0] else { panic!() };
+ assert!((split - (fl + field.width * 0.5)).abs() < 1e-4 && (end - fr).abs() < 1e-4, "on: the right half");
+
+ t.slide_t = 0.5;
+ let Prim::Field { split, end, .. } = fields(&t)[0] else { panic!() };
+ assert!(split > fl + 1.0 && end < fr - 1.0, "mid-glide, a well either side of the run");
+ assert!(end < fr + FIELD_RUN_ONLY, "and the run's end is its own, not the field's");
+
+ let (face, face_r) = t.inner().face(rect);
+ let (a, b) = t.inner().run(rect);
+ assert!((face.x - (a + depth * 0.5)).abs() < 1e-4 && (face.x + face.width - (b - depth * 0.5)).abs() < 1e-4);
+ assert!((face_r - (radii.0 - depth * 0.5).max(0.0)).abs() < 1e-4, "concentric with the field");
+
+ // Focus lights the field's rim.
+ t.focused = true;
+ assert!(matches!(fields(&t)[0], Prim::Field { tint: Some(_), .. }), "focus tints the field");
}
#[test]