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

commit8e63dade29d3658c5b0b0fdff0099b6c6cefb9a4
parent385fdb5035
authorLucas Galante <[email protected]>
date2026-09-20 14:19
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()
+            );
+        }
+    }
+}