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

commit218f8fcf76f765e07d42a3c17b62fa8ed23d0151
parent44ab9aca08
authorLucas Galante <[email protected]>
date2026-09-08 10:26
feat(checkbox): a round style — the ring-and-dot mark cce-list drew by hand

`style.control.checkbox.style = "round"` (or `set_round(true)` on the model)
swaps the square box for a 14px ring in TEXT_DIM with a TOGGLE_ON dot when
checked, drawn on the LEFT of the label the way a list row reads; standalone,
the mark fills the rect. The drawing is `Checkbox::paint_round_mark`, a
public associated fn, so a host that paints its own rows (cce-list) draws the
identical mark instead of a private copy. The square style is unchanged and
stays the default.

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

 src/layout.rs                | 14 +++++++++++
 src/widget/input/checkbox.rs | 55 +++++++++++++++++++++++++++++++++++++++++++-
 2 files changed, 68 insertions(+), 1 deletion(-)

diff --git a/src/layout.rs b/src/layout.rs
index 29ded73..954759c 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -108,6 +108,7 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
                 "style.control.textbox.multiline.line_wrap" | "style.textbox.multiline.line_wrap" | "style.data.textbox.multiline.line_wrap" => "textbox_line_wrap",
                 "style.control.textbox.multiline.border_width" | "style.textbox.multiline.border_width" | "style.data.textbox.multiline.border_width" => "textbox_multiline_border_width",
                 "style.control.toggle.style" => "toggle_style",
+                "style.control.checkbox.style" => "checkbox_style",
                 "style.control.toggle.height" => "toggle_height",
                 "style.control.toggle.border_width" => "toggle_border_width",
                 "style.control.toggle.disabled_color" => "toggle_disabled_color",
@@ -1867,6 +1868,19 @@ pub fn slider_corner_radius() -> f32 {
     get_style_registry().read().unwrap().get_float("slider_corner_radius").unwrap_or(4.0)
 }
 
+/// The checkbox's render style: `style.control.checkbox.style = "round"` swaps
+/// the square box for the ring-and-dot mark (see `Checkbox::paint_round_mark`),
+/// with the mark on the LEFT of the label the way a list row reads. Anything
+/// else is the square style.
+pub fn checkbox_round() -> bool {
+    lazy_init_style_registry();
+    get_style_registry()
+        .read()
+        .unwrap()
+        .get_string("checkbox_style")
+        .is_some_and(|s| s == "round")
+}
+
 /// The toggle's render style: `style.control.toggle.style = "slide"` swaps the
 /// rocker/gradient pill for a half-width button that slides between the left
 /// (off) and right (on) ends of the widget (see `Toggle::paint`). Anything
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index a8ca78c..04bd68c 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -31,9 +31,15 @@ pub struct Checkbox {
     label: Option<String>,
     hovered: bool,
     focused: bool,
+    /// Round style: a ring-and-dot mark on the left of the label instead of the
+    /// square box on the right. Seeded from `style.control.checkbox.style`.
+    round: bool,
 }
 
 impl Checkbox {
+    /// The round mark's radius: a 14px disc, the size cce-list's rows always drew.
+    pub const ROUND_RADIUS: f32 = 7.0;
+
     pub fn new() -> Adapted<Checkbox> {
         Adapted::new(Checkbox {
             checked: false,
@@ -42,6 +48,7 @@ impl Checkbox {
             label: None,
             hovered: false,
             focused: false,
+            round: crate::layout::checkbox_round(),
         })
     }
 
@@ -53,6 +60,33 @@ impl Checkbox {
         self.checked
     }
 
+    /// Select the round style regardless of config (a list row wants it whatever
+    /// the DE-wide setting says).
+    pub fn set_round(&mut self, round: bool) {
+        self.round = round;
+    }
+
+    pub fn round(&self) -> bool {
+        self.round
+    }
+
+    /// The round style's 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) {
+        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],
+            colors::TEXT_DIM,
+            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 {
@@ -86,7 +120,7 @@ impl Paint for Checkbox {
         // is the colored box itself. Style-property paths (demo `widget_vertices`) read this.
         // Width isn't known here, so mirror the legacy intent via the label: labeled checkboxes
         // are the wide rows.
-        if self.label.is_some() {
+        if self.label.is_some() || self.round {
             [0.0, 0.0, 0.0, 0.0]
         } else {
             self.box_color()
@@ -99,6 +133,25 @@ impl Paint for Checkbox {
 
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
         let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
+        if self.round {
+            // Round style: the mark leads and the label follows, a list row's
+            // reading order. Standalone, the mark fills the rect.
+            let r = if self.label.is_some() { Self::ROUND_RADIUS } else { (w.min(h) / 2.0).max(1.0) };
+            let (cx, cy) = if self.label.is_some() { (x + r, y + h / 2.0) } else { (x + w / 2.0, y + h / 2.0) };
+            Self::paint_round_mark(ctx, cx, cy, r, self.checked);
+            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);
+                ctx.text(
+                    label.clone(),
+                    cx + r + 8.0,
+                    ty,
+                    font_size,
+                    colors::control_label_color_for_state(self.hovered, self.focused),
+                );
+            }
+            return;
+        }
         if w > 30.0 {
             // Wide mode: label on the left, 18px box on the right — the legacy `extra_quads`
             // geometry verbatim.