GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
PlateSpec carries a Material, not a colour and a flag (RFC material, step 2a)
PlateSpec { rect, material, window_corners, depth }: the tint, frost and
finish travel together, and fill() is material.fill(role()) — the same
bytes as before, since Material::fill_tint is the rule PlateSpec::fill
already called. Every client site is Material::opaque(<its colour>);
none of them frosted.
tests/plate_golden.rs is the exit test for this migration: one scene
through every surface that carries a material (root and nested plates in
both frost regimes, the roll overlay, bevels, every ControlPlate stance
× face × tint, inset plates, wells, sphere, droplet), tessellated at two
scales on both edge paths and dumped as text. Generated from the commit
before this one and compared after it: identical, 148518 lines.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/main.rs | 3 +-
src/scene/paint.rs | 38 +++++-----
tests/plate_golden.rs | 188 ++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 209 insertions(+), 20 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index 359798d..c5138c0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -371,8 +371,7 @@ impl Application for DemoApp {
let frame = Rect { x: 0.0, y: 0.0, width: w, height: h };
pc.plate_spec(&cce_ui::scene::paint::PlateSpec {
rect: frame,
- color: plate,
- blur: false,
+ material: cce_ui::scene::Material::opaque(plate),
window_corners: (true, true, true, true),
depth: cce_ui::layout::bevel_width(),
});
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index ffc1d71..6a28d3a 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -18,7 +18,7 @@
//! follow-ups.
use crate::scene::layout::Rect;
-use crate::scene::material::{Frost, Material, PlateRole};
+use crate::scene::material::{Material, PlateRole};
/// End-cap style for a [`Prim::Vector`], mirroring the toolkit's line caps.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -39,18 +39,18 @@ pub type Radii = (f32, f32, f32, f32);
/// type. A window root is a plate whose four corners are all window corners;
/// detaching a pane into its own window is a role flip, nothing more.
///
-/// `color` always carries POSITIVE alpha; the frost encoding is applied by
-/// [`Self::fill`] per the role (see the Phase 7b blur-regime note in
-/// `docs/rfc-core-rebuild.md`): a root plate stays positive-alpha (the
-/// COMPOSITOR frosts behind the window), a nested plate with `blur` encodes
-/// the in-app frost pass's negative-alpha sentinel.
+/// The material's tint always carries POSITIVE alpha; the frost encoding is
+/// applied by [`Self::fill`] per the role (see the Phase 7b blur-regime note
+/// in `docs/rfc-core-rebuild.md`): a root plate stays positive-alpha (the
+/// COMPOSITOR frosts behind the window), a nested plate whose material is
+/// [`Frost::Frosted`] encodes the in-app frost pass's negative-alpha sentinel.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PlateSpec {
pub rect: Rect,
- /// Fill, linear RGBA, alpha positive — the role encoding is `fill()`'s.
- pub color: [f32; 4],
- /// Frost the surface (encoding per role; see `fill`).
- pub blur: bool,
+ /// What the plate is made of: tint, frost and finish
+ /// (`docs/rfc-material.md`). `Material::root()` / `Material::pane()` are
+ /// the rung defaults; `Material::opaque(c)` an app's own colour.
+ pub material: Material,
/// Which corners lie ON the window silhouette (TL, TR, BR, BL).
pub window_corners: (bool, bool, bool, bool),
/// Transition-band width of the rolled perimeter. Negative = the fill-less
@@ -115,11 +115,11 @@ impl PlateSpec {
}
/// The fill with the role-correct frost encoding: root → alpha forced
- /// non-negative (the compositor's frost, not ours), nested + `blur` →
+ /// non-negative (the compositor's frost, not ours), nested + frosted →
/// the in-app frost pass's negative-alpha sentinel. The rule itself is
/// [`Material::fill_tint`], the one place a negative alpha is written.
pub fn fill(&self) -> [f32; 4] {
- Material::fill_tint(self.color, Frost::from_flag(self.blur), self.role())
+ self.material.fill(self.role())
}
}
@@ -1678,6 +1678,7 @@ impl crate::layout::RenderTarget for PaintCtx {
#[cfg(test)]
mod tests {
use super::*;
+ use crate::scene::material::Frost;
/// RFC Phase 7b: PlateSpec role mechanics — flag derivation from window
/// geometry, silhouette-vs-nominal radii selection, and the role-encoded
@@ -1706,25 +1707,27 @@ mod tests {
assert_eq!(r, (nominal, window_r, window_r, nominal));
// Frost encoding by role.
+ let frosted = Frost::Frosted { compression: 0.0, refraction: 0.0, radius: Frost::DEFAULT_RADIUS };
let mut spec = PlateSpec {
rect: Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 },
- color: [0.1, 0.2, 0.3, 0.8],
- blur: true,
+ material: Material::opaque([0.1, 0.2, 0.3, 0.8]).with_frost(frosted),
window_corners: (true, true, true, true),
depth: 3.0,
};
assert!(spec.is_root());
+ assert_eq!(spec.role(), PlateRole::Root);
assert!(spec.fill()[3] > 0.0, "root frost is the compositor's; alpha stays positive");
spec.window_corners = (false, true, true, false);
assert!(!spec.is_root());
+ assert_eq!(spec.role(), PlateRole::Nested);
assert!(spec.fill()[3] < 0.0, "nested frost = negative-alpha sentinel");
- spec.blur = false;
+ spec.material.frost = Frost::Opaque;
assert_eq!(spec.fill()[3], 0.8, "no frost, no encoding");
// The detach role flip (RFC 7c): a frosted nested pane becomes a
// root — silhouette corners, and the frost regime flips from the
// in-app sentinel to the compositor's (alpha back to positive).
- spec.blur = true;
+ spec.material.frost = frosted;
assert!(spec.fill()[3] < 0.0);
let det = spec.detached();
assert!(det.is_root());
@@ -1746,8 +1749,7 @@ mod tests {
fn plate_spec_emission_round_trips_the_span() {
let spec = PlateSpec {
rect: r(0.0, 0.0, 400.0, 300.0),
- color: [0.1, 0.2, 0.3, 0.8],
- blur: false,
+ material: Material::opaque([0.1, 0.2, 0.3, 0.8]),
window_corners: (true, true, false, false),
depth: 4.0,
};
diff --git a/tests/plate_golden.rs b/tests/plate_golden.rs
new file mode 100644
index 0000000..51aea06
--- /dev/null
+++ b/tests/plate_golden.rs
@@ -0,0 +1,188 @@
+//! The plate-path golden: one scene through every surface that carries a
+//! material — root and nested plates in both frost regimes, the roll overlay,
+//! bevels, every `ControlPlate` stance with and without a face and a focus
+//! tint, inset plates, wells, the sphere and the droplet — tessellated at two
+//! scales on both the SDF and the legacy edge path, and dumped as text.
+//!
+//! This is the RFC-material migration's exit test (`docs/rfc-material.md`
+//! § 7): a step that must not move a pixel proves it by tessellating the SAME
+//! SCENE to the SAME BYTES as the commit before it. The scene is built through
+//! the public paint API, so the builder is rewritten as the API changes while
+//! the dump it must reproduce is not.
+//!
+//! Not a fixture check — the dump depends on the machine's live style config
+//! (roll width, radii, frost recipe), so it is generated and compared on the
+//! same machine across commits:
+//!
+//! ```sh
+//! CCE_PLATE_GOLDEN_WRITE=/tmp/golden.txt cargo test -p cce-ui --test plate_golden
+//! # ...migrate...
+//! CCE_PLATE_GOLDEN=/tmp/golden.txt cargo test -p cce-ui --test plate_golden
+//! ```
+//!
+//! With neither variable set the test passes trivially (it only asserts that
+//! the scene tessellates).
+
+use cce_ui::scene::layout::Rect;
+use cce_ui::scene::paint::{ControlPlate, DropletSpec, PaintCtx, PlateSpec, PlateStance};
+use std::fmt::Write as _;
+
+fn r(x: f32, y: f32, w: f32, h: f32) -> Rect {
+ Rect { x, y, width: w, height: h }
+}
+
+/// The scene. Every branch of the tessellator that reads a colour or the
+/// material push constants is reached at least once.
+fn scene(sw: f32, sh: f32) -> cce_ui::scene::paint::DisplayList {
+ let mut pc = PaintCtx::new();
+ let depth = cce_ui::layout::bevel_width();
+ let rr = (8.0, 8.0, 8.0, 8.0);
+
+ // Root plate: opaque, and "frosted" (alpha must stay positive — the
+ // compositor's regime).
+ pc.plate_spec(&PlateSpec {
+ rect: r(0.0, 0.0, sw, sh),
+ material: cce_ui::scene::Material::opaque([0.10, 0.10, 0.14, 0.9]),
+ window_corners: (true, true, true, true),
+ depth,
+ });
+ pc.plate_spec(&PlateSpec {
+ rect: r(0.0, 0.0, sw, sh),
+ material: cce_ui::scene::Material::opaque([0.10, 0.10, 0.14, 0.9]).with_frost(cce_ui::scene::Frost::from_style()),
+ window_corners: (true, true, true, true),
+ depth,
+ });
+ // A carve into the root plate: exercises the CSG feature grouping.
+ pc.recess(r(20.0, 20.0, 100.0, 30.0), rr, depth.min(6.0));
+
+ // Nested pane plates: opaque, frosted (the in-app sentinel), on a right
+ // edge (mixed corners).
+ pc.plate_spec(&PlateSpec {
+ rect: r(20.0, 60.0, 160.0, 100.0),
+ material: cce_ui::scene::Material::opaque([0.2, 0.2, 0.25, 0.8]),
+ window_corners: (false, false, false, false),
+ depth: depth.min(6.0),
+ });
+ pc.plate_spec(&PlateSpec {
+ rect: r(200.0, 60.0, 160.0, 100.0),
+ material: cce_ui::scene::Material::opaque([0.02, 0.02, 0.03, 0.25]).with_frost(cce_ui::scene::Frost::from_style()),
+ window_corners: (false, false, false, false),
+ depth: depth.min(6.0),
+ });
+ pc.plate_spec(&PlateSpec {
+ rect: r(sw - 120.0, 0.0, 120.0, sh),
+ material: cce_ui::scene::Material::opaque([0.2, 0.2, 0.25, 0.8]).with_frost(cce_ui::scene::Frost::from_style()),
+ window_corners: (false, true, true, false),
+ depth,
+ });
+ // The fill-less roll overlay (negative depth) and an explicit shape.
+ pc.plate_spec(&PlateSpec {
+ rect: r(0.0, 0.0, sw, sh),
+ material: cce_ui::scene::Material::opaque([0.0; 4]),
+ window_corners: (true, true, true, true),
+ depth: -depth,
+ });
+ pc.plate_shaped(r(30.0, 170.0, 40.0, 40.0), (20.0, 20.0, 20.0, 20.0), [0.3, 0.3, 0.35, 1.0], 4.0, Some(2.0));
+
+ // Bare plates the legacy callers emit: an opaque face, a frosted one
+ // (negative alpha handed straight in, the menu idiom).
+ pc.plate(r(80.0, 170.0, 60.0, 30.0), rr, [0.13, 0.14, 0.16, 1.0], 4.0);
+ pc.plate(r(150.0, 170.0, 60.0, 30.0), rr, [0.13, 0.14, 0.16, -0.8], 4.0);
+
+ // Bevels, plain and tinted (the focused-pane ring).
+ pc.bevel(r(220.0, 170.0, 60.0, 30.0), rr, [0.25, 0.25, 0.3, 1.0], 4.0);
+ pc.bevel_tinted(r(290.0, 170.0, 60.0, 30.0), rr, [0.25, 0.25, 0.3, 1.0], 4.0, [0.4, 0.6, 1.0]);
+ pc.bevel_tinted(r(290.0, 205.0, 60.0, 30.0), rr, [0.0; 4], 4.0, [0.4, 0.6, 1.0]);
+
+ // Every control stance × face × tint.
+ let faces: [[f32; 4]; 3] = [[0.3, 0.3, 0.36, 1.0], [0.0; 4], [0.3, 0.3, 0.36, -0.6]];
+ let mut y = 210.0;
+ for stance in [PlateStance::Raised, PlateStance::Flush, PlateStance::Flat] {
+ 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);
+ pc.control_plate(&plate);
+ x += 42.0;
+ }
+ }
+ y += 26.0;
+ }
+
+ // 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.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);
+ pc.canvas_well(r(330.0, 320.0, 40.0, 24.0), 6.0, false, true);
+
+ // The lit ball and the water: default spec, and one with its own finish
+ // and depth terms.
+ pc.sphere(40.0, 340.0, 10.0, [0.4, 0.4, 0.5, 1.0]);
+ pc.droplet(r(80.0, 330.0, 120.0, 30.0), [0.1, 0.1, 0.12, -0.7], DropletSpec::default());
+ let wet = DropletSpec::parse("gleam=1.0 shine=16 rim=0.3 clarity=0.5 core=0.4 shadow=0.3 refr=2");
+ pc.droplet(r(220.0, 330.0, 120.0, 30.0), [0.1, 0.1, 0.12, 0.9], wet);
+ pc.droplet_scrim(r(220.0, 365.0, 120.0, 30.0), [0.1, 0.1, 0.12, 0.9], wet, 3.0);
+
+ pc.finish()
+}
+
+fn dump(out: &mut String, label: &str, dl: &cce_ui::scene::paint::DisplayList, sw: f32, sh: f32, scale: f32) {
+ let (verts, batches, images, features) =
+ cce_ui::backend::window_runner::tessellate_display_list(dl, sw, sh, scale);
+ writeln!(out, "== {label} scale={scale} verts={} batches={} images={} features={}",
+ verts.len(), batches.len(), images.len(), features.len()).unwrap();
+ for (i, v) in verts.iter().enumerate() {
+ writeln!(out, "v{i} {:?} {:?} {:?}", v.position, v.color, v.clip_circle).unwrap();
+ }
+ for (i, b) in batches.iter().enumerate() {
+ writeln!(out, "b{i} scissor={:?} clip={:?} {}..{} blur={} plate={:?}",
+ b.scissor, b.clip_rrect, b.start, b.end, b.blur_behind, b.plate).unwrap();
+ }
+ for (i, f) in features.iter().enumerate() {
+ writeln!(out, "f{i} {f:?}").unwrap();
+ }
+}
+
+#[test]
+fn plate_paths_tessellate_to_the_golden() {
+ let (sw, sh) = (400.0, 400.0);
+ let mut out = String::new();
+ for shader in [true, false] {
+ cce_ui::layout::get_style_registry().write().unwrap().set_float("bevel_shader", if shader { 1.0 } else { 0.0 });
+ let dl = scene(sw, sh);
+ for scale in [1.0, 2.0] {
+ dump(&mut out, if shader { "sdf" } else { "legacy" }, &dl, sw, sh, scale);
+ }
+ }
+ cce_ui::layout::get_style_registry().write().unwrap().set_float("bevel_shader", 1.0);
+ assert!(out.lines().count() > 100, "the scene tessellated to almost nothing");
+
+ if let Ok(path) = std::env::var("CCE_PLATE_GOLDEN_WRITE") {
+ std::fs::write(&path, &out).expect("write golden");
+ eprintln!("wrote {} lines to {path}", out.lines().count());
+ return;
+ }
+ if let Ok(path) = std::env::var("CCE_PLATE_GOLDEN") {
+ let want = std::fs::read_to_string(&path).expect("read golden");
+ if want != out {
+ let mut shown = 0;
+ for (n, (a, b)) in want.lines().zip(out.lines()).enumerate() {
+ if a != b {
+ eprintln!("line {}:\n golden: {a}\n now: {b}", n + 1);
+ shown += 1;
+ if shown >= 12 {
+ break;
+ }
+ }
+ }
+ panic!(
+ "tessellation drifted from {path}: {} vs {} lines, first differences above",
+ want.lines().count(),
+ out.lines().count()
+ );
+ }
+ }
+}