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

commit27639d880121f0164cc76407f91402e1995c4a33
parent38e45fbf4a
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-02 14:59
feat: an inline check is the checkbox's field

A check drawn inline -- cce-list's rows, a markdown task item, the doc
editor -- is the Checkbox widget's box: Checkbox::paint_inline(ctx, cx,
cy, half, checked), an empty well, or checked a run standing in its
middle. Both build it through Checkbox::box_field, so they cannot
disagree. The corner is the toggle's in proportion to the box's size,
which is the toggle's corner exactly at a toggle's height and keeps a
14px box a rounded square where the radius taken whole made it a disc.

paint_round_mark, paint_round_mark_ringed and ROUND_RADIUS are gone;
INLINE_HALF is the 14px box's half-side.

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

 CLAUDE.md                    |  26 +++++---
 src/widget/doc_editor/mod.rs |   2 +-
 src/widget/input/checkbox.rs | 144 ++++++++++++++++++++++++++-----------------
 src/widget/markdown.rs       |   2 +-
 4 files changed, 108 insertions(+), 66 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 03e4913..a585991 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -457,11 +457,20 @@ square in the rect when it has none — an empty well unchecked, and checked a r
 half its width standing in its middle, the well either side. The toggle's object
 with no travel: its run is there or not. A box FILLED with its run when checked
 was tried first and dropped: at a control's size an all-run field's outline is
-an empty well's, and the two states were hard to tell apart in a render. It drew
-the ring-and-dot mark until 2026-10-02; that mark stays
-(`Checkbox::paint_round_mark`) for what draws a check inline in text, where there
-is no plate to cut a well in. In a parameter pane a `checkbox` row has always
-been a Toggle. `a_checkbox_is_a_field_with_a_run_in_it_or_not` is the test.
+an empty well's, and the two states were hard to tell apart in a render. In a
+parameter pane a `checkbox` row has always been a Toggle.
+`a_checkbox_is_a_field_with_a_run_in_it_or_not` is the test.
+
+**A check drawn inline is the same box** (`Checkbox::paint_inline(ctx, cx, cy,
+half, checked)`): cce-list's rows, a markdown task item, the doc editor. Both it
+and the widget build the field through `Checkbox::box_field(square, checked)`, so
+they cannot disagree; the corner is the toggle's IN PROPORTION (its radius over
+its height), which is the toggle's corner exactly at a toggle's height and keeps a
+14px box (`Checkbox::INLINE_HALF`, cce-list's) a rounded square where the
+toggle's radius taken whole would make it a disc. They drew a ring with a blue
+dot until the same day (`paint_round_mark`, gone). Rendered at 10–14px it reads at
+1x and 2x, the smallest least clearly; the state is the plate, with no colour.
+`an_inline_check_is_the_widgets_box` is the test.
 
 ### A grouped carve shades as its overlay does (since 2026-10-02)
 
@@ -618,10 +627,9 @@ widget does not fit one of them, say so rather than stretching a word.
   text/swatch split. One silhouette, one relief pass, seams between. A
   `Separator` is the same cut made in the plate it sits on, with no segment
   to part: a groove that dies out at its own ends (flat: a hairline).
-- **Marks and bands sit outside this vocabulary on purpose.** The round
-  check mark drawn inline in text (`Checkbox::paint_round_mark`: cce-list's
-  rows, a markdown task item, the doc editor) is a mark — the `Checkbox`
-  WIDGET is a field; the Slider's swelling band is a band. Do not call
+- **Bands sit outside this vocabulary on purpose.** The Slider's swelling
+  band is a band. (The round Checkbox mark was the other exception, a mark;
+  since 2026-10-02 a check box is a field wherever it is drawn.) Do not call
   them plates or wells.
 
 What this buys, and where the code is heading:
diff --git a/src/widget/doc_editor/mod.rs b/src/widget/doc_editor/mod.rs
index acdf4ac..6b4a992 100644
--- a/src/widget/doc_editor/mod.rs
+++ b/src/widget/doc_editor/mod.rs
@@ -914,7 +914,7 @@ impl DocEditor {
                     match d {
                         Deco::Quad(r, c) => pc.quad(Rect { x: ox + r.x, y: top + r.y, width: r.width, height: r.height }, *c),
                         Deco::Dot { cx, cy, r, color } => pc.circle(ox + cx, top + cy, *r, *color),
-                        Deco::Check { cx, cy, r, checked } => crate::widget::Checkbox::paint_round_mark(pc, ox + cx, top + cy, *r, *checked),
+                        Deco::Check { cx, cy, r, checked } => crate::widget::Checkbox::paint_inline(pc, ox + cx, top + cy, *r, *checked),
                         Deco::Text { text, x, y, size, color, font } => {
                             pc.text_with(text.clone(), ox + x, top + y, *size, srgb_u8(*color), Some(font.clone()), None)
                         }
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index e2d9426..6b4c22f 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -23,6 +23,25 @@ use crate::widget::{
 /// is the empty well's, and the two states were hard to tell apart.)
 const RUN_SHARE: f32 = 0.5;
 
+/// Paint a check box's field ([`Checkbox::box_field`]): the field itself
+/// with relief on; off, as the toggle's — a well is its frame, the hairline
+/// every well falls back to (lit by `hovered` / `focused`), and a checked
+/// box's run a lit face in it.
+fn paint_box(ctx: &mut PaintCtx, field: &Field, hovered: bool, focused: bool) {
+    if crate::layout::control_relief() {
+        ctx.field(field);
+        return;
+    }
+    let bw = crate::layout::toggle_border_width().max(1.0);
+    ctx.border(field.rect, field.radii, [0.0; 4], colors::well_frame_color(hovered, focused), bw);
+    if let Some((a, b)) = field.run_span() {
+        let run = Rect { x: a, y: field.rect.y, width: b - a, height: field.rect.height };
+        let (face, face_r) = inset(run, field.radii.0, field.depth * 0.5);
+        let lit = (0.16 * (crate::layout::bevel_depth() / 0.15)).clamp(0.0, 0.5);
+        ctx.rounded_rect(face, face_r, (true, true, true, true), [1.0, 1.0, 1.0, lit]);
+    }
+}
+
 /// A rect shrunk by `g` on every side, its uniform corner radius shrunk to
 /// match so the inner silhouette stays concentric with the outer one.
 fn inset(rect: Rect, radius: f32, g: f32) -> (Rect, f32) {
@@ -53,10 +72,9 @@ fn parse_bool(val: &str) -> Option<bool> {
 /// check box's is there or not. Standalone, the box is the largest square in
 /// the rect, centred.
 ///
-/// Until 2026-10-02 it drew the ring-and-dot MARK, which is still
-/// [`Checkbox::paint_round_mark`] for what draws a check inline in text — a
-/// list row (cce-list), a markdown task item, the doc editor — where there is
-/// no plate to cut a well in.
+/// A check drawn inline in text — a list row (cce-list), a markdown task
+/// item, the doc editor — is the same box, through [`Checkbox::paint_inline`].
+/// Until 2026-10-02 the widget and all of those drew a ring-and-dot mark.
 pub struct Checkbox {
     checked: bool,
     just_clicked: bool,
@@ -67,14 +85,15 @@ pub struct Checkbox {
 }
 
 impl Checkbox {
-    /// The inline mark's radius: a 14px disc ([`Checkbox::paint_round_mark`]).
-    pub const ROUND_RADIUS: f32 = 7.0;
+    /// Half the side of an inline box ([`Checkbox::paint_inline`]) at a
+    /// list row's size: a 14px square, the size of the round mark it
+    /// replaced.
+    pub const INLINE_HALF: f32 = 7.0;
 
     /// The box as the [`Field`] it is: a square as tall as the control (at
     /// most a toggle's height) at the rect's left when labelled, the largest
-    /// square in the rect, centred, when not — carved inside that square
-    /// through [`crate::layout::carve_inside`], as every field is. All well
-    /// unchecked; checked, a run in the middle; lit while focused.
+    /// square in the rect, centred, when not ([`Checkbox::box_field`]). Lit
+    /// while focused.
     pub fn field(&self, rect: Rect) -> Field {
         let side = if self.label.is_some() {
             rect.height.min(crate::layout::toggle_height())
@@ -84,15 +103,37 @@ impl Checkbox {
         .max(1.0);
         let x = if self.label.is_some() { rect.x } else { rect.x + (rect.width - side) * 0.5 };
         let square = Rect { x, y: rect.y + (rect.height - side) * 0.5, width: side, height: side };
-        let r = crate::layout::toggle_corner_radius().min(side * 0.5);
+        Self::box_field(square, self.checked).with_tint(self.focused.then(crate::scene::paint::ControlPlate::focus_tint))
+    }
+
+    /// A check box on `square` — its footprint — as a [`Field`], carved
+    /// inside it through [`crate::layout::carve_inside`] as every field is:
+    /// all well unchecked; checked, a run [`RUN_SHARE`] of its width in the
+    /// middle. The ONE description the widget and every inline box share.
+    ///
+    /// The corner is the toggle's in PROPORTION — its radius over its height
+    /// — so a box as tall as a toggle has the toggle's corner exactly, and a
+    /// 14px box in a line of text is a rounded square rather than a disc,
+    /// which the toggle's radius taken whole would make it.
+    pub fn box_field(square: Rect, checked: bool) -> Field {
+        let side = square.width.min(square.height).max(1.0);
+        let r = (crate::layout::toggle_corner_radius() * side / crate::layout::toggle_height().max(1.0)).min(side * 0.5);
         let depth = crate::layout::bevel_width().min(side * 0.2);
         let (outline, radii) = crate::layout::carve_inside(square, (r, r, r, r), depth);
-        let field = if self.checked {
+        if checked {
             Field::sliding_run(outline, radii, depth, outline.width * RUN_SHARE, 0.5)
         } else {
             Field::well(outline, radii, depth)
-        };
-        field.with_tint(self.focused.then(crate::scene::paint::ControlPlate::focus_tint))
+        }
+    }
+
+    /// Paint a check box centred on (`cx`, `cy`), `half` its half-side, for
+    /// a host that draws one inline — cce-list's rows, a markdown task item,
+    /// the doc editor — where the widget would be a whole control. The same
+    /// box the widget draws ([`Checkbox::box_field`]), relief on or off.
+    pub fn paint_inline(ctx: &mut PaintCtx, cx: f32, cy: f32, half: f32, checked: bool) {
+        let square = Rect { x: cx - half, y: cy - half, width: 2.0 * half, height: 2.0 * half };
+        paint_box(ctx, &Self::box_field(square, checked), false, false);
     }
 
     pub fn new() -> Adapted<Checkbox> {
@@ -114,30 +155,6 @@ impl Checkbox {
         self.checked
     }
 
-    /// The mark, centred on (`cx`, `cy`): a dim ring, and a solid dot in the
-    /// toggle-on colour when checked. Shared with hosts that draw their own rows
-    /// (cce-list) so a list's marks and a `Checkbox` agree pixel for pixel.
-    pub fn paint_round_mark(ctx: &mut PaintCtx, cx: f32, cy: f32, radius: f32, checked: bool) {
-        Self::paint_round_mark_ringed(ctx, cx, cy, radius, checked, colors::TEXT_DIM);
-    }
-
-    /// [`Checkbox::paint_round_mark`] with the ring in `ring` — the mark's
-    /// silhouette lit in the highlight colour is its keyboard-focus ring (a
-    /// mark is not a plate, so it has no rim to tint; the ring it already
-    /// draws is the silhouette).
-    pub fn paint_round_mark_ringed(ctx: &mut PaintCtx, cx: f32, cy: f32, radius: f32, checked: bool, ring: [f32; 4]) {
-        ctx.border(
-            Rect { x: cx - radius, y: cy - radius, width: 2.0 * radius, height: 2.0 * radius },
-            (radius, radius, radius, radius),
-            [0.0, 0.0, 0.0, 0.0],
-            ring,
-            1.5,
-        );
-        if checked {
-            ctx.circle(cx, cy, radius - 3.0, colors::TOGGLE_ON);
-        }
-    }
-
     /// Hover state, also settable directly for immediate-mode hosts that do their own
     /// hit-testing instead of routing `MouseEnter`/`MouseLeave` (json_layout).
     pub fn hovered(&self) -> bool {
@@ -157,7 +174,7 @@ impl Layout for Checkbox {
 
 impl Paint for Checkbox {
     fn color(&self) -> [f32; 4] {
-        // The mark is painted; the widget itself has no background.
+        // The box is carved; the widget itself has no background.
         [0.0, 0.0, 0.0, 0.0]
     }
 
@@ -173,21 +190,7 @@ impl Paint for Checkbox {
         let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
         // The box leads and the label follows, a list row's reading order.
         let field = self.field(rect);
-        if crate::layout::control_relief() {
-            ctx.field(&field);
-        } else {
-            // Relief off, as the toggle's: a well is its frame — the hairline
-            // every well falls back to, lit while focused — and a checked
-            // box's run a lit face in it.
-            let bw = crate::layout::toggle_border_width().max(1.0);
-            ctx.border(field.rect, field.radii, [0.0; 4], colors::well_frame_color(self.hovered, self.focused), bw);
-            if let Some((a, b)) = field.run_span() {
-                let run = Rect { x: a, y: field.rect.y, width: b - a, height: field.rect.height };
-                let (face, face_r) = inset(run, field.radii.0, field.depth * 0.5);
-                let lit = (0.16 * (crate::layout::bevel_depth() / 0.15)).clamp(0.0, 0.5);
-                ctx.rounded_rect(face, face_r, (true, true, true, true), [1.0, 1.0, 1.0, lit]);
-            }
-        }
+        paint_box(ctx, &field, self.hovered, self.focused);
         if let Some(ref label) = self.label {
             let (_, font_size) = crate::layout::control_label_font_parsed();
             let ty = crate::layout::align_text_y(y, h, font_size, 0.0);
@@ -659,8 +662,8 @@ mod tests {
         assert!(cb.take_change(), "set_value_string marked the change");
     }
 
-    /// A labelled checkbox paints its ring-and-dot mark on the left and the label after
-    /// it: no quads at all, one circle through the bridge once checked.
+    /// A labelled checkbox is a square field at the left of its label: an
+    /// empty well, or checked a run standing in its middle.
     #[test]
     fn a_checkbox_is_a_field_with_a_run_in_it_or_not() {
         use crate::scene::paint::Prim;
@@ -709,6 +712,37 @@ mod tests {
         let _ = &ctx;
     }
 
+    /// An inline check — a list row's, a markdown task item's — is the
+    /// widget's box at the size it is given: the same field, its corner the
+    /// toggle's in proportion, so a 14px box is a rounded square and not a
+    /// disc, and a box a toggle tall has the toggle's corner exactly.
+    #[test]
+    fn an_inline_check_is_the_widgets_box() {
+        use crate::scene::paint::Prim;
+        let h = Checkbox::INLINE_HALF;
+        let square = Rect { x: 50.0 - h, y: 20.0 - h, width: 2.0 * h, height: 2.0 * h };
+        for checked in [false, true] {
+            let mut pc = crate::scene::paint::PaintCtx::new();
+            Checkbox::paint_inline(&mut pc, 50.0, 20.0, h, checked);
+            let prims: Vec<Prim> = pc.finish().items.into_iter().map(|i| i.prim).collect();
+            let want = Checkbox::box_field(square, checked);
+            if crate::layout::control_relief() {
+                let mut expect = crate::scene::paint::PaintCtx::new();
+                expect.field(&want);
+                let expect: Vec<Prim> = expect.finish().items.into_iter().map(|i| i.prim).collect();
+                assert_eq!(format!("{prims:?}"), format!("{expect:?}"), "the box_field, painted");
+            }
+            assert!(want.run_span().is_some() == checked, "a run when checked, none when not");
+            assert!(want.radii.0 < want.rect.width * 0.5 - 0.5, "a rounded square, not a disc: {:?}", want.radii);
+        }
+        // A box a toggle tall has the toggle's corner, as the widget's always had.
+        let th = crate::layout::toggle_height();
+        let big = Checkbox::box_field(Rect { x: 0.0, y: 0.0, width: th, height: th }, false);
+        let r = crate::layout::toggle_corner_radius().min(th * 0.5);
+        let (_, radii) = crate::layout::carve_inside(Rect { x: 0.0, y: 0.0, width: th, height: th }, (r, r, r, r), big.depth);
+        assert_eq!(big.radii, radii);
+    }
+
     #[test]
     fn toggle_click_glides_the_run_across_its_field() {
         let mut ctx = UiContext::new();
diff --git a/src/widget/markdown.rs b/src/widget/markdown.rs
index 50599cc..1746a6f 100644
--- a/src/widget/markdown.rs
+++ b/src/widget/markdown.rs
@@ -168,7 +168,7 @@ impl Layout {
                 }
                 Draw::Check { cx, cy, r, checked } => {
                     let (x, y) = at(*cx, *cy);
-                    crate::widget::Checkbox::paint_round_mark(pc, x, y, r * k, *checked)
+                    crate::widget::Checkbox::paint_inline(pc, x, y, r * k, *checked)
                 }
                 Draw::Dot { cx, cy, r, color } => {
                     let (x, y) = at(*cx, *cy);