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