GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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)]