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

commitd8e496a051d4032829bfc86afb9861645e98eea3
parentbb014e298d
authorLucas Galante <[email protected]>
date2026-09-08 20:19
feat(paint): ControlPlate — one spec for every control face

The control rung of the plate ladder gets its type, beside the root/pane
PlateSpec of RFC 7b: a ControlPlate is a footprint, a per-corner silhouette,
a PlateStance (raised or flush), a face and a depth, painted by
PaintCtx::control_plate — the ONE place a control face's relief is composed.
Raised with a face is a bevel on the footprint; raised faceless carves inside
and raises a boss; flush carves inside and lays an inset plate. The
transparent-face rule the Dropdown and Breadcrumb each spelled out lives on
ControlPlate::face_from_fill.

Button, Dropdown (the trigger, with its frame-adjusted corners), FontSelector,
Breadcrumb (both stances, the raised frost kept) and the ButtonStrip's
selected plateau draw through it. Button::inset_face stays as the legacy
tuple view of Button::plate for cce-system-interface's flat bridge.

Verified prim-identical: a dump of every face in every stance before and
after the migration (288 prims across nine configurations) is byte-equal,
and a shadow pixel comparison of the gallery differs only outside the window.

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

 CLAUDE.md                          | 17 ++++---
 src/scene/paint.rs                 | 94 ++++++++++++++++++++++++++++++++++++++
 src/widget/container/breadcrumb.rs | 28 ++++--------
 src/widget/input/button.rs         | 28 ++++++------
 src/widget/input/button_strip.rs   | 10 ++--
 src/widget/input/dropdown.rs       | 31 +++++--------
 src/widget/input/font_selector.rs  |  8 ++--
 src/widget/mod.rs                  |  1 +
 8 files changed, 151 insertions(+), 66 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 5c9a146..3177fe4 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -138,13 +138,16 @@ What this buys, and where the code is heading:
 - **Navigation is stated in plate terms.** Focus moves between plates, a press
   acts on a plate, a well opens for typing. Hit testing and focus rings are the
   plate's silhouette.
-- **One plate spec, not five copies.** Button, Dropdown, FontSelector,
-  Breadcrumb and ButtonStrip each re-derive the same carve-inside, radius,
-  depth and transparent-face rules today (the Breadcrumb comments that it
-  mirrors the Dropdown's face logic exactly). The intended direction is a
-  shared plate spec (stance, radius, depth, face) and one paint entry point
-  those controls draw through — migrated behaviour-preserving and verified
-  pixel-identical in a shadow session.
+- **One plate spec per rung, not five copies.** The root and pane rungs are
+  `scene::paint::PlateSpec` (RFC 7b, painted by `PaintCtx::plate`). The
+  control rung is `scene::paint::ControlPlate` (re-exported from `widget`):
+  footprint, per-corner silhouette, `PlateStance` (raised or flush), face and
+  depth, painted by `PaintCtx::control_plate` — the ONE place a control face's
+  relief is composed (raised with a face = bevel; raised faceless = carve
+  inside + boss; flush = carve inside + inset plate). Button, Dropdown,
+  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.
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 94ab36b..b408a48 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -137,6 +137,78 @@ impl PlateSpec {
 /// faces and another for the edges drawn over them (cce-files' `rects` vs
 /// `reliefs`).
 ///
+/// How a control plate sits on the surface beneath it — see "Plates, wells
+/// and seams" in `CLAUDE.md`.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum PlateStance {
+    /// Floats above the surface: a [`Prim::Bevel`] when it has a face of its
+    /// own, an edges-only [`Prim::Boss`] carved inside its footprint when the
+    /// face is transparent (the surface below shows through as the face).
+    Raised,
+    /// Level with the surface inside a groove ring: a [`Prim::Trough`] carved
+    /// inside its footprint, with the face as a flat fill when it has one
+    /// ([`PaintCtx::inset_plate`]).
+    Flush,
+}
+
+/// A control plate: the thing you press, at the control rung of the plate
+/// ladder. One description for every control face — Button, Dropdown,
+/// FontSelector, Breadcrumb, a ButtonStrip's selected plateau — so their
+/// carve-inside, radius, depth and transparent-face rules cannot drift.
+/// Painted by [`PaintCtx::control_plate`]. The root and pane rungs of the
+/// ladder are [`PlateSpec`]; this is the same idea one rung down.
+///
+/// `rect` is the plate's footprint, the OUTER edge of its silhouette; the
+/// carve is taken inside it ([`crate::layout::carve_inside`]), so the gap
+/// beside the plate is the gap. `radii` is the silhouette, per corner (a
+/// Dropdown nested concentrically in a frame corner adjusts each). `face`
+/// is the plate's own fill; transparent means the surface below IS the face
+/// (a negative alpha is the blur-behind frost, a real face). `depth` is the
+/// relief's wall width — [`ControlPlate::control`] takes the DE relief width
+/// capped at a fifth of the height.
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct ControlPlate {
+    pub rect: Rect,
+    pub radii: Radii,
+    pub stance: PlateStance,
+    pub face: [f32; 4],
+    pub depth: f32,
+}
+
+impl ControlPlate {
+    /// A control plate at `rect` with a uniform corner `radius`: depth from
+    /// the DE relief width, capped at a fifth of the plate's height.
+    pub fn control(rect: Rect, radius: f32, stance: PlateStance, face: [f32; 4]) -> Self {
+        let depth = crate::layout::bevel_width().min(rect.height * 0.2);
+        Self { rect, radii: (radius, radius, radius, radius), stance, face, depth }
+    }
+
+    /// Per-corner silhouette (a concentric corner-frame adjustment).
+    pub fn with_radii(mut self, radii: Radii) -> Self {
+        self.radii = radii;
+        self
+    }
+
+    /// An explicit wall width — a plate that shares its depth with the well
+    /// it stands in, or one capped by its short side rather than its height.
+    pub fn with_depth(mut self, depth: f32) -> Self {
+        self.depth = depth;
+        self
+    }
+
+    /// A control plate's face from a configured fill: an opaque one is the
+    /// face (alpha forced to 1 — a translucent face would blend into the
+    /// relief's shading and read as a second material); a transparent one
+    /// leaves the surface below as the face (edges only).
+    pub fn face_from_fill(raw: [f32; 4]) -> [f32; 4] {
+        if raw[3] > 0.001 {
+            [raw[0], raw[1], raw[2], 1.0]
+        } else {
+            [0.0; 4]
+        }
+    }
+}
+
 /// Call the family **relief primitives**, not "bevel primitives": `Bevel` is one
 /// specific member — a filled rounded rect plus a lit roll on its lip — and a
 /// groove, a fillet or a sphere is not a bevel in any sense. "Relief" is also
@@ -947,6 +1019,28 @@ impl PaintCtx {
         self.push(Prim::Boss { rect, radii, depth, edges, tint: Some(tint) });
     }
 
+    /// Paint a control plate — see [`ControlPlate`]. The ONE place a control face's
+    /// relief is composed: raised with a face is a `bevel` on the footprint;
+    /// raised without one carves inside and raises a `boss`; flush carves
+    /// inside and lays an `inset_plate` (trough plus face).
+    pub fn control_plate(&mut self, plate: &ControlPlate) {
+        match plate.stance {
+            PlateStance::Raised => {
+                // abs(): a negative alpha is the frost sentinel, a real face.
+                if plate.face[3].abs() > 0.001 {
+                    self.bevel(plate.rect, plate.radii, plate.face, plate.depth);
+                } else {
+                    let (plateau, radii) = crate::layout::carve_inside(plate.rect, plate.radii, plate.depth);
+                    self.boss(plateau, radii, plate.depth);
+                }
+            }
+            PlateStance::Flush => {
+                let (trough, radii) = crate::layout::carve_inside(plate.rect, plate.radii, plate.depth);
+                self.inset_plate(trough, radii, plate.face, plate.depth);
+            }
+        }
+    }
+
     /// A flush inset control: `rect`'s plate sits SUNKEN into the surface with
     /// its face level with it — a valley seam runs the boundary, the surface
     /// falling into it on the way out and the control's own face rising back
diff --git a/src/widget/container/breadcrumb.rs b/src/widget/container/breadcrumb.rs
index f6018f0..60a4cbf 100644
--- a/src/widget/container/breadcrumb.rs
+++ b/src/widget/container/breadcrumb.rs
@@ -329,15 +329,8 @@ impl Paint for Breadcrumb {
                 // face, which is what the boss run always did here; an opaque
                 // one makes both controls that color. Mirrors
                 // `Dropdown::paint_background`'s `face` exactly.
-                let raw_bg = crate::color::dropdown_background_color();
-                let face = if raw_bg[3] > 0.001 {
-                    let mut c = raw_bg;
-                    c[3] = 1.0;
-                    c
-                } else {
-                    [0.0; 4]
-                };
-                if self.raised {
+                let face = crate::widget::ControlPlate::face_from_fill(crate::color::dropdown_background_color());
+                let (stance, face) = if self.raised {
                     // The floating stance: the run rises out of the surface as
                     // ONE beveled plate — fill and raised roll in a single
                     // lighting pass. The face is deliberately translucent
@@ -348,18 +341,17 @@ impl Paint for Breadcrumb {
                     // legible over live content beneath. A transparent
                     // configured fill keeps the boss degradation: edges only,
                     // the surface as the face.
-                    if face[3] > 0.001 {
-                        let mut c = face;
+                    let mut c = face;
+                    if c[3] > 0.001 {
                         c[3] = -(c[3] * Self::RAISED_FACE_OPACITY);
-                        ctx.bevel(run_rect, (r, r, r, r), c, depth);
-                    } else {
-                        let (plateau, radii) = crate::layout::carve_inside(run_rect, (r, r, r, r), depth);
-                        ctx.boss(plateau, radii, depth);
                     }
+                    (crate::widget::PlateStance::Raised, c)
                 } else {
-                    let (trough, radii) = crate::layout::carve_inside(run_rect, (r, r, r, r), depth);
-                    ctx.inset_plate(trough, radii, face, depth);
-                }
+                    (crate::widget::PlateStance::Flush, face)
+                };
+                ctx.control_plate(
+                    &crate::widget::ControlPlate::control(run_rect, r, stance, face).with_depth(depth),
+                );
                 for (a, b) in self.seams(rect) {
                     ctx.groove(a, b, Self::SEAM_WIDTH, depth, run_rect);
                 }
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index 21d06d0..480123b 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -185,15 +185,11 @@ impl Button {
         }
     }
 
-    /// The flush inset plate this Button's `paint` draws, as `(rect, corner
-    /// radius, depth, face colour)` — `None` when it draws none (flat styling,
-    /// or a ListRow, which is a transparent-until-hover surface and would wear
-    /// a permanent carved ring on every idle row).
-    ///
-    /// The single source `paint` and the flat-path bridge in
-    /// `layout::render_widget` both read, so a flat host's groove can't drift
-    /// from the drawn one.
-    pub fn inset_face(&self, rect: Rect) -> Option<(Rect, f32, f32, [f32; 4])> {
+    /// The control plate this Button's `paint` draws — flush, at the button
+    /// radius, its state colour as the face — or `None` when it draws none
+    /// (flat styling, or a ListRow / MenuItem, transparent-until-hover
+    /// surfaces that would wear a permanent carved ring on every idle row).
+    pub fn plate(&self, rect: Rect) -> Option<crate::widget::ControlPlate> {
         if !self.raised
             || self.kind == ButtonKind::ListRow
             || self.kind == ButtonKind::MenuItem
@@ -201,8 +197,13 @@ impl Button {
             return None;
         }
         let radius = crate::layout::button_corner_radius();
-        let depth = crate::layout::bevel_width().min(rect.height * 0.2);
-        Some((rect, radius, depth, self.color()))
+        Some(crate::widget::ControlPlate::control(rect, radius, crate::widget::PlateStance::Flush, self.color()))
+    }
+
+    /// [`Button::plate`] as the legacy `(rect, corner radius, depth, face
+    /// colour)` tuple — the flat-path bridge's view of the same plate.
+    pub fn inset_face(&self, rect: Rect) -> Option<(Rect, f32, f32, [f32; 4])> {
+        self.plate(rect).map(|p| (p.rect, p.radii.0, p.depth, p.face))
     }
 }
 
@@ -378,9 +379,8 @@ impl Paint for Button {
         // List rows are exempt: they are transparent-until-hover/selected
         // surfaces, and the edges-only groove would stack a permanent carved
         // ring on every idle row of a list.
-        if let Some((face, r, depth, c)) = self.inset_face(rect) {
-            let (trough, radii) = crate::layout::carve_inside(face, (r, r, r, r), depth);
-            ctx.inset_plate(trough, radii, c, depth);
+        if let Some(plate) = self.plate(rect) {
+            ctx.control_plate(&plate);
         } else {
             // ListRow also skips the border idiom below: it draws the border
             // color as a FULL rect with the fill inset over it, which only
diff --git a/src/widget/input/button_strip.rs b/src/widget/input/button_strip.rs
index 3c666af..46d66a6 100644
--- a/src/widget/input/button_strip.rs
+++ b/src/widget/input/button_strip.rs
@@ -379,9 +379,13 @@ impl crate::widget::Paint for ButtonStrip {
                 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.
-                let (plateau, radii) = crate::layout::carve_inside(seg, (seg_r, seg_r, seg_r, seg_r), depth);
-                pc.boss(plateau, radii, depth);
+                // The selected segment: a raised control plate standing on the
+                // well floor, faceless (the floor shows through), at the well's
+                // depth.
+                pc.control_plate(
+                    &crate::widget::ControlPlate::control(seg, seg_r, crate::widget::PlateStance::Raised, [0.0; 4])
+                        .with_depth(depth),
+                );
             }
 
             if self.vertical {
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index d22a935..875b08f 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -451,26 +451,19 @@ impl Dropdown {
                     r4[3] = (pr - g_left).max(0.0) * cf;
                 }
             }
-            // Flush inset plate: groove ring down, beveled lip back up, face
-            // level with the surface (transparent raw fill = edges only).
-            // Carved INSIDE the trigger's rect (`layout::carve_inside`): the
-            // groove's outer edge lands on the rect, so everything below —
-            // which outsets the ring by half the depth — starts from the rect
-            // inset by that much, radii reduced to keep the outer silhouette.
-            let (inner, r4t) = crate::layout::carve_inside(
+            // The trigger is a flush control plate (groove ring down, beveled
+            // lip back up, face level with the surface; a transparent raw
+            // fill = edges only), its footprint the trigger's rect and its
+            // silhouette the frame-adjusted radii.
+            let plate = crate::widget::ControlPlate::control(
                 Rect { x, y, width: w, height: visual_h },
-                (r4[0], r4[1], r4[2], r4[3]),
-                depth,
-            );
-            let (x, y, w, visual_h) = (inner.x, inner.y, inner.width, inner.height);
-            let r4 = [r4t.0, r4t.1, r4t.2, r4t.3];
-            let face = if raw_bg[3] > 0.001 { bg_color } else { [0.0; 4] };
-            ctx.inset_plate(
-                Rect { x, y, width: w, height: visual_h },
-                (r4[0], r4[1], r4[2], r4[3]),
-                face,
-                depth,
-            );
+                radius,
+                crate::widget::PlateStance::Flush,
+                crate::widget::ControlPlate::face_from_fill(raw_bg),
+            )
+            .with_radii((r4[0], r4[1], r4[2], r4[3]))
+            .with_depth(depth);
+            ctx.control_plate(&plate);
             return;
         }
         if radius <= 0.0 {
diff --git a/src/widget/input/font_selector.rs b/src/widget/input/font_selector.rs
index 89ae1ba..3b229a9 100644
--- a/src/widget/input/font_selector.rs
+++ b/src/widget/input/font_selector.rs
@@ -155,11 +155,9 @@ impl Paint for FontSelector {
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
         let r = crate::layout::font_selector_corner_radius();
         if self.raised {
-            // The closed-dropdown chrome: a flush inset trough with a transparent
-            // face, the state fill rounded to sit inside it.
-            let depth = crate::layout::bevel_width().min(rect.height * 0.2);
-            let (trough, radii) = crate::layout::carve_inside(rect, (r, r, r, r), depth);
-            ctx.inset_plate(trough, radii, [0.0; 4], depth);
+            // The closed-dropdown chrome: a flush control plate with a
+            // transparent face, the state fill rounded to sit inside it.
+            ctx.control_plate(&crate::widget::ControlPlate::control(rect, r, crate::widget::PlateStance::Flush, [0.0; 4]));
             let wash = if self.pressed {
                 Some(colors::button_press_color())
             } else if self.hovered {
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 74011ca..ad38bbd 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -139,6 +139,7 @@ pub struct LayoutTree {
     pub children: HashMap<WidgetId, Vec<WidgetId>>,
 }
 
+pub use crate::scene::paint::{ControlPlate, PlateStance};
 pub use crate::context::UiContext;
 
 #[derive(Debug, Clone, PartialEq)]