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

commitde33ff0adddef2b86bd5649c4bdd6af1de1f420d
parent0a7d9be49a
authorLucas Galante <[email protected]>
date2026-09-08 19:46
feat(widgets): the square-cornered leaves round like the rest of the toolkit

Rounded corners are the DE's standard look, and a handful of widgets still
drew plain quads. StatusDot is a disc (a rounded rect at half-side radius, so
it survives every legacy bridge); InfoBox is a rounded card in a hairline
frame at the plate radius; InteractiveListItem's state wash rounds like a
list-row Button's; Splitter's bar is a capsule. The flat styles follow the
relief ones: UsageBar draws the ProgressBar's rounded track and fill,
Trackpad, KeybindRecorder and FontSelector frame their dark field with a
rounded Border at their control's radius, and ColorSelector's text frame is a
rounded Border instead of four hairline strips.

ButtonStrip's segment fills are rounded rects on the segment's footprint (the
selected plateau's inset rect in the well, the item rect flat), so the state
fill no longer pokes square corners out from under the rounded plateau. They
now reach legacy hosts through all_rounded_quads — the Paginator aggregates
them, and cce-layout-interface reads that getter alongside extra_quads.

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

 src/widget/container/paginator.rs    | 14 ++++++++------
 src/widget/display/info_box.rs       | 12 +++++-------
 src/widget/display/list_item.rs      |  4 +++-
 src/widget/display/splitter.rs       | 10 ++++++++++
 src/widget/display/status_dot.rs     | 22 ++++++++++++++++------
 src/widget/display/usage_bar.rs      | 33 ++++++++++++++++++++-------------
 src/widget/input/button_strip.rs     | 20 +++++++++++---------
 src/widget/input/color_selector.rs   | 10 ++++------
 src/widget/input/font_selector.rs    | 13 ++-----------
 src/widget/input/keybind_recorder.rs |  8 +++-----
 src/widget/input/trackpad.rs         | 11 +++--------
 11 files changed, 85 insertions(+), 72 deletions(-)

diff --git a/src/widget/container/paginator.rs b/src/widget/container/paginator.rs
index 455e8d0..e7f5002 100644
--- a/src/widget/container/paginator.rs
+++ b/src/widget/container/paginator.rs
@@ -9,10 +9,11 @@
 //!   the ctx registry every frame — load-bearing for the spatial grid (the registered strip is
 //!   what makes the sidebar block root plate drags).
 //! - [`Paint::aggregates_child_extra_quads`] + [`Paint::forwarded_highlight`]: legacy
-//!   `extra_quads` served the strip's chrome only (cce-mail and cce-layout-interface render
-//!   the tab column through that getter — the paginator's own background quads live in
-//!   `all_quads` alone), and legacy `highlight_quad` forwarded to the strip's (the hovered-tab
-//!   tint cce-layout-interface draws directly).
+//!   `extra_quads` served the strip's chrome only (cce-layout-interface renders the tab
+//!   column through that getter plus `all_rounded_quads`, which carries the strip's rounded
+//!   state fills — the paginator's own background quads live in `all_quads` alone), and
+//!   legacy `highlight_quad` forwarded to the strip's (the hovered-tab tint
+//!   cce-layout-interface draws directly).
 
 use crate::colors;
 use crate::scene::layout::Rect;
@@ -197,8 +198,9 @@ impl Paint for Paginator {
         }
     }
 
-    /// Legacy `extra_quads` served the strip's + selected page's chrome only — cce-mail and
-    /// cce-layout-interface draw the tab column through this getter, over their own backgrounds.
+    /// Legacy `extra_quads` served the strip's + selected page's chrome only —
+    /// cce-layout-interface draws the tab column through this getter (and the strip's rounded
+    /// state fills through `all_rounded_quads`), over its own backgrounds.
     fn aggregates_child_extra_quads(&self) -> bool {
         true
     }
diff --git a/src/widget/display/info_box.rs b/src/widget/display/info_box.rs
index 7ae57a7..91c495f 100644
--- a/src/widget/display/info_box.rs
+++ b/src/widget/display/info_box.rs
@@ -29,14 +29,12 @@ impl Paint for InfoBox {
     }
 
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
-        let (x, y, w, h) = (rect.x, rect.y, rect.width, rect.height);
+        let (x, y) = (rect.x, rect.y);
         let theme = colors::active_theme();
-        let border_t = 1.0;
-        ctx.quad(rect, theme.surface_bg);
-        ctx.quad(Rect { x, y, width: w, height: border_t }, theme.surface_border);
-        ctx.quad(Rect { x, y: y + h - border_t, width: w, height: border_t }, theme.surface_border);
-        ctx.quad(Rect { x, y, width: border_t, height: h }, theme.surface_border);
-        ctx.quad(Rect { x: x + w - border_t, y, width: border_t, height: h }, theme.surface_border);
+        // A card: the themed surface in a hairline frame, rounded at the plate
+        // radius like every other surface set on the plate.
+        let r = crate::layout::plate_corner_radius();
+        ctx.border(rect, (r, r, r, r), theme.surface_bg, theme.surface_border, 1.0);
 
         ctx.text(self.title.clone(), x + 16.0, y + 12.0, 12.0, [89, 165, 229]);
         let mut current_y = y + 32.0;
diff --git a/src/widget/display/list_item.rs b/src/widget/display/list_item.rs
index 8ae0ba6..085c054 100644
--- a/src/widget/display/list_item.rs
+++ b/src/widget/display/list_item.rs
@@ -105,7 +105,9 @@ impl Paint for InteractiveListItem {
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
         let col = self.color();
         if col[3] > 0.0 {
-            ctx.quad(rect, col);
+            // The state wash, rounded like a list-row Button's.
+            let r = crate::layout::button_corner_radius();
+            ctx.rounded_rect(rect, r, (true, true, true, true), col);
         }
 
         let (x, y, h) = (rect.x, rect.y, rect.height);
diff --git a/src/widget/display/splitter.rs b/src/widget/display/splitter.rs
index 0bb57fd..4ab89e7 100644
--- a/src/widget/display/splitter.rs
+++ b/src/widget/display/splitter.rs
@@ -4,6 +4,7 @@
 
 use crate::colors;
 use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
 use crate::widget::{Adapted, WidgetHost, ElementState, Event, EventCtx, Input, Layout, MouseButton, Paint};
 
 pub struct Splitter {
@@ -32,6 +33,15 @@ impl Paint for Splitter {
             colors::SPLITTER_IDLE
         }
     }
+
+    /// The bar as a capsule: fully rounded ends on its short side.
+    fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+        let color = self.color();
+        if color[3].abs() > 0.001 {
+            let r = rect.width.min(rect.height) * 0.5;
+            ctx.rounded_rect(rect, r, (true, true, true, true), color);
+        }
+    }
 }
 
 impl Input for Splitter {
diff --git a/src/widget/display/status_dot.rs b/src/widget/display/status_dot.rs
index 7072ee4..3e9d221 100644
--- a/src/widget/display/status_dot.rs
+++ b/src/widget/display/status_dot.rs
@@ -7,6 +7,8 @@
 //! [`Paint`] default emits the color quad, so the dot now actually shows. The probe test at the
 //! bottom documents the fix.
 
+use crate::scene::layout::Rect;
+use crate::scene::paint::PaintCtx;
 use crate::widget::{Adapted, Input, Layout, Paint};
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -36,7 +38,7 @@ impl StatusDot {
 }
 
 impl Layout for StatusDot {
-    /// A dot: a fixed small square unless the host sizes it.
+    /// A dot: a fixed small disc unless the host sizes it.
     fn intrinsic_size(&self) -> Option<crate::scene::layout::Size> {
         Some(crate::scene::layout::Size::new(StatusDot::SIZE, StatusDot::SIZE))
     }
@@ -44,8 +46,6 @@ impl Layout for StatusDot {
 
 impl Paint for StatusDot {
     fn color(&self) -> [f32; 4] {
-        // `Paint::paint`'s default emits this as a plain quad — which is the fix: the legacy
-        // impl never got its color onto any render path.
         match self.status {
             DotStatus::Active => [0.20, 0.70, 0.35, 1.0],
             DotStatus::Inactive => [0.50, 0.50, 0.55, 1.0],
@@ -53,6 +53,14 @@ impl Paint for StatusDot {
             DotStatus::Error => [0.85, 0.25, 0.25, 1.0],
         }
     }
+
+    /// A disc: the colour as a rounded rect whose radius is half the short side. A rounded
+    /// rect rather than a circle prim so it survives every legacy bridge (`all_rounded_quads`,
+    /// `render_widget`), which carry rounded rects but not circles.
+    fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
+        let r = rect.width.min(rect.height) * 0.5;
+        ctx.rounded_rect(rect, r, (true, true, true, true), self.color());
+    }
 }
 
 impl Input for StatusDot {
@@ -67,13 +75,15 @@ mod tests {
     use crate::widget::{WidgetHost, UiContext};
 
     #[test]
-    fn emits_its_color_quad_through_the_bridge() {
+    fn emits_its_disc_through_the_rounded_bridge() {
         let mut dot = StatusDot::new(DotStatus::Warning);
         WidgetHost::set_rect(&mut dot, 5.0, 6.0, 10.0, 10.0);
         assert_eq!(
-            WidgetHost::extra_quads(&dot),
-            vec![(5.0, 6.0, 10.0, 10.0, [0.90, 0.60, 0.10, 1.0])],
+            WidgetHost::all_rounded_quads(&dot, &UiContext::new()),
+            vec![(5.0, 6.0, 10.0, 10.0, 5.0, [0.90, 0.60, 0.10, 1.0], (true, true, true, true))],
+            "a disc: the rect at half-side radius",
         );
+        assert!(WidgetHost::extra_quads(&dot).is_empty(), "nothing on the plain path (apps read both)");
         // Drags pass through, as legacy declared.
         assert!(!WidgetHost::blocks_root_plate_drag(&dot));
         // State mutation through Deref, as call sites write it.
diff --git a/src/widget/display/usage_bar.rs b/src/widget/display/usage_bar.rs
index 03d0019..c554c9d 100644
--- a/src/widget/display/usage_bar.rs
+++ b/src/widget/display/usage_bar.rs
@@ -1,6 +1,6 @@
-//! Narrow-trait usage bar (Phase 5c leaf sweep). Legacy geometry lived in `extra_quads` (plain
-//! bg + fill quads); the narrow [`Paint::paint`] emits the same two quads, which reach legacy
-//! render loops byte-identically through the adapter's `extra_quads` reverse bridge.
+//! Narrow-trait usage bar (Phase 5c leaf sweep). The narrow [`Paint::paint`] emits a rounded
+//! track and fill (the ProgressBar's composition, in both styles), which reach legacy render
+//! loops through the adapter's `all_rounded_quads` reverse bridge.
 
 use crate::scene::layout::Rect;
 use crate::scene::paint::PaintCtx;
@@ -76,8 +76,13 @@ impl Paint for UsageBar {
             ctx.recess(well, radii, depth);
             return;
         }
-        ctx.quad(rect, self.bg_color);
-        ctx.quad(Rect { width: rect.width * self.value, ..rect }, self.fill_color);
+        // The flat style: the ProgressBar's rounded track and fill.
+        let radius = crate::layout::slider_corner_radius();
+        ctx.rounded_rect(rect, radius, (true, true, true, true), self.bg_color);
+        let fill_w = rect.width * self.value;
+        if fill_w > 0.0 {
+            ctx.rounded_rect(Rect { width: fill_w, ..rect }, radius.min(rect.height / 2.0), (true, true, true, true), self.fill_color);
+        }
     }
 }
 
@@ -88,21 +93,23 @@ mod tests {
     use super::*;
     use crate::widget::WidgetHost;
 
-    /// Byte-identical to the legacy `extra_quads` override: full-width bg quad, then a fill quad
-    /// scaled by the clamped value.
+    /// The flat style is the ProgressBar's: a full-width rounded track, then a fill scaled by
+    /// the clamped value with its radius clamped to half the height — on the rounded getter,
+    /// nothing on the plain one (apps read both).
     #[test]
-    fn bridge_matches_legacy_extra_quads() {
+    fn flat_style_is_rounded_track_then_fill() {
         let mut bar = UsageBar::new(0.5).with_recessed(false).with_colors([0.1, 0.2, 0.3, 1.0], [0.4, 0.5, 0.6, 1.0]);
         WidgetHost::set_rect(&mut bar, 12.0, 30.0, 200.0, 8.0);
+        let radius = crate::layout::slider_corner_radius();
+        let all = (true, true, true, true);
         assert_eq!(
-            WidgetHost::extra_quads(&bar),
+            WidgetHost::all_rounded_quads(&bar, &crate::widget::UiContext::new()),
             vec![
-                (12.0, 30.0, 200.0, 8.0, [0.4, 0.5, 0.6, 1.0]),
-                (12.0, 30.0, 100.0, 8.0, [0.1, 0.2, 0.3, 1.0]),
+                (12.0, 30.0, 200.0, 8.0, radius, [0.4, 0.5, 0.6, 1.0], all),
+                (12.0, 30.0, 100.0, 8.0, radius.min(4.0), [0.1, 0.2, 0.3, 1.0], all),
             ],
         );
-        // Nothing leaks onto the rounded path (apps read both getters).
-        assert!(WidgetHost::all_rounded_quads(&bar, &crate::widget::UiContext::new()).is_empty());
+        assert!(WidgetHost::extra_quads(&bar).is_empty(), "no plain quad leaks to flat hosts");
     }
 
     /// Recessed: no bg quad at all — the fill on the floor, then the carve.
diff --git a/src/widget/input/button_strip.rs b/src/widget/input/button_strip.rs
index 81abe3d..3c666af 100644
--- a/src/widget/input/button_strip.rs
+++ b/src/widget/input/button_strip.rs
@@ -344,8 +344,8 @@ impl crate::widget::Paint for ButtonStrip {
         use crate::scene::layout::Rect;
         // The strip's well: one recess around the whole run, rounded like the
         // buttons, before the segments so their fills sit on its floor. The
-        // segment fills stay plain quads either way — they are what reaches the
-        // flat hosts that read this widget through `extra_quads`.
+        // segment fills are rounded at the same radius (they reach legacy hosts
+        // through `all_rounded_quads`, which the Paginator aggregates).
         let (sx, sy, sw, sh) = self.rect();
         let radius = crate::layout::button_corner_radius();
         let depth = if self.recessed {
@@ -369,16 +369,18 @@ impl crate::widget::Paint for ButtonStrip {
             } else if Some(i) == self.hovered_idx {
                 bg_color = colors::PANEL_MENU_HOVER;
             }
+            // The segment's footprint: in the well, inset by the wall's inner
+            // half-span so it stands on the floor (the selected plateau's rect);
+            // flat, the item rect itself. The state fill and the plateau share it.
+            let inset = if self.recessed { depth * 0.5 } else { 0.0 };
+            let seg = Rect { x: r.0 + inset, y: r.1 + inset, width: (r.2 - 2.0 * inset).max(0.0), height: (r.3 - 2.0 * inset).max(0.0) };
+            let seg_r = (radius - inset).max(0.0);
             if bg_color != [0.0, 0.0, 0.0, 0.0] {
-                pc.quad(Rect { x: r.0, y: r.1, width: r.2, height: r.3 }, bg_color);
+                pc.rounded_rect(seg, seg_r, (true, true, true, true), bg_color);
             }
             if self.recessed && Some(i) == self.selected {
-                // The selected segment: a plateau raised back out of the well,
-                // inset by the wall's inner half-span so it stands on the floor.
-                let inset = depth * 0.5;
-                let plateau = Rect { x: r.0 + inset, y: r.1 + inset, width: (r.2 - 2.0 * inset).max(0.0), height: (r.3 - 2.0 * inset).max(0.0) };
-                let pr = (radius - inset).max(0.0);
-                let (plateau, radii) = crate::layout::carve_inside(plateau, (pr, pr, pr, pr), depth);
+                // The selected segment: a plateau raised back out of the well.
+                let (plateau, radii) = crate::layout::carve_inside(seg, (seg_r, seg_r, seg_r, seg_r), depth);
                 pc.boss(plateau, radii, depth);
             }
 
diff --git a/src/widget/input/color_selector.rs b/src/widget/input/color_selector.rs
index 8311104..fa1a6f8 100644
--- a/src/widget/input/color_selector.rs
+++ b/src/widget/input/color_selector.rs
@@ -254,13 +254,11 @@ impl Paint for ColorSelector {
         };
 
         // A real frame, not a border-quad-under-fill-quad: with no fill, the
-        // old full-rect border quad would read as a solid slab.
+        // old full-rect border quad would read as a solid slab. Rounded at the
+        // selector's radius like the well it stands in for.
         if !self.recessed {
-            let bw = 1.0;
-            quads.push((rect.x, rect.y, rect.width, bw, border_color));
-            quads.push((rect.x, rect.y + visual_h - bw, rect.width, bw, border_color));
-            quads.push((rect.x, rect.y, bw, visual_h, border_color));
-            quads.push((rect.x + rect.width - bw, rect.y, bw, visual_h, border_color));
+            let fr = crate::layout::color_selector_corner_radius();
+            ctx.border(rect, (fr, fr, fr, fr), [0.0; 4], border_color, 1.0);
         }
 
         if self.editing {
diff --git a/src/widget/input/font_selector.rs b/src/widget/input/font_selector.rs
index 30a7a78..89ae1ba 100644
--- a/src/widget/input/font_selector.rs
+++ b/src/widget/input/font_selector.rs
@@ -173,12 +173,7 @@ impl Paint for FontSelector {
             self.paint_labels(rect, ctx);
             return;
         }
-        // Rounded base plate (the legacy leaf default from color + corner style)
-        if r > 0.0 {
-            ctx.rounded_rect(rect, r, (true, true, true, true), self.color());
-        }
-
-        // Border + inner background (the legacy extra_quads pair)
+        // The flat style: the framed dark field, rounded at the selector's radius.
         let bg_color = [0.08, 0.08, 0.12, 1.0];
         let border_color = if self.pressed {
             [0.30, 0.50, 0.32, 1.0]
@@ -187,11 +182,7 @@ impl Paint for FontSelector {
         } else {
             [0.18, 0.18, 0.24, 1.0]
         };
-        ctx.quad(rect, border_color);
-        ctx.quad(
-            Rect { x: rect.x + 1.0, y: rect.y + 1.0, width: rect.width - 2.0, height: rect.height - 2.0 },
-            bg_color,
-        );
+        ctx.border(rect, (r, r, r, r), bg_color, border_color, 1.0);
 
         self.paint_labels(rect, ctx);
     }
diff --git a/src/widget/input/keybind_recorder.rs b/src/widget/input/keybind_recorder.rs
index c8e3b4f..622cf30 100644
--- a/src/widget/input/keybind_recorder.rs
+++ b/src/widget/input/keybind_recorder.rs
@@ -88,11 +88,9 @@ impl Paint for KeybindRecorder {
         } else {
             [0.18, 0.18, 0.24, 1.0]
         };
-        ctx.quad(rect, border_color);
-        ctx.quad(
-            Rect { x: rect.x + 1.0, y: rect.y + 1.0, width: rect.width - 2.0, height: rect.height - 2.0 },
-            [0.08, 0.08, 0.12, 1.0],
-        );
+        // The framed dark field, rounded like the text wells.
+        let radius = crate::layout::textbox_corner_radius();
+        ctx.border(rect, (radius, radius, radius, radius), [0.08, 0.08, 0.12, 1.0], border_color, 1.0);
 
         self.paint_text(rect, ctx);
     }
diff --git a/src/widget/input/trackpad.rs b/src/widget/input/trackpad.rs
index 4a64f38..1d740bc 100644
--- a/src/widget/input/trackpad.rs
+++ b/src/widget/input/trackpad.rs
@@ -96,15 +96,10 @@ impl Paint for Trackpad {
             let (well, radii) = crate::layout::carve_inside(area, (radius, radius, radius, radius), depth);
             ctx.recess(well, radii, depth);
         } else {
-            // 1. Background
-            ctx.quad(area, [0.11, 0.11, 0.16, 0.85]);
-
-            // 2. Borders
+            // 1+2. The framed dark pane, rounded like the text wells.
+            let radius = crate::layout::textbox_corner_radius();
             let border_color = [0.28, 0.28, 0.38, 1.0];
-            ctx.quad(Rect { x, y, width: w, height: 1.0 }, border_color);
-            ctx.quad(Rect { x, y: y + visual_h - 1.0, width: w, height: 1.0 }, border_color);
-            ctx.quad(Rect { x, y, width: 1.0, height: visual_h }, border_color);
-            ctx.quad(Rect { x: x + w - 1.0, y, width: 1.0, height: visual_h }, border_color);
+            ctx.border(area, (radius, radius, radius, radius), [0.11, 0.11, 0.16, 0.85], border_color, 1.0);
         }
 
         // 3. Fingers