GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
7b fix: plate_spec double-spanned every window corner
The spec's radii are FINAL on-screen values — a window corner already
wears the full silhouette span. Prim::Plate speaks the older convention:
NOMINAL radii, span applied downstream by plate_push_raised
(scale_corners = true), which the unmigrated hand-rolled plates
(cce-cloud, the test-interface gallery shim) still rely on. plate_spec
fed the final values straight through, so the five Plate-group clients
(files, terminal, system-interface, authenticator, data-editor) drew
window corners at span squared — 12 → ~100 logical at corner_shape 4.5.
The plate arc pulled away from the compositor clip, the black window
background showed through as a corner crescent, and corners stopped
matching the desktop grid.
Those five apps had been correct BEFORE 7b-2 (nominal in, spanned once
by the push); the "arc correction" the A/B eyeball accepted was the
regression itself. It surfaced as a user report — corners visibly
rounder than the grid — and was pinned by measuring a live corner
diagonal: clip and grid arc at the expected 0.202·span depth (~14 device
px), plate arc ~2x deeper, the circle-vs-superellipse ratio a red
herring against the span-squared superellipse. The four scalar-group
clients tessellate without a downstream span; their spanned scalar was
and remains the genuine correction and is untouched here.
Fix: plate_spec pre-divides all four radii by corner_span_factor() so
the push's multiply reconstructs the spec's exact values — one function,
no Prim change, no per-app edits, and interior corners now land at
exactly plate_corner_radius() instead of being quietly spanned. Unit
test plate_spec_emission_round_trips_the_span guards the round-trip for
any configured corner_shape. RFC 7b notes corrected, and the stale
"PROPOSED, not started" Phase 7 header updated to the real state.
Clients need a REBUILD to pick this up (toolkit change; see the
WORKSPACE.md note) — the five Plate-group apps ship it with their next
install.
Co-Authored-By: Claude Opus 5 <[email protected]>
docs/rfc-core-rebuild.md | 25 ++++++++++++++++++++---
src/scene/paint.rs | 52 +++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 73 insertions(+), 4 deletions(-)
diff --git a/docs/rfc-core-rebuild.md b/docs/rfc-core-rebuild.md
index 350a021..6861390 100644
--- a/docs/rfc-core-rebuild.md
+++ b/docs/rfc-core-rebuild.md
@@ -2347,7 +2347,7 @@ Constraint respected: **each crate still builds standalone** — the new core is
phase-4 flip reaches them first; no standalone pointer-to-id
signature sweep.
-- **Phase 7 — Plate unification: backplate becomes a ROLE of Plate (PROPOSED, not started).**
+- **Phase 7 — Plate unification: backplate becomes a ROLE of Plate (7a/7b/7c stages DONE 2026-08-25; open questions below).**
Finish what 6as/6at began. The `Backplate` and `Plate` container widgets are deleted and the
`is_backplate`/`is_movable_backplate`/`is_plate` flags are folded, but "backplate" survives as
a second vocabulary for what is now one concept — a lit base surface (`Prim::Plate`). What
@@ -2495,6 +2495,23 @@ Constraint respected: **each crate still builds standalone** — the new core is
design. Verified: seven apps diff ONLY within 120px corner squares; the eighth
(system-interface, hardcoded r=12 → silhouette, the largest jump) also shifts the
perimeter roll's edge gradient, eyeball-confirmed as the arc correction.
+
+ **CORRECTION (2026-08-25, post-7c): the Plate-group half of that story was
+ inverted — a double-span, since fixed.** `plate_spec` fed the spec's FINAL
+ radii into `Prim::Plate`, whose contract is NOMINAL radii spanned downstream
+ by `plate_push_raised(scale_corners = true)` — so the five Plate-group
+ clients drew window corners at span² (12 → ~100 logical at n=4.5). Those
+ apps had been CORRECT all along (nominal in, spanned once by the push); the
+ "arc correction" the A/B eyeball accepted was the regression itself, caught
+ when the user reported corners rounder than the desktop grid. Measured on a
+ live corner diagonal: clip/grid arc at the expected 0.202·span depth, plate
+ arc ~2× deeper. The four scalar-group clients (graph, fonts, color-editor,
+ text-editor) tessellate without a downstream span, so for them the spanned
+ scalar was and remains the genuine correction. Fix: `plate_spec` pre-divides
+ by `corner_span_factor()` so the push's multiply reconstructs the spec's
+ exact values; unit test `plate_spec_emission_round_trips_the_span` guards
+ it. The same double-span reached DemoApp via 7b-1 — its "AE≠0 expected"
+ diff bundled the genuine correction WITH the overshoot.
Authenticator is values-only verification (never launch it in a shadow — it claims
the PolicyKit D-Bus name). SKIPPED deliberately: cce-cloud (overlay popup windows —
whether they share the decorated-window silhouette is an open question for 7c) and
@@ -2546,8 +2563,10 @@ Constraint respected: **each crate still builds standalone** — the new core is
appear). It lands with the first non-designer detach implementation, as does the
cce-cloud overlay-silhouette question from 7b-2.
- Not started; no code moves before 7a's alias sweep is reviewed, and stages land in order —
- 7b is the value (one paint path, plates compositional), 7c is the payoff feature.
+ All numbered stages DONE (2026-08-25). Still open: detached-window CSD packaging (lands
+ with the first non-designer detach), the cce-cloud overlay-silhouette question, and
+ designer pane spec-OBJECT emission (focus tint / widget-driven bevel styling are not yet
+ spec fields).
Order rationale: each phase is independently valuable and reversible, and no phase requires the
next to compile. Phase 0 can land immediately regardless of the rest.
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index ae6c1e1..7247df7 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -1012,8 +1012,25 @@ impl PaintCtx {
/// Emit the plate a [`PlateSpec`] describes: role-resolved per-corner
/// radii and role-encoded frost (RFC Phase 7b).
+ ///
+ /// The spec's radii are FINAL on-screen values (a window corner already
+ /// wears the full silhouette span), but `Prim::Plate` speaks the older
+ /// convention — NOMINAL radii, span applied downstream by
+ /// `plate_push_raised(scale_corners = true)`, which the unmigrated
+ /// hand-rolled plates (cce-cloud, the test-interface gallery shim) still
+ /// rely on. So divide the span back out here and let the push multiply
+ /// reconstruct the spec's exact values.
+ ///
+ /// Feeding the final radii straight through double-spanned every window
+ /// corner (12 → ~100 logical at corner_shape 4.5): the plate arc pulled
+ /// away from the compositor's clip, the black window background showed
+ /// through as a corner crescent, and the corners stopped matching the
+ /// desktop grid — the original 7b-2 report of this looking like "the arc
+ /// correction" was the regression itself.
pub fn plate_spec(&mut self, spec: &PlateSpec) {
- self.plate(spec.rect, spec.radii(), spec.fill(), spec.depth);
+ let f = crate::layout::corner_span_factor();
+ let (tl, tr, br, bl) = spec.radii();
+ self.plate(spec.rect, (tl / f, tr / f, br / f, bl / f), spec.fill(), spec.depth);
}
pub fn arc(&mut self, cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4]) {
@@ -1275,6 +1292,39 @@ mod tests {
Rect { x, y, width: w, height: h }
}
+ /// The emission round-trip: `plate_spec` pre-divides by the span factor so
+ /// `plate_push_raised(scale_corners = true)` lands each corner at exactly
+ /// the spec's final radius. Guards the double-span regression (7b-2), and
+ /// holds for any configured corner_shape because both sides use the same
+ /// factor.
+ #[test]
+ 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,
+ window_corners: (true, true, false, false),
+ depth: 4.0,
+ };
+ let mut pc = PaintCtx::new();
+ pc.plate_spec(&spec);
+ let f = crate::layout::corner_span_factor();
+ let emitted = pc
+ .finish()
+ .items
+ .iter()
+ .find_map(|it| match &it.prim {
+ Prim::Plate { radii, .. } => Some(radii.clone()),
+ _ => None,
+ })
+ .expect("plate_spec emits a Prim::Plate");
+ let want = spec.radii();
+ let got = (emitted.0 * f, emitted.1 * f, emitted.2 * f, emitted.3 * f);
+ for (g, w) in [(got.0, want.0), (got.1, want.1), (got.2, want.2), (got.3, want.3)] {
+ assert!((g - w).abs() < 1e-3, "span round-trip drifted: {g} vs {w}");
+ }
+ }
+
#[test]
fn emits_in_order_unclipped() {
let mut ctx = PaintCtx::new();