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

commitcc36c1f310d9edf43e6d5f413aed0e89b5f4132e
parent2e1abb3d23
authorLucas Galante <[email protected]>
date2026-09-20 14:29
ControlPlate has a face of Material, or none (RFC material, step 2c)

ControlPlate.face: Option<Material> — None is the surface below showing
through (edges only), Some a face, frosted where the stance can carry
it. control_plate composes from faced() (a visible tint) the way it did
from |alpha| > 0.001; face_fill() is the encoded colour the flat-path
bridges (Button::inset_face, cce-system-interface's ControlCarve) still
consume. face_from_fill is gone: the opaque-or-none rule is
Material::control_face, and Material::face is the bridge for a face
slot holding an encoded colour (transparent = None).

inset_plate / inset_plate_tinted take Option<&Material>. The Breadcrumb
says what its raised face is — the control face at RAISED_FACE_OPACITY,
frosted — instead of negating an alpha; Dropdown::with_face takes a
Material.

Prim-identical against the pre-2a golden: identical, 148518 lines.

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

 src/bin/cce-relief.rs              |  2 +-
 src/scene/material.rs              | 13 +++++++-
 src/scene/paint.rs                 | 67 +++++++++++++++++++-------------------
 src/widget/container/breadcrumb.rs | 15 +++++----
 src/widget/container/menu.rs       |  2 +-
 src/widget/input/button.rs         |  4 +--
 src/widget/input/button_strip.rs   |  2 +-
 src/widget/input/checkbox.rs       |  2 +-
 src/widget/input/dropdown.rs       | 10 +++---
 src/widget/input/font_selector.rs  |  2 +-
 tests/plate_golden.rs              |  9 ++---
 11 files changed, 71 insertions(+), 57 deletions(-)

diff --git a/src/bin/cce-relief.rs b/src/bin/cce-relief.rs
index d224e1a..d50b049 100644
--- a/src/bin/cce-relief.rs
+++ b/src/bin/cce-relief.rs
@@ -882,7 +882,7 @@ fn draw_section(pc: &mut PaintCtx, rect: Rect, profile: &ProfileKnobs, shape: Sh
                     Shape::Boss => pc.boss(tall, sq, unit),
                     Shape::Ridge => pc.ridge(tall, sq, unit),
                     Shape::Trough => pc.trough(tall, sq, unit),
-                    Shape::InsetPlate => pc.inset_plate(tall, sq, [0.0; 4], unit),
+                    Shape::InsetPlate => pc.inset_plate(tall, sq, None, unit),
                     Shape::Groove => {
                         let cx = x0 + unit * (0.5 + GROOVE_FLOOR * 0.5);
                         pc.groove(
diff --git a/src/scene/material.rs b/src/scene/material.rs
index 8b493af..74aef9b 100644
--- a/src/scene/material.rs
+++ b/src/scene/material.rs
@@ -194,6 +194,14 @@ impl Material {
         if a < 0.0 { m.with_frost(Frost::from_style()) } else { m }
     }
 
+    /// The legacy bridge for a FACE slot: a transparent fill is no face at
+    /// all (`None` — the surface below shows through), anything else is
+    /// [`Material::from_fill`] of it. The `|alpha| > 0.001` test every face
+    /// slot applied, stated once.
+    pub fn face(encoded: [f32; 4]) -> Option<Self> {
+        (encoded[3].abs() > 0.001).then(|| Self::from_fill(encoded))
+    }
+
     /// This material as a plate in `role` carries it: a root plate's frost
     /// is the COMPOSITOR's, so under [`PlateRole::Root`] the client-side
     /// material is opaque — the prim a `PlateSpec` emits carries this, and
@@ -227,7 +235,7 @@ impl Material {
     }
 
     /// A control face from a configured fill, under the rule
-    /// `ControlPlate::face_from_fill` states: an opaque one is the face
+    /// the control rung states: 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 is `None`,
     /// the surface below showing as the face (edges only).
@@ -366,6 +374,9 @@ mod tests {
             }
         }
         assert_eq!(Material::from_fill([0.0, 0.0, 0.0, -0.5]).frost, Frost::from_style());
+        assert!(Material::face([0.3, 0.3, 0.3, 0.0]).is_none(), "transparent = no face");
+        assert!(Material::face([0.3, 0.3, 0.3, -0.5]).is_some_and(|m| m.frost.is_frosted()));
+        assert_eq!(Material::face([0.3, 0.3, 0.3, 0.7]).map(|m| m.tint), Some([0.3, 0.3, 0.3, 0.7]));
     }
 
     /// The control-face rule: opaque or nothing.
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index e6b988f..cc68ca6 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -185,8 +185,9 @@ pub enum PlateStance {
 /// 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
+/// is the plate's own material; `None` means the surface below IS the face
+/// (edges only), and a frosted material is a real face — the frost carried
+/// where the stance can (`Flat`; see [`PlateStance`]). `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)]
@@ -194,7 +195,7 @@ pub struct ControlPlate {
     pub rect: Rect,
     pub radii: Radii,
     pub stance: PlateStance,
-    pub face: [f32; 4],
+    pub face: Option<Material>,
     pub depth: f32,
     /// The rim lit in this colour: the keyboard-focus ring, drawn on the
     /// plate's own silhouette rather than as extra geometry. `None` unlit.
@@ -204,7 +205,7 @@ pub struct ControlPlate {
 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 {
+    pub fn control(rect: Rect, radius: f32, stance: PlateStance, face: Option<Material>) -> Self {
         let depth = crate::layout::bevel_width().min(rect.height * 0.2);
         Self { rect, radii: (radius, radius, radius, radius), stance, face, depth, tint: None }
     }
@@ -236,15 +237,18 @@ impl ControlPlate {
         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] {
-        // The rule lives on the material (`Material::control_face`); this is
-        // its `[f32; 4]` spelling until step 2 gives `ControlPlate` a
-        // `face: Option<Material>`.
-        Material::control_face(raw).map_or([0.0; 4], |m| m.tint)
+    /// The face a stance draws: `Some` only for a material with a visible
+    /// tint — a transparent one is the surface below showing through, the
+    /// same as `None`.
+    pub fn faced(&self) -> Option<&Material> {
+        self.face.as_ref().filter(|m| m.tint[3] > 0.001)
+    }
+
+    /// The face as the encoded fill the flat-path bridges consume
+    /// (`Button::inset_face`, cce-system-interface's `ControlCarve`):
+    /// transparent for no face, else the material's nested fill.
+    pub fn face_fill(&self) -> [f32; 4] {
+        self.face.map_or([0.0; 4], |m| m.fill(PlateRole::Nested))
     }
 }
 
@@ -1163,12 +1167,10 @@ impl PaintCtx {
     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 {
-                    let face = Material::from_fill(plate.face);
+                if let Some(face) = plate.faced() {
                     match plate.tint {
-                        Some(t) => self.bevel_tinted(plate.rect, plate.radii, &face, plate.depth, t),
-                        None => self.bevel(plate.rect, plate.radii, &face, plate.depth),
+                        Some(t) => self.bevel_tinted(plate.rect, plate.radii, face, plate.depth, t),
+                        None => self.bevel(plate.rect, plate.radii, face, plate.depth),
                     }
                 } else {
                     let (plateau, radii) = crate::layout::carve_inside(plate.rect, plate.radii, plate.depth);
@@ -1181,13 +1183,12 @@ impl PaintCtx {
             PlateStance::Flush => {
                 let (trough, radii) = crate::layout::carve_inside(plate.rect, plate.radii, plate.depth);
                 match plate.tint {
-                    Some(t) => self.inset_plate_tinted(trough, radii, plate.face, plate.depth, t),
-                    None => self.inset_plate(trough, radii, plate.face, plate.depth),
+                    Some(t) => self.inset_plate_tinted(trough, radii, plate.faced(), plate.depth, t),
+                    None => self.inset_plate(trough, radii, plate.faced(), plate.depth),
                 }
             }
             PlateStance::Flat => {
-                // abs(): a negative alpha is the frost sentinel, a real face.
-                if plate.face[3].abs() > 0.001 {
+                if let Some(face) = plate.faced() {
                     // A QUAD deliberately, not the `Border` the relief stances
                     // fill through: carrying the blur-behind sentinel is half
                     // the point of this stance, and only quads reach it.
@@ -1195,7 +1196,7 @@ impl PaintCtx {
                         plate.rect,
                         plate.radii.0,
                         (true, true, true, true),
-                        plate.face,
+                        face.fill(PlateRole::Nested),
                     );
                 }
                 if let Some(t) = plate.tint {
@@ -1300,10 +1301,10 @@ impl PaintCtx {
     ///
     /// An opaque `color` fills the face; transparent leaves the surface below
     /// showing through as the face.
-    pub fn inset_plate(&mut self, rect: Rect, radii: Radii, color: [f32; 4], depth: f32) {
-        // abs(): negative alpha is the blur-behind frost sentinel, a real
-        // face — only a genuinely transparent color skips the fill.
-        if color[3].abs() > 0.001 {
+    pub fn inset_plate(&mut self, rect: Rect, radii: Radii, face: Option<&Material>, depth: f32) {
+        // A transparent material is no face either — only a visible tint
+        // fills; a frosted one fills with the sentinel.
+        if let Some(face) = face.filter(|m| m.tint[3] > 0.001) {
             // Flat fill only — the relief is the trough's, so the face must not
             // carry a lip of its own (that lip WAS the second wall).
             //
@@ -1315,7 +1316,7 @@ impl PaintCtx {
             // those getters — a change to the legacy surface that has nothing to
             // do with the relief. Border also keeps all four radii, which
             // `Prim::RoundedRect`'s single radius cannot.
-            self.border(rect, radii, color, [0.0; 4], 0.0);
+            self.border(rect, radii, face.fill(PlateRole::Nested), [0.0; 4], 0.0);
         }
         self.trough(rect, radii, depth);
     }
@@ -1323,9 +1324,9 @@ impl PaintCtx {
     /// [`inset_plate`](Self::inset_plate) with the rim lit — the focused flush
     /// control plate's ring (`ControlPlate::with_tint`); the face fill as
     /// there, the trough tinted.
-    pub fn inset_plate_tinted(&mut self, rect: Rect, radii: Radii, color: [f32; 4], depth: f32, tint: [f32; 3]) {
-        if color[3].abs() > 0.001 {
-            self.border(rect, radii, color, [0.0; 4], 0.0);
+    pub fn inset_plate_tinted(&mut self, rect: Rect, radii: Radii, face: Option<&Material>, depth: f32, tint: [f32; 3]) {
+        if let Some(face) = face.filter(|m| m.tint[3] > 0.001) {
+            self.border(rect, radii, face.fill(PlateRole::Nested), [0.0; 4], 0.0);
         }
         self.trough_tinted(rect, radii, depth, tint);
     }
@@ -1679,10 +1680,10 @@ impl crate::layout::RenderTarget for PaintCtx {
         self.pop_clip();
     }
     fn inset_plate(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32) {
-        PaintCtx::inset_plate(self, Rect { x, y, width: w, height: h }, (radius, radius, radius, radius), color, depth);
+        PaintCtx::inset_plate(self, Rect { x, y, width: w, height: h }, (radius, radius, radius, radius), Material::face(color).as_ref(), depth);
     }
     fn inset_plate_tinted(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, tint: [f32; 3]) {
-        PaintCtx::inset_plate_tinted(self, Rect { x, y, width: w, height: h }, (radius, radius, radius, radius), color, depth, tint);
+        PaintCtx::inset_plate_tinted(self, Rect { x, y, width: w, height: h }, (radius, radius, radius, radius), Material::face(color).as_ref(), depth, tint);
     }
     fn relief_carve(&mut self, carve: &crate::layout::ReliefCarve) {
         PaintCtx::carve(self, carve);
diff --git a/src/widget/container/breadcrumb.rs b/src/widget/container/breadcrumb.rs
index 6dcc1e0..f3e2c45 100644
--- a/src/widget/container/breadcrumb.rs
+++ b/src/widget/container/breadcrumb.rs
@@ -336,22 +336,23 @@ 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 face = crate::widget::ControlPlate::face_from_fill(crate::color::dropdown_background_color());
+                let face = crate::scene::Material::control_face(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
                     // ([`Self::RAISED_FACE_OPACITY`] over the configured fill)
-                    // and ALWAYS frosted (negative alpha, the blur-behind
-                    // sentinel): a floating part shows what is under it, and
+                    // and ALWAYS frosted (`Frost::from_style`, the blur-behind
+                    // pass): a floating part shows what is under it, and
                     // at this translucency the frost is what keeps the names
                     // legible over live content beneath. A transparent
                     // configured fill keeps the boss degradation: edges only,
                     // the surface as the face.
-                    let mut c = face;
-                    if c[3] > 0.001 {
-                        c[3] = -(c[3] * Self::RAISED_FACE_OPACITY);
-                    }
+                    let c = face.map(|m| {
+                        let t = m.tint;
+                        m.with_tint([t[0], t[1], t[2], t[3] * Self::RAISED_FACE_OPACITY])
+                            .with_frost(crate::scene::Frost::from_style())
+                    });
                     (crate::widget::PlateStance::Raised, c)
                 } else {
                     (crate::widget::PlateStance::Flush, face)
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index cbec5f2..1b925e4 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -517,7 +517,7 @@ impl Paint for MenuBar {
             let radius = crate::layout::dropdown_corner_radius();
             let depth = crate::layout::bevel_width().min(trough_h * 0.2);
             let (trough, radii) = crate::layout::carve_inside(trough, (radius, radius, radius, radius), depth);
-            ctx.inset_plate(trough, radii, [0.0; 4], depth);
+            ctx.inset_plate(trough, radii, None, depth);
             if let Some(c) = fill {
                 ctx.rounded_rect(trough, radius, (true, true, true, true), c);
             }
diff --git a/src/widget/input/button.rs b/src/widget/input/button.rs
index d37a923..d484b25 100644
--- a/src/widget/input/button.rs
+++ b/src/widget/input/button.rs
@@ -262,7 +262,7 @@ impl Button {
         // Keyboard focus lights the plate's own rim — the ring IS the silhouette.
         let tint = self.focused.then(crate::widget::ControlPlate::focus_tint);
         Some(
-            crate::widget::ControlPlate::control(rect, radius, stance, self.color())
+            crate::widget::ControlPlate::control(rect, radius, stance, crate::scene::Material::face(self.color()))
                 .with_tint(tint),
         )
     }
@@ -270,7 +270,7 @@ impl Button {
     /// [`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))
+        self.plate(rect).map(|p| (p.rect, p.radii.0, p.depth, p.face_fill()))
     }
 }
 
diff --git a/src/widget/input/button_strip.rs b/src/widget/input/button_strip.rs
index ad3665a..09a443e 100644
--- a/src/widget/input/button_strip.rs
+++ b/src/widget/input/button_strip.rs
@@ -408,7 +408,7 @@ impl crate::widget::Paint for ButtonStrip {
                 // well floor, faceless (the floor shows through), at the well's
                 // depth — its rim lit while the strip holds keyboard focus.
                 pc.control_plate(
-                    &crate::widget::ControlPlate::control(seg, seg_r, crate::widget::PlateStance::Raised, [0.0; 4])
+                    &crate::widget::ControlPlate::control(seg, seg_r, crate::widget::PlateStance::Raised, None)
                         .with_depth(depth)
                         .with_tint(focus_ring.then(crate::widget::ControlPlate::focus_tint)),
                 );
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index 8a8bbf0..5f0c946 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -463,7 +463,7 @@ impl Paint for Toggle {
             ctx.recess(well, well_radii, depth);
             let focus = if self.focused { Some(ControlPlate::focus_tint()) } else { None };
             ctx.control_plate(
-                &ControlPlate::control(plate, plate_r, PlateStance::Raised, [0.0; 4])
+                &ControlPlate::control(plate, plate_r, PlateStance::Raised, None)
                     .with_depth(plate_depth)
                     .with_tint(focus),
             );
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index b1987d1..95f518f 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -113,11 +113,11 @@ pub struct Dropdown {
     /// carries one radius, and the adjustment exists to nest relief outlines.
     flat: bool,
     /// Per-widget override for the trigger plate's FACE, bypassing
-    /// [`ControlPlate::face_from_fill`] on the configured fill. The default
+    /// [`crate::scene::Material::control_face`] on the configured fill. The default
     /// forces the face opaque; an app that wants its controls made of the
     /// same frosted material as its panes passes the blur-behind sentinel (a
     /// negative alpha) here, which that helper would strip.
-    face: Option<[f32; 4]>,
+    face: Option<crate::scene::Material>,
     /// Keyboard focus (FocusIn / FocusOut): lights the trigger plate's rim.
     focused: bool,
     /// The open menu REPLACES the trigger instead of growing out of it: no
@@ -450,7 +450,7 @@ impl Dropdown {
                 Rect { x, y, width: w, height: visual_h },
                 radius,
                 crate::widget::PlateStance::Flat,
-                self.face.unwrap_or_else(|| crate::widget::ControlPlate::face_from_fill(raw_bg)),
+                self.face.or_else(|| crate::scene::Material::control_face(raw_bg)),
             )
             .with_tint(self.focused.then(crate::widget::ControlPlate::focus_tint));
             ctx.control_plate(&plate);
@@ -493,7 +493,7 @@ impl Dropdown {
                 Rect { x, y, width: w, height: visual_h },
                 radius,
                 crate::widget::PlateStance::Flush,
-                self.face.unwrap_or_else(|| crate::widget::ControlPlate::face_from_fill(raw_bg)),
+                self.face.or_else(|| crate::scene::Material::control_face(raw_bg)),
             )
             .with_radii((r4[0], r4[1], r4[2], r4[3]))
             .with_depth(depth)
@@ -779,7 +779,7 @@ impl Adapted<Dropdown> {
     }
 
     /// Override the trigger plate's face — see the `face` field.
-    pub fn with_face(mut self, face: [f32; 4]) -> Self {
+    pub fn with_face(mut self, face: crate::scene::Material) -> Self {
         self.face = Some(face);
         self
     }
diff --git a/src/widget/input/font_selector.rs b/src/widget/input/font_selector.rs
index 27f432d..b5d2d16 100644
--- a/src/widget/input/font_selector.rs
+++ b/src/widget/input/font_selector.rs
@@ -171,7 +171,7 @@ impl Paint for FontSelector {
             // 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])
+                &crate::widget::ControlPlate::control(rect, r, crate::widget::PlateStance::Flush, None)
                     .with_tint(self.focused.then(crate::widget::ControlPlate::focus_tint)),
             );
             let wash = if self.pressed {
diff --git a/tests/plate_golden.rs b/tests/plate_golden.rs
index 13c259e..25e86e6 100644
--- a/tests/plate_golden.rs
+++ b/tests/plate_golden.rs
@@ -25,6 +25,7 @@
 
 use cce_ui::scene::layout::Rect;
 use cce_ui::scene::paint::{ControlPlate, DropletSpec, PaintCtx, PlateSpec, PlateStance};
+use cce_ui::scene::Material;
 use std::fmt::Write as _;
 
 fn r(x: f32, y: f32, w: f32, h: f32) -> Rect {
@@ -101,7 +102,7 @@ fn scene(sw: f32, sh: f32) -> cce_ui::scene::paint::DisplayList {
         let mut x = 20.0;
         for face in faces {
             for tint in [None, Some(ControlPlate::focus_tint())] {
-                let plate = ControlPlate::control(r(x, y, 36.0, 20.0), 6.0, stance, face).with_tint(tint);
+                let plate = ControlPlate::control(r(x, y, 36.0, 20.0), 6.0, stance, Material::face(face)).with_tint(tint);
                 pc.control_plate(&plate);
                 x += 42.0;
             }
@@ -110,9 +111,9 @@ fn scene(sw: f32, sh: f32) -> cce_ui::scene::paint::DisplayList {
     }
 
     // Inset plates and wells.
-    pc.inset_plate(r(20.0, 290.0, 60.0, 24.0), rr, [0.2, 0.2, 0.24, 1.0], 4.0);
-    pc.inset_plate(r(90.0, 290.0, 60.0, 24.0), rr, [0.0; 4], 4.0);
-    pc.inset_plate_tinted(r(160.0, 290.0, 60.0, 24.0), rr, [0.2, 0.2, 0.24, -0.5], 4.0, [1.0, 0.5, 0.2]);
+    pc.inset_plate(r(20.0, 290.0, 60.0, 24.0), rr, Material::face([0.2, 0.2, 0.24, 1.0]).as_ref(), 4.0);
+    pc.inset_plate(r(90.0, 290.0, 60.0, 24.0), rr, Material::face([0.0; 4]).as_ref(), 4.0);
+    pc.inset_plate_tinted(r(160.0, 290.0, 60.0, 24.0), rr, Material::face([0.2, 0.2, 0.24, -0.5]).as_ref(), 4.0, [1.0, 0.5, 0.2]);
     pc.well_floor(r(230.0, 290.0, 40.0, 24.0), 6.0, false);
     pc.well_floor(r(280.0, 290.0, 40.0, 24.0), 6.0, true);
     pc.canvas_well(r(330.0, 290.0, 40.0, 24.0), 6.0, true, false);