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

commit79cead898e7efb9562a200975512ed19ceeb1b4c
parentd8e496a051
authorLucas Galante <[email protected]>
date2026-09-08 20:31
feat(layout): radii configured per rung — style.control.corner_radius

The control rung gets its default: `style.control.corner_radius`
(`control_corner_radius`, default 8, the DE's standard). Every control-scale
radius getter — button, dropdown, font selector, slider, spinbox, textbox,
toggle, and the list and tree wells — falls back to it when the widget's own
`corner_radius` key is unset, so the per-widget keys are overrides rather
than nine separately-defaulted knobs (each used to default to 4, which is how
the ColorSelector's swatch drifted from the field beside it). The pane rung
already fell back to the root's.

Verified: the gallery renders identically in a shadow whose config carries
only the rung key against one carrying every per-widget key; a mapping test
pins `control corner_radius=8 { button corner_radius=10 }` flattening to the
rung key beside the override.

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

 CLAUDE.md     | 14 +++++++++-----
 src/layout.rs | 53 ++++++++++++++++++++++++++++++++++++++++++++---------
 2 files changed, 53 insertions(+), 14 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 3177fe4..71a16af 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -148,11 +148,15 @@ What this buys, and where the code is heading:
   FontSelector, Breadcrumb and the ButtonStrip's selected plateau draw through
   it; the migration was prim-identical against a dump of every face. A new
   control face goes through `ControlPlate`, never a hand-rolled carve.
-- **Radii are configured per rung, overridden per widget.** Today every
-  control has its own `corner_radius` key with a separate default, which is how
-  the ColorSelector's swatch drifted to 4px while the field beside it used 8.
-  The intended shape is a default radius per rung (root, pane, control) with
-  the per-widget keys as overrides.
+- **Radii are configured per rung, overridden per widget.** Root:
+  `style.surface.plate.root.corner_radius` (`color::root_plate_corner_radius`).
+  Pane: `plate_corner_radius`, falling back to the root's. Control:
+  `style.control.corner_radius` (`layout::control_corner_radius`, default 8) —
+  every control-scale getter (button, dropdown, font selector, slider,
+  spinbox, textbox, toggle, list and tree wells; the ColorSelector's two via
+  the textbox) falls back to it when the widget's own `corner_radius` key is
+  unset, so a per-widget key is an override, not a requirement. Do not give a
+  new control-scale radius getter a literal default; fall back to the rung.
 
 ## The `scene/` core rebuild (read `docs/rfc-core-rebuild.md` before touching it)
 
diff --git a/src/layout.rs b/src/layout.rs
index 2d86a7e..cd635df 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -67,6 +67,9 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
                 "style.list.font_color" | "style.data.list.font_color" => "list_font_color",
                 "style.control.breadcrumb.font" => "breadcrumb_font",
 
+                // The control rung's default radius: every control-scale
+                // `corner_radius` below falls back to it when its own key is unset.
+                "style.control.corner_radius" => "control_corner_radius",
                 "style.control.button.padding" => "button_padding",
                 "style.control.button.height" => "button_height",
                 "style.control.button.corner_radius" => "button_corner_radius",
@@ -1854,7 +1857,7 @@ pub fn control_relief() -> bool {
 
 pub fn toggle_corner_radius() -> f32 {
     lazy_init_style_registry();
-    get_style_registry().read().unwrap().get_float("toggle_corner_radius").unwrap_or(4.0)
+    get_style_registry().read().unwrap().get_float("toggle_corner_radius").unwrap_or_else(control_corner_radius)
 }
 
 pub fn set_toggle_corner_radius(radius: f32) {
@@ -1894,7 +1897,7 @@ pub fn set_toggle_border_width(width: f32) {
 
 pub fn slider_corner_radius() -> f32 {
     lazy_init_style_registry();
-    get_style_registry().read().unwrap().get_float("slider_corner_radius").unwrap_or(4.0)
+    get_style_registry().read().unwrap().get_float("slider_corner_radius").unwrap_or_else(control_corner_radius)
 }
 
 /// The toggle's render style: `style.control.toggle.style = "slide"` swaps the
@@ -2776,9 +2779,28 @@ pub fn set_color_selector_corner_radius(radius: f32) {
     }
 }
 
+/// The control rung's corner radius (`style.control.corner_radius`): the
+/// default every control-scale radius getter falls back to when the widget's
+/// own `corner_radius` key is unset — buttons, dropdowns, font selectors,
+/// sliders, spinboxes, text boxes, toggles, and the list and tree wells. The
+/// per-widget keys are overrides on top of it. See "Plates, wells and seams"
+/// in `CLAUDE.md`; the pane and root rungs are `plate_corner_radius` and
+/// `crate::color::root_plate_corner_radius`.
+pub fn control_corner_radius() -> f32 {
+    lazy_init_style_registry();
+    get_style_registry().read().unwrap().get_float("control_corner_radius").unwrap_or(8.0)
+}
+
+pub fn set_control_corner_radius(radius: f32) {
+    lazy_init_style_registry();
+    if let Ok(mut registry) = get_style_registry().write() {
+        registry.set_float("control_corner_radius", radius);
+    }
+}
+
 pub fn button_corner_radius() -> f32 {
     lazy_init_style_registry();
-    get_style_registry().read().unwrap().get_float("button_corner_radius").unwrap_or(4.0)
+    get_style_registry().read().unwrap().get_float("button_corner_radius").unwrap_or_else(control_corner_radius)
 }
 
 pub fn set_button_corner_radius(radius: f32) {
@@ -2790,7 +2812,7 @@ pub fn set_button_corner_radius(radius: f32) {
 
 pub fn spinbox_corner_radius() -> f32 {
     lazy_init_style_registry();
-    get_style_registry().read().unwrap().get_float("spinbox_corner_radius").unwrap_or(4.0)
+    get_style_registry().read().unwrap().get_float("spinbox_corner_radius").unwrap_or_else(control_corner_radius)
 }
 
 pub fn set_spinbox_corner_radius(radius: f32) {
@@ -2940,7 +2962,7 @@ pub fn set_spinbox_button_padding(padding: f32) {
 
 pub fn textbox_corner_radius() -> f32 {
     lazy_init_style_registry();
-    get_style_registry().read().unwrap().get_float("textbox_corner_radius").unwrap_or(4.0)
+    get_style_registry().read().unwrap().get_float("textbox_corner_radius").unwrap_or_else(control_corner_radius)
 }
 
 pub fn set_textbox_corner_radius(radius: f32) {
@@ -2989,7 +3011,7 @@ pub fn set_textbox_multiline_border_width(width: f32) {
 
 pub fn list_corner_radius() -> f32 {
     lazy_init_style_registry();
-    get_style_registry().read().unwrap().get_float("list_corner_radius").unwrap_or(4.0)
+    get_style_registry().read().unwrap().get_float("list_corner_radius").unwrap_or_else(control_corner_radius)
 }
 
 pub fn set_list_corner_radius(radius: f32) {
@@ -3001,7 +3023,7 @@ pub fn set_list_corner_radius(radius: f32) {
 
 pub fn tree_corner_radius() -> f32 {
     lazy_init_style_registry();
-    get_style_registry().read().unwrap().get_float("tree_corner_radius").unwrap_or(4.0)
+    get_style_registry().read().unwrap().get_float("tree_corner_radius").unwrap_or_else(control_corner_radius)
 }
 
 pub fn set_tree_corner_radius(radius: f32) {
@@ -3160,7 +3182,7 @@ pub fn set_graph_connector_activation_radius(radius: f32) {
 
 pub fn font_selector_corner_radius() -> f32 {
     lazy_init_style_registry();
-    get_style_registry().read().unwrap().get_float("font_selector_corner_radius").unwrap_or(4.0)
+    get_style_registry().read().unwrap().get_float("font_selector_corner_radius").unwrap_or_else(control_corner_radius)
 }
 
 pub fn set_font_selector_corner_radius(radius: f32) {
@@ -3172,7 +3194,7 @@ pub fn set_font_selector_corner_radius(radius: f32) {
 
 pub fn dropdown_corner_radius() -> f32 {
     lazy_init_style_registry();
-    get_style_registry().read().unwrap().get_float("dropdown_corner_radius").unwrap_or(4.0)
+    get_style_registry().read().unwrap().get_float("dropdown_corner_radius").unwrap_or_else(control_corner_radius)
 }
 
 pub fn set_dropdown_corner_radius(radius: f32) {
@@ -6111,6 +6133,19 @@ mod tests {
         assert!(padding >= 0.0);
     }
 
+    /// The control rung's key flattens to `control_corner_radius`, beside a
+    /// widget's own override — the config shape `control corner_radius=8 { button corner_radius=10 }`.
+    #[test]
+    fn control_rung_radius_flattens_beside_widget_overrides() {
+        let val: serde_json::Value = serde_json::json!({
+            "style": { "control": { "corner_radius": 8, "button": { "corner_radius": 10 } } }
+        });
+        let mut flat = String::new();
+        flatten_json_to_flat_props(&val, "", &mut flat);
+        assert!(flat.lines().any(|l| l.starts_with("control_corner_radius")), "{flat}");
+        assert!(flat.lines().any(|l| l.starts_with("button_corner_radius")), "{flat}");
+    }
+
     #[test]
     fn test_graph_style_configuration() {
         // Trigger load_colors_once first so it doesn't overwrite values later