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

commit398f965169f7c653e6ecb6898efd585fee657e73
parente2e2c1cdb8
authorLucas Galante <[email protected]>
date2026-08-25 13:39
7b-1: PlateSpec — plates carry their role; DemoApp migrates

RFC Phase 7b, first cut, with the blur-regime design note written into the
RFC first per the phase invariant. PlateSpec (scene/paint.rs): rect,
positive-alpha color, blur flag, per-corner window_corners role flags, and
perimeter depth. window_corner_flags derives the flags from window geometry
(the designer's pane_plate_radii derivation, moved in); radii_for gives
window-flagged corners the SHARED silhouette curve (window_corner_radius x
corner_span_factor) and interior corners the nominal plate radius; fill()
applies the role-correct frost encoding — root stays positive-alpha (the
compositor frosts behind the window), nested+blur takes the in-app frost
pass's negative-alpha sentinel. PaintCtx::plate_spec emits it.

DemoApp's hand-rolled root plate migrates to the spec — a deliberate
visual correction: its un-spanned root_plate_corner_radius detached the
perimeter shading from the compositor's span-widened clip under squircle
corner_shape; the spec snaps it to the silhouette.

Also two 7a stragglers the getter census could not see (raw JSON-pointer
reads): layout::window_corner_radius now reads the canonical
plate/root/corner_radius pointer first (get_i64_shared_opt added), legacy
backplate second. Role tests cover flag derivation, radii selection, and
both frost encodings; designer A/B AE=0, demo corner change verified
intended.

 docs/rfc-core-rebuild.md |  37 ++++++++++++++
 src/config.rs            |   6 +++
 src/layout.rs            |   7 ++-
 src/main.rs              |  15 ++++--
 src/scene/paint.rs       | 126 +++++++++++++++++++++++++++++++++++++++++++++++
 5 files changed, 187 insertions(+), 4 deletions(-)

diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index 0474c5c..29167b6 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -2450,6 +2450,43 @@ Constraint respected: **each crate still builds standalone** — the new core is
     `pane_plate_radii` in, and give the engine a root-plate paint path fed by a spec instead of
     each app's hand-rolled quads (DemoApp first, then the clients). The designer's per-pane
     plates convert to specs with computed role flags. A/B: AE=0 per app.
+
+    **Blur-regime design note (required before code; written 2026-08-25).** Two frost
+    regimes; the role selects between them:
+    - *Root plate* (all four corners on the window silhouette): the fill stays
+      POSITIVE-alpha translucent; the COMPOSITOR frosts what lies behind the window
+      (`plate.root.blur` in the shared config drives cce-fx's blur-behind). The app
+      draws no frost of its own.
+    - *Nested plate* (any interior corner): frost is the NEGATIVE-ALPHA sentinel on
+      the fill — the in-app vk frost pass blurs app content drawn BEFORE the plate
+      (draw-order-dependent by design; see `param_plate_fill`). A pane touching some
+      window edges is still nested for blur purposes: it frosts app content.
+    `PlateSpec` therefore stores `color` with positive alpha plus `blur: bool`, and
+    `fill()` applies the role-correct encoding: root → alpha forced non-negative,
+    nested+blur → alpha negated. DETACH is exactly a role flip: interior corners become
+    window corners, `fill()` flips regimes, and the formerly-frosted app content beneath
+    simply does not exist in the new window. No other app-side blur change is needed.
+
+    **Radii rule.** A window-flagged corner wears
+    `window_corner_radius() * corner_span_factor()` (the SHARED silhouette curve — the
+    invariant); an interior corner wears `plate_corner_radius()` (the app-overridable
+    pane value). This is `pane_plate_radii` verbatim, moved in.
+
+    - **7b-1 DONE (2026-08-25).** `PlateSpec` in `scene/paint.rs` (rect, positive-alpha
+      color, `blur`, per-corner `window_corners`, perimeter `depth`) with
+      `window_corner_flags(rect, win_w, win_h)`, `radii()`/`radii_for()`, role-aware
+      `fill()`, and `PaintCtx::plate_spec`. DemoApp's hand-rolled root plate migrated —
+      DELIBERATE visual correction: its radius was the un-spanned
+      `root_plate_corner_radius`, so under squircle `corner_shape` its perimeter shading
+      detached from the compositor's span-widened clip; the spec snaps it to the
+      silhouette (demo AE≠0 expected and intended). The designer's `pane_plate_radii`
+      delegates to the toolkit (A/B AE=0). Two 7a stragglers the getter census could not
+      see (raw JSON-pointer reads, not getter calls) gained the canonical-first chain:
+      `layout::window_corner_radius`'s shared-config read, and cce-grid's silhouette
+      read.
+    - **7b-2 — pending.** Migrate the remaining clients' hand-rolled root plates onto
+      `PlateSpec` (per-repo, AE=0 each) and convert the designer's pane plate EMISSION
+      (not just radii) to specs. 7c builds on the role flip.
   - **7c — Detach/dock generalization.** Lift the designer's plate-corner control, collapse,
     and dock-drag onto `PlateSpec` so any app can offer them. The detached-window PROCESS model
     and sync channel (`default_project.json` polling) remain app policy — the toolkit provides
diff --git a/src/config.rs b/src/config.rs
index 96875f3..c18a935 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -614,6 +614,12 @@ pub fn get_i64_shared(pointer: &str, default: i64) -> i64 {
     cached_shared_config().pointer(pointer).and_then(|v| v.as_i64()).unwrap_or(default)
 }
 
+/// [`get_i64_shared`] without a default — for canonical-first alias chains
+/// (RFC Phase 7a) where absence must fall through to the next spelling.
+pub fn get_i64_shared_opt(pointer: &str) -> Option<i64> {
+    cached_shared_config().pointer(pointer).and_then(|v| v.as_i64())
+}
+
 // ── Typed accessors over the cached config ──────────────────────────────────
 // Each reads the mtime-cached config and extracts a value at a JSON pointer
 // (e.g. "/notifications/enable"), returning the default when absent or mistyped.
diff --git a/src/layout.rs b/src/layout.rs
index c250f14..6740ee2 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -1596,7 +1596,12 @@ pub fn corner_shape() -> f32 {
 /// override file — or its corners detach from the silhouette (and from the
 /// desktop grid's cells, which share the same knob).
 pub fn window_corner_radius() -> f32 {
-    crate::config::get_i64_shared("/style/surface/backplate/corner_radius", 12) as f32
+    // Canonical-first (RFC Phase 7a): the shared config may spell the
+    // silhouette radius either way; both feed the one value.
+    crate::config::get_i64_shared_opt("/style/surface/plate/root/corner_radius")
+        .unwrap_or_else(|| {
+            crate::config::get_i64_shared("/style/surface/backplate/corner_radius", 12)
+        }) as f32
 }
 
 /// The curvature-matched corner-span factor for window-scale squircle corners.
diff --git a/src/main.rs b/src/main.rs
index 2d97f77..00b09c0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -363,10 +363,19 @@ impl Application for DemoApp {
         if plate[3] > 0.001 {
             plate[3] = cce_ui::color::root_plate_opacity();
         }
-        let radius = cce_ui::colors::root_plate_corner_radius();
+        // The root plate as a PlateSpec (RFC Phase 7b): all four corners are
+        // window corners, so the radii come from the SHARED silhouette curve
+        // — under squircle corner_shape this widens the perimeter roll to
+        // match the compositor's clip, which the old hand-rolled
+        // root_plate_corner_radius did not.
         let frame = Rect { x: 0.0, y: 0.0, width: w, height: h };
-        let bevel = cce_ui::layout::bevel_width();
-        pc.plate(frame, (radius, radius, radius, radius), plate, bevel);
+        pc.plate_spec(&cce_ui::scene::paint::PlateSpec {
+            rect: frame,
+            color: plate,
+            blur: false,
+            window_corners: (true, true, true, true),
+            depth: cce_ui::layout::bevel_width(),
+        });
 
         // Header band: the title strip carved one step down into the plate. Flush to the
         // window's top and sides, so its only real wall is the bottom one facing the
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index dda2714..71513d8 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -33,6 +33,83 @@ pub enum Cap {
 /// `CornerRadii` order.
 pub type Radii = (f32, f32, f32, f32);
 
+/// RFC Phase 7b: the ONE description of a lit base surface — a window's root
+/// plate or a nested pane plate — distinguished only by ROLE data, never by
+/// 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.
+#[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,
+    /// 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.
+    pub depth: f32,
+}
+
+impl PlateSpec {
+    /// All four corners on the silhouette: this plate IS the window's base
+    /// surface.
+    pub fn is_root(&self) -> bool {
+        let (tl, tr, br, bl) = self.window_corners;
+        tl && tr && br && bl
+    }
+
+    /// Which of `rect`'s corners lie on a `win_w` x `win_h` window's
+    /// silhouette (edge tolerance 1.5px) — the designer's `pane_plate_radii`
+    /// derivation, toolkit-side.
+    pub fn window_corner_flags(rect: Rect, win_w: f32, win_h: f32) -> (bool, bool, bool, bool) {
+        let e = 1.5;
+        let left = rect.x <= e;
+        let top = rect.y <= e;
+        let right = rect.x + rect.width >= win_w - e;
+        let bottom = rect.y + rect.height >= win_h - e;
+        (top && left, top && right, bottom && right, bottom && left)
+    }
+
+    /// Per-corner radii for `flags`: a window corner wears the SHARED
+    /// silhouette curve (`window_corner_radius * corner_span_factor` — the
+    /// compositor clips the window and the desktop grid draws its cells from
+    /// the same value, so window-corner arcs must follow it, never a per-app
+    /// plate override); an interior corner wears the nominal
+    /// `plate_corner_radius`.
+    pub fn radii_for(flags: (bool, bool, bool, bool)) -> Radii {
+        let nominal = crate::layout::plate_corner_radius();
+        let window_r =
+            crate::layout::window_corner_radius() * crate::layout::corner_span_factor();
+        let (tl, tr, br, bl) = flags;
+        let pick = |on: bool| if on { window_r } else { nominal };
+        (pick(tl), pick(tr), pick(br), pick(bl))
+    }
+
+    /// [`Self::radii_for`] over this spec's flags.
+    pub fn radii(&self) -> Radii {
+        Self::radii_for(self.window_corners)
+    }
+
+    /// The fill with the role-correct frost encoding: root → alpha forced
+    /// non-negative (the compositor's frost, not ours), nested + `blur` →
+    /// the in-app frost pass's negative-alpha sentinel.
+    pub fn fill(&self) -> [f32; 4] {
+        let mut c = self.color;
+        if self.is_root() {
+            c[3] = c[3].abs();
+        } else if self.blur {
+            c[3] = -c[3].abs();
+        }
+        c
+    }
+}
+
 /// The **relief primitives** are the members of this enum that describe a lit
 /// surface rather than a flat fill: [`Prim::Bevel`], [`Prim::Plate`],
 /// [`Prim::Recess`], [`Prim::Boss`], [`Prim::Ridge`], [`Prim::ConcaveFillet`],
@@ -923,6 +1000,12 @@ impl PaintCtx {
         self.push(Prim::Plate { rect, radii, color, depth });
     }
 
+    /// Emit the plate a [`PlateSpec`] describes: role-resolved per-corner
+    /// radii and role-encoded frost (RFC Phase 7b).
+    pub fn plate_spec(&mut self, spec: &PlateSpec) {
+        self.plate(spec.rect, spec.radii(), spec.fill(), spec.depth);
+    }
+
     pub fn arc(&mut self, cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4]) {
         let (ox, oy) = self.offset;
         self.push(Prim::Arc { cx: cx + ox, cy: cy + oy, radius, thickness, start, end, color });
@@ -1124,6 +1207,49 @@ impl crate::layout::RenderTarget for PaintCtx {
 mod tests {
     use super::*;
 
+    /// RFC Phase 7b: PlateSpec role mechanics — flag derivation from window
+    /// geometry, silhouette-vs-nominal radii selection, and the role-encoded
+    /// frost (root positive-alpha, nested negative-alpha sentinel).
+    #[test]
+    fn plate_spec_roles() {
+        // Flags: a full-window rect is root; an inset pane has none; a pane
+        // flush to the window's right edge owns the two right corners.
+        let root_flags = PlateSpec::window_corner_flags(
+            Rect { x: 0.0, y: 0.0, width: 800.0, height: 600.0 }, 800.0, 600.0);
+        assert_eq!(root_flags, (true, true, true, true));
+        let inset = PlateSpec::window_corner_flags(
+            Rect { x: 20.0, y: 20.0, width: 100.0, height: 100.0 }, 800.0, 600.0);
+        assert_eq!(inset, (false, false, false, false));
+        let right_pane = PlateSpec::window_corner_flags(
+            Rect { x: 500.0, y: 0.0, width: 300.0, height: 600.0 }, 800.0, 600.0);
+        assert_eq!(right_pane, (false, true, true, false));
+
+        // Radii: flagged corners wear the shared silhouette curve, interior
+        // ones the nominal plate radius (compared against the same getters,
+        // so the assertion holds for any configured values).
+        let window_r =
+            crate::layout::window_corner_radius() * crate::layout::corner_span_factor();
+        let nominal = crate::layout::plate_corner_radius();
+        let r = PlateSpec::radii_for((false, true, true, false));
+        assert_eq!(r, (nominal, window_r, window_r, nominal));
+
+        // Frost encoding by role.
+        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,
+            window_corners: (true, true, true, true),
+            depth: 3.0,
+        };
+        assert!(spec.is_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!(spec.fill()[3] < 0.0, "nested frost = negative-alpha sentinel");
+        spec.blur = false;
+        assert_eq!(spec.fill()[3], 0.8, "no frost, no encoding");
+    }
+
     fn r(x: f32, y: f32, w: f32, h: f32) -> Rect {
         Rect { x, y, width: w, height: h }
     }