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

src/scene/paint.rs (97.3K)

   1 //! Display list + paint context — Phase 3 of the core rebuild (single paint path).
   2 //!
   3 //! Today the toolkit paints through **three** uncoordinated routes — the app's top-level
   4 //! `view*` methods, each container's recursive `all_quads`/`all_rounded_quads` (with clipping
   5 //! hand-copied into every container), and the immediate-mode `render_widget`/`SectionContext`.
   6 //! Nothing arbitrates z-order (hence the `overlay_quads` escape hatch) and every clip is CPU
   7 //! rect-intersection math duplicated per container (there is no GPU scissor).
   8 //!
   9 //! This module is the foundation for collapsing those into **one** ordered pass: a paint walk
  10 //! emits primitives into a single [`DisplayList`] through a [`PaintCtx`] that carries a **clip
  11 //! stack** (each pushed clip is intersected with the current one, so a primitive records the exact
  12 //! scissor rect it should be drawn under) and a **translate stack** (local coordinates compose to
  13 //! absolute — the seam Phase 4 animation slides/scales through). The backend then tessellates the
  14 //! one ordered list, using the recorded clip as a GPU `set_scissor_rect`.
  15 //!
  16 //! This first cut is pure data + bookkeeping, fully unit-tested without a GPU. Wiring the widget
  17 //! tree's paint into it, and routing the backend through the result, are the runtime-gated
  18 //! follow-ups.
  19 
  20 use crate::scene::layout::Rect;
  21 use crate::scene::material::{Finish, Material, PlateRole};
  22 
  23 /// End-cap style for a [`Prim::Vector`], mirroring the toolkit's line caps.
  24 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
  25 pub enum Cap {
  26     Flat,
  27     Round,
  28     Arrow,
  29 }
  30 
  31 /// A single paint primitive in logical pixels (absolute coordinates once emitted). These mirror
  32 /// the toolkit's existing tessellators so a `DisplayList` maps directly onto them at draw time.
  33 /// Per-corner radii `(top_left, top_right, bottom_right, bottom_left)`, matching the toolkit's
  34 /// `CornerRadii` order.
  35 pub type Radii = (f32, f32, f32, f32);
  36 
  37 /// RFC Phase 7b: the ONE description of a lit base surface — a window's root
  38 /// plate or a nested pane plate — distinguished only by ROLE data, never by
  39 /// type. A window root is a plate whose four corners are all window corners;
  40 /// detaching a pane into its own window is a role flip, nothing more.
  41 ///
  42 /// The material's tint always carries POSITIVE alpha; the frost encoding is
  43 /// applied by [`Self::fill`] per the role (see the Phase 7b blur-regime note
  44 /// in `docs/rfc-core-rebuild.md`): a root plate stays positive-alpha (the
  45 /// COMPOSITOR frosts behind the window), a nested plate whose material is
  46 /// [`Frost::Frosted`] encodes the in-app frost pass's negative-alpha sentinel.
  47 #[derive(Debug, Clone, Copy, PartialEq)]
  48 pub struct PlateSpec {
  49     pub rect: Rect,
  50     /// What the plate is made of: tint, frost and finish
  51     /// (`docs/rfc-material.md`). `Material::root()` / `Material::pane()` are
  52     /// the rung defaults; `Material::opaque(c)` an app's own colour.
  53     pub material: Material,
  54     /// Which corners lie ON the window silhouette (TL, TR, BR, BL).
  55     pub window_corners: (bool, bool, bool, bool),
  56     /// Transition-band width of the rolled perimeter. Negative = the fill-less
  57     /// roll-overlay sentinel (see [`PaintCtx::plate`]).
  58     pub depth: f32,
  59 }
  60 
  61 impl PlateSpec {
  62     /// THE standard root plate of a `width` x `height` window — the base
  63     /// surface every cce app stands its panes and controls on: the whole
  64     /// window, the root rung's material ([`Material::root`], which is the
  65     /// DE's `style.surface.plate.root.color` at its configured opacity
  66     /// unless a `material=` is bound), all four corners on the silhouette,
  67     /// and the perimeter rolled over [`crate::layout::bevel_width`].
  68     ///
  69     /// This is the spec every app used to hand-copy as an eight-line block
  70     /// (page-low colour, opacity override, four window corners, the DE roll)
  71     /// — the copies are gone, and a window whose base is anything else is
  72     /// off the standard on purpose, which its code should say. Emit it with
  73     /// [`PaintCtx::root_plate`]; deviate with [`Self::with_material`] /
  74     /// [`Self::with_depth`] (cce-system-interface's own tint, an overlay's
  75     /// shallower roll).
  76     pub fn window(width: f32, height: f32) -> Self {
  77         Self::root_at(Rect { x: 0.0, y: 0.0, width, height })
  78     }
  79 
  80     /// [`Self::window`] for a root plate that is not the whole surface — a
  81     /// layer-shell overlay drawing the window silhouette itself inside a
  82     /// larger transparent surface (cce-cloud). Same material, corners and
  83     /// roll; `rect` is where the "window" is.
  84     pub fn root_at(rect: Rect) -> Self {
  85         Self {
  86             rect,
  87             material: Material::root(),
  88             window_corners: (true, true, true, true),
  89             depth: crate::layout::bevel_width(),
  90         }
  91     }
  92 
  93     /// This plate made of `material` instead of its rung's default.
  94     pub fn with_material(mut self, material: Material) -> Self {
  95         self.material = material;
  96         self
  97     }
  98 
  99     /// This plate with a `depth` roll instead of the DE's `bevel_width`.
 100     pub fn with_depth(mut self, depth: f32) -> Self {
 101         self.depth = depth;
 102         self
 103     }
 104 
 105     /// All four corners on the silhouette: this plate IS the window's base
 106     /// surface.
 107     pub fn is_root(&self) -> bool {
 108         let (tl, tr, br, bl) = self.window_corners;
 109         tl && tr && br && bl
 110     }
 111 
 112     /// Which of `rect`'s corners lie on a `win_w` x `win_h` window's
 113     /// silhouette (edge tolerance 1.5px) — the designer's `pane_plate_radii`
 114     /// derivation, toolkit-side.
 115     pub fn window_corner_flags(rect: Rect, win_w: f32, win_h: f32) -> (bool, bool, bool, bool) {
 116         let e = 1.5;
 117         let left = rect.x <= e;
 118         let top = rect.y <= e;
 119         let right = rect.x + rect.width >= win_w - e;
 120         let bottom = rect.y + rect.height >= win_h - e;
 121         (top && left, top && right, bottom && right, bottom && left)
 122     }
 123 
 124     /// Per-corner radii for `flags`: a window corner wears the SHARED
 125     /// silhouette curve (`window_corner_radius * corner_span_factor` — the
 126     /// compositor clips the window and the desktop grid draws its cells from
 127     /// the same value, so window-corner arcs must follow it, never a per-app
 128     /// plate override); an interior corner wears the nominal
 129     /// `plate_corner_radius`.
 130     pub fn radii_for(flags: (bool, bool, bool, bool)) -> Radii {
 131         let nominal = crate::layout::plate_corner_radius();
 132         let window_r = crate::layout::window_silhouette_radius();
 133         let (tl, tr, br, bl) = flags;
 134         let pick = |on: bool| if on { window_r } else { nominal };
 135         (pick(tl), pick(tr), pick(br), pick(bl))
 136     }
 137 
 138     /// [`Self::radii_for`] over this spec's flags.
 139     pub fn radii(&self) -> Radii {
 140         Self::radii_for(self.window_corners)
 141     }
 142 
 143     /// This plate detached into its own window (RFC Phase 7c): every corner
 144     /// becomes a window corner, and with the role the radii snap to the
 145     /// silhouette curve and [`Self::fill`] flips frost regimes (the
 146     /// compositor's blur-behind takes over from the in-app sentinel). The
 147     /// reverse — reattaching — is the host assigning its computed
 148     /// `window_corner_flags` back.
 149     pub fn detached(mut self) -> Self {
 150         self.window_corners = (true, true, true, true);
 151         self
 152     }
 153 
 154     /// The frost regime this plate is under: [`PlateRole::Root`] when it IS
 155     /// the window's base surface, [`PlateRole::Nested`] otherwise.
 156     pub fn role(&self) -> PlateRole {
 157         if self.is_root() { PlateRole::Root } else { PlateRole::Nested }
 158     }
 159 
 160     /// The fill with the role-correct frost encoding: root → alpha forced
 161     /// non-negative (the compositor's frost, not ours), nested + frosted →
 162     /// the in-app frost pass's negative-alpha sentinel. The rule itself is
 163     /// [`Material::fill_tint`], the one place a negative alpha is written.
 164     pub fn fill(&self) -> [f32; 4] {
 165         self.material.fill(self.role())
 166     }
 167 }
 168 
 169 /// The **relief primitives** are the members of this enum that describe a lit
 170 /// surface rather than a flat fill: [`Prim::Bevel`], [`Prim::Plate`],
 171 /// [`Prim::Recess`], [`Prim::Boss`], [`Prim::Ridge`], [`Prim::ConcaveFillet`],
 172 /// [`Prim::Groove`], [`Prim::Lattice`], [`Prim::CarveUnion`] and [`Prim::Sphere`]. They share one lighting model — the
 173 /// DE's light vector, roll width and profile, per-pixel through shader2d's
 174 /// SDF branch (see `crate::layout::bevel_shader`) — and split in two:
 175 ///
 176 /// - **plates** carry their own fill: `Bevel`, `Plate`. Shader mode 1.
 177 /// - **carves** emit shading ONLY, no fill, over whatever is already painted
 178 ///   beneath: `Recess`, `Boss`, `Ridge`, `ConcaveFillet`, `Groove`, `Lattice`,
 179 ///   `CarveUnion`. Modes 2-4, 6-8, 13 and 14. (`Sphere`, mode 5, is neither —
 180 ///   a lit ball under the same model.)
 181 ///
 182 /// That split is load-bearing for flat-path hosts, which need one list for the
 183 /// faces and another for the edges drawn over them (cce-files' `rects` vs
 184 /// `reliefs`).
 185 ///
 186 /// How a control plate sits on the surface beneath it — see "Plates, wells
 187 /// and seams" in `CLAUDE.md`.
 188 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 189 pub enum PlateStance {
 190     /// Floats above the surface: a [`Prim::Bevel`] when it has a face of its
 191     /// own, an edges-only [`Prim::Boss`] carved inside its footprint when the
 192     /// face is transparent (the surface below shows through as the face).
 193     Raised,
 194     /// Level with the surface inside a groove ring: a [`Prim::Trough`] carved
 195     /// inside its footprint, with the face as a flat fill when it has one
 196     /// ([`PaintCtx::inset_plate`]).
 197     Flush,
 198     /// No relief at all — the face alone, filling the footprint as a flat
 199     /// rounded rect. This is the PANE rung's material brought down to the
 200     /// control rung, and it exists because the other two stances cannot give
 201     /// a control two things a pane has:
 202     ///
 203     /// - **Its silhouette IS its rect.** `Raised` and `Flush` both carve
 204     ///   inside the footprint, so their visible edge sits half the carve depth
 205     ///   in and a control laid out on the same numbers as a pane does not line
 206     ///   up with it. Nothing is inset here, so it does.
 207     /// - **It can be frosted.** The blur-behind sentinel (a negative alpha)
 208     ///   only reaches quads, and the relief stances lay their face through
 209     ///   `Border`/`Trough` strokes. This one fills with a quad, so a control
 210     ///   can be made of the same frosted material as the pane behind it.
 211     ///
 212     /// The cost is that a flat fill carries ONE radius, not four: the
 213     /// per-corner silhouette a nested relief control computes (Dropdown's
 214     /// concentric corner adjustment) has no equivalent here, and `radii.0` is
 215     /// used for all four corners. A focus `tint` is drawn as a ring, since
 216     /// there is no rim to light.
 217     Flat,
 218 }
 219 
 220 /// A control plate: the thing you press, at the control rung of the plate
 221 /// ladder. One description for every control face — Button, Dropdown,
 222 /// FontSelector, Breadcrumb, a ButtonStrip's selected plateau — so their
 223 /// carve-inside, radius, depth and transparent-face rules cannot drift.
 224 /// Painted by [`PaintCtx::control_plate`]. The root and pane rungs of the
 225 /// ladder are [`PlateSpec`]; this is the same idea one rung down.
 226 ///
 227 /// `rect` is the plate's footprint, the OUTER edge of its silhouette; the
 228 /// carve is taken inside it ([`crate::layout::carve_inside`]), so the gap
 229 /// beside the plate is the gap. `radii` is the silhouette, per corner (a
 230 /// Dropdown nested concentrically in a frame corner adjusts each). `face`
 231 /// is the plate's own material; `None` means the surface below IS the face
 232 /// (edges only), and a frosted material is a real face — the frost carried
 233 /// where the stance can (`Flat`; see [`PlateStance`]). `depth` is the
 234 /// relief's wall width — [`ControlPlate::control`] takes the DE relief width
 235 /// capped at a fifth of the height.
 236 #[derive(Debug, Clone, Copy, PartialEq)]
 237 pub struct ControlPlate {
 238     pub rect: Rect,
 239     pub radii: Radii,
 240     pub stance: PlateStance,
 241     pub face: Option<Material>,
 242     pub depth: f32,
 243     /// The rim lit in this colour: the keyboard-focus ring, drawn on the
 244     /// plate's own silhouette rather than as extra geometry. `None` unlit.
 245     pub tint: Option<[f32; 3]>,
 246 }
 247 
 248 impl ControlPlate {
 249     /// A control plate at `rect` with a uniform corner `radius`: depth from
 250     /// the DE relief width, capped at a fifth of the plate's height.
 251     pub fn control(rect: Rect, radius: f32, stance: PlateStance, face: Option<Material>) -> Self {
 252         let depth = crate::layout::bevel_width().min(rect.height * 0.2);
 253         Self { rect, radii: (radius, radius, radius, radius), stance, face, depth, tint: None }
 254     }
 255 
 256     /// Light the rim — the focus ring on the plate's silhouette. Pass the
 257     /// highlight colour while the control holds keyboard focus, `None` otherwise.
 258     pub fn with_tint(mut self, tint: Option<[f32; 3]>) -> Self {
 259         self.tint = tint;
 260         self
 261     }
 262 
 263     /// The DE's focus-ring colour for a plate rim: the highlight accent, the
 264     /// same the wells light their rims with while editing.
 265     pub fn focus_tint() -> [f32; 3] {
 266         let c = crate::color::highlight_primary_color();
 267         [c[0], c[1], c[2]]
 268     }
 269 
 270     /// Per-corner silhouette (a concentric corner-frame adjustment).
 271     pub fn with_radii(mut self, radii: Radii) -> Self {
 272         self.radii = radii;
 273         self
 274     }
 275 
 276     /// An explicit wall width — a plate that shares its depth with the well
 277     /// it stands in, or one capped by its short side rather than its height.
 278     pub fn with_depth(mut self, depth: f32) -> Self {
 279         self.depth = depth;
 280         self
 281     }
 282 
 283     /// The face a stance draws: `Some` only for a material with a visible
 284     /// tint — a transparent one is the surface below showing through, the
 285     /// same as `None`.
 286     pub fn faced(&self) -> Option<&Material> {
 287         self.face.as_ref().filter(|m| m.tint[3] > 0.001)
 288     }
 289 
 290     /// The face as the encoded fill the flat-path bridges consume
 291     /// (`Button::inset_face`, cce-system-interface's `ControlCarve`):
 292     /// transparent for no face, else the material's nested fill.
 293     pub fn face_fill(&self) -> [f32; 4] {
 294         self.face.map_or([0.0; 4], |m| m.fill(PlateRole::Nested))
 295     }
 296 }
 297 
 298 /// Call the family **relief primitives**, not "bevel primitives": `Bevel` is one
 299 /// specific member — a filled rounded rect plus a lit roll on its lip — and a
 300 /// groove, a fillet or a sphere is not a bevel in any sense. "Relief" is also
 301 /// what the rest of the stack already says: `layout::control_relief` gates the
 302 /// whole family, and the config node is `relief`. The name **bevel** is reserved
 303 /// for two things: the `Bevel` prim, and the shared *edge treatment* every
 304 /// relief primitive is shaded with (`bevel_width`, `bevel_depth`,
 305 /// `bevel_shader`, `bevel_profile` — the lit roll, not the shape).
 306 /// Shape and material knobs for [`Prim::Droplet`]. Fractions are of the
 307 /// droplet rect's height unless said otherwise, so a spec is resolution- and
 308 /// module-size-independent; the tessellator resolves and clamps them against
 309 /// the concrete rect.
 310 #[derive(Clone, Copy, Debug, PartialEq)]
 311 pub struct DropletSpec {
 312     /// How far the sheet's bottom lifts above the rect bottom (the waist the
 313     /// sides pull up into), fraction of height. 0 = no waist (a capsule).
 314     pub sag: f32,
 315     /// Belly capsule radius, fraction of height. **≤ 0 disables the belly**:
 316     /// the drop is the sheet alone — with `attach` and `sheet_r` rounding its
 317     /// top and bottom this is the oval dewdrop, and the default.
 318     pub belly: f32,
 319     /// Belly half-width, fraction of the half-width left after the belly
 320     /// radius (1 = the belly spans the whole bottom).
 321     pub belly_w: f32,
 322     /// Smooth-union blend distance, fraction of height — bigger = softer neck
 323     /// between sheet and belly.
 324     pub blend: f32,
 325     /// Sheet bottom-corner radius, fraction of height.
 326     pub sheet_r: f32,
 327     /// Sheet TOP-corner radius (the meniscus taper at the attach line),
 328     /// fraction of height. 0 = the sides meet the attach edge square (the
 329     /// clinging-pool look); larger values narrow the contact span so the
 330     /// silhouette curves into the edge like a dewdrop. When `attach + sheet_r`
 331     /// exceeds the sheet height the pair scales down proportionally, so 0.5 +
 332     /// 0.5 is the fully continuous egg curve with no straight side segment.
 333     pub attach: f32,
 334     /// Tint opacity at the deep interior relative to the color's own alpha;
 335     /// the rim falls toward `clarity` × that (thin water is clearer). 1 = flat.
 336     pub clarity: f32,
 337     /// Dome slope amplitude: scales the surface tilt the shading sees.
 338     pub dome: f32,
 339     /// Shaded band width (the dome's curved skirt), fraction of height.
 340     pub band: f32,
 341     /// Specular (gleam) strength — replaces the DE material's slot.
 342     pub gleam: f32,
 343     /// Wet-surface shininess exponent.
 344     pub shine: f32,
 345     /// Fresnel rim crest amplitude (the glass-edge brightening).
 346     pub rim: f32,
 347     /// Bottom bow: the drop's bottom boundary becomes ONE continuous circular
 348     /// arc — lowest at center, rising by `bow` (fraction of height) at the
 349     /// drop's side extents. The arc's radius is derived per drop from that
 350     /// fixed edge rise, so a wide drop gets a huge radius and the curvature
 351     /// stays subtle at the middle while a narrow drop curves visibly. 0
 352     /// disables it (flat bottom run between the corner arcs).
 353     pub bow: f32,
 354     /// Corner-curve exponent for the silhouette (and the dome profile riding
 355     /// it): 2 = circular arcs, above 2 = superellipse quadrants whose
 356     /// curvature ramps to ZERO at both ends of each arc — every junction
 357     /// (attach↔side, side↔bottom, curve↔flat top) becomes curvature-
 358     /// continuous, so unequal attach/sheet_r radii read as ONE flowing curve
 359     /// instead of two arcs meeting, and the contact eases out of the flat
 360     /// top like a meniscus. Clamped to [2, 6].
 361     pub curve: f32,
 362     /// Extra tint density at the drop's deep interior: the body opacity ramps
 363     /// from `clarity` at the rim up to `1 + core` (× the color's own alpha,
 364     /// clamped to opaque) inside — the water reads thickest in the middle,
 365     /// which is also where a module's text sits, so glyphs get a calmer
 366     /// field without giving up the watery rim. 0 = the original flat
 367     /// interior falloff.
 368     pub core: f32,
 369     /// Refraction strength in logical px — how far the COMPOSITOR's droplet
 370     /// backdrop pass bends the image behind the drop at the rim. Client-side
 371     /// rendering ignores it (a Wayland client cannot see behind its own
 372     /// window); the compositor reads the same spec and drives its scenefx
 373     /// droplet node with it. 0 disables the backdrop pass.
 374     pub refr: f32,
 375     /// Strength (0-1) of the compositor pass's inverted lens ghost — the
 376     /// faint upside-down image of the scene a real hanging drop shows in its
 377     /// belly. Client-side ignored, like `refr`.
 378     pub ghost: f32,
 379     /// Contact-shadow strength (0-1): a soft dark falloff cast below the
 380     /// drop's lower arc, outside the silhouette — the volume cue of a bead
 381     /// sitting proud of the surface. The host must leave room beneath the
 382     /// drop box for it (the status bar insets the box by
 383     /// [`DropletSpec::shadow_gap`]). 0 disables it.
 384     pub shadow: f32,
 385 }
 386 
 387 impl DropletSpec {
 388     /// Parse the DE's droplet spec string — whitespace-separated `k=v` pairs
 389     /// onto the defaults (an empty string is all defaults). Unknown keys and
 390     /// non-numeric values `log::warn!` and are skipped, so a typo surfaces in
 391     /// the log instead of silently reverting one knob. Shared by the status
 392     /// bar (which draws the drop) and the compositor (whose scenefx droplet
 393     /// node refracts the backdrop behind it) so the two sides can never
 394     /// disagree about a spec's meaning.
 395     pub fn parse(raw: &str) -> Self {
 396         let mut spec = Self::default();
 397         for tok in raw.split_whitespace() {
 398             let Some((key, val)) = tok.split_once('=') else {
 399                 log::warn!("droplet spec: token '{}' is not k=v — skipped", tok);
 400                 continue;
 401             };
 402             let Ok(v) = val.parse::<f32>() else {
 403                 log::warn!("droplet spec: '{}' has a non-numeric value — skipped", tok);
 404                 continue;
 405             };
 406             match key {
 407                 "sag" => spec.sag = v,
 408                 "belly" => spec.belly = v,
 409                 "belly_w" => spec.belly_w = v,
 410                 "blend" => spec.blend = v,
 411                 "sheet_r" => spec.sheet_r = v,
 412                 "attach" => spec.attach = v,
 413                 "clarity" => spec.clarity = v,
 414                 "dome" => spec.dome = v,
 415                 "band" => spec.band = v,
 416                 "gleam" => spec.gleam = v,
 417                 "shine" => spec.shine = v,
 418                 "rim" => spec.rim = v,
 419                 "bow" => spec.bow = v,
 420                 "curve" => spec.curve = v,
 421                 "core" => spec.core = v,
 422                 "refr" => spec.refr = v,
 423                 "ghost" => spec.ghost = v,
 424                 "shadow" => spec.shadow = v,
 425                 _ => log::warn!("droplet spec: unknown key '{}' — skipped", key),
 426             }
 427         }
 428         spec
 429     }
 430 
 431     /// Resolve the silhouette's height-fraction knobs against a concrete rect
 432     /// (logical px) with the SAME clamps the tessellator applies: returns
 433     /// `(sheet_r, attach_r, bow_rise)` in logical px, the attach/sheet pair
 434     /// proportionally scaled down when it overfills the height. The
 435     /// compositor's droplet backdrop node uses this so its refracting
 436     /// silhouette and the client-drawn drop are the same shape.
 437     pub fn resolve_silhouette(&self, w: f32, h: f32) -> (f32, f32, f32) {
 438         let hx = w * 0.5;
 439         let hy = h * 0.5;
 440         let mut sr = (self.sheet_r.clamp(0.0, 1.0) * h).min(hx);
 441         let mut ar = (self.attach.clamp(0.0, 1.0) * h).min(hx);
 442         let sheet_h = 2.0 * hy;
 443         if sr + ar > sheet_h && sr + ar > 0.0 {
 444             let f = sheet_h / (sr + ar);
 445             sr *= f;
 446             ar *= f;
 447         }
 448         let bow = (self.bow.clamp(0.0, 0.5) * h).min(hy * 0.9);
 449         (sr, ar, bow)
 450     }
 451 
 452     /// Vertical room (logical px) a host should leave BELOW the drop box for
 453     /// the contact shadow, given the full slot height. One place, so the
 454     /// bar's reserved gap and the shader's falloff reach stay proportioned.
 455     pub fn shadow_gap(&self, slot_h: f32) -> f32 {
 456         if self.shadow > 0.0 {
 457             (0.16 * slot_h).ceil()
 458         } else {
 459             0.0
 460         }
 461     }
 462 }
 463 
 464 impl DropletSpec {
 465     /// The drop's finish: its own gleam, shine and rim in the specular,
 466     /// shininess and curvature slots of a [`Finish`] (a drop is wetter than
 467     /// the DE's plates), the shading strength the DE's. The material a
 468     /// droplet is emitted with carries this — `Material::from_fill(c)
 469     /// .with_finish(spec.finish())` — and the tessellator reads it from
 470     /// there like any plate's, instead of packing the slots by hand.
 471     pub fn finish(&self) -> Finish {
 472         Finish { spec: self.gleam, shininess: self.shine, curvature: self.rim, ..Finish::from_style() }
 473     }
 474 }
 475 
 476 impl Default for DropletSpec {
 477     fn default() -> Self {
 478         // The oval dewdrop: no belly, no sag — one continuous curve from a
 479         // tapered attach line to a fully round bottom. attach + sheet_r fill
 480         // the whole height (no straight side segment), biased bottom-heavy,
 481         // and the superellipse curve exponent keeps the unequal pair
 482         // curvature-continuous. The pendant-pool look is reachable by
 483         // setting `belly` > 0 (and usually some `sag`).
 484         Self {
 485             sag: 0.0,
 486             belly: 0.0,
 487             belly_w: 0.5,
 488             blend: 0.35,
 489             sheet_r: 0.58,
 490             attach: 0.42,
 491             clarity: 0.5,
 492             dome: 0.9,
 493             band: 0.9,
 494             gleam: 1.4,
 495             shine: 32.0,
 496             rim: 0.5,
 497             bow: 0.12,
 498             curve: 2.6,
 499             core: 0.35,
 500             refr: 0.0,
 501             ghost: 0.0,
 502             shadow: 0.35,
 503         }
 504     }
 505 }
 506 
 507 #[derive(Clone, Debug, PartialEq)]
 508 pub enum Prim {
 509     Quad { rect: Rect, color: [f32; 4] },
 510     RoundedRect { rect: Rect, radius: f32, corners: (bool, bool, bool, bool), color: [f32; 4] },
 511     /// A rounded fill plus a solid border stroke — a widget's own "plate" (mirrors
 512     /// `push_widget_vertices`' non-bevel branch: rounded bg + `push_plate_solid_border_vertices`).
 513     Border { rect: Rect, radii: Radii, fill: [f32; 4], border: [f32; 4], thickness: f32 },
 514     /// A beveled plate: a rounded fill at full size plus a light/shadow overlay lip
 515     /// (mirrors `push_widget_vertices`' bevel branch). `tint` multiplies the lit
 516     /// roll's specular color — neutral white normally; a host sets it to a
 517     /// highlight color to mark the plate (the focused-pane treatment) without a
 518     /// separate border ring. Shader-plates path only; the legacy banded
 519     /// tessellation ignores it.
 520     Bevel { rect: Rect, radii: Radii, material: Material, depth: f32, tint: [f32; 3] },
 521     /// A recess carved into whatever is already painted underneath — the inverse of
 522     /// `Bevel`. Emits ONLY the shaded edges, never a fill, so the surface below shows
 523     /// through the middle: a relief cut into the root plate rather than a plate laid on
 524     /// top of it. The light vector is negated relative to `Bevel`, so the edges facing
 525     /// `light_source_position` fall into shadow and the far edges catch the light —
 526     /// which is what reads as "lower" instead of "raised".
 527     ///
 528     /// The shading is a translucent light/shadow overlay, so the carve needs no knowledge
 529     /// of what it carves: fills, gradients, and translucency below all show through
 530     /// modulated rather than repainted.
 531     /// `edges` is (top, right, bottom, left): which walls of the carve actually exist.
 532     /// A region flush with the plate's own edge is a step, not a trough — see
 533     /// `push_bevel_edge_vertices_banded`.
 534     /// `tint` colors the wall's lit rim — the same focused-pane treatment as
 535     /// [`Prim::Bevel`]'s tint, for carved wells instead of raised plates. A tinted
 536     /// recess never groups into a host plate's CSG features (a feature carries no
 537     /// color), so it always renders as the free-carve overlay. Shader-plates path
 538     /// only; the legacy banded tessellation ignores it.
 539     Recess { rect: Rect, radii: Radii, depth: f32, edges: (bool, bool, bool, bool), tint: Option<[f32; 3]> },
 540     /// The inverse of [`Prim::Recess`]: a plateau RAISED out of the surface below.
 541     /// Like `Recess` it emits only the shaded edges, never a fill — the face is the
 542     /// untouched surface underneath — so a region outlined by raised rolled bumps
 543     /// keeps the root plate's own color and translucency. Same wall semantics as
 544     /// `Recess` (`edges` = top/right/bottom/left); the lighting is the raised sign,
 545     /// so the edges facing `light_source_position` catch the light. `tint` colors
 546     /// the lit rim like [`Prim::Recess`]'s — the focused-pane treatment for a
 547     /// rim-only pane (a fill-less surface can't carry [`Prim::Bevel`]'s tint).
 548     /// Like a tinted recess it never groups into a host plate's CSG features.
 549     Boss { rect: Rect, radii: Radii, depth: f32, edges: (bool, bool, bool, bool), tint: Option<[f32; 3]> },
 550     /// A raised RIM riding the rect's boundary: a bump profile straddling the
 551     /// outline (span ±depth/2), rising from the surrounding surface to a crest on
 552     /// the boundary and falling back to the same level inside — an elevated border
 553     /// around a channel, both faces at the underlying surface's own level. One
 554     /// primitive, ONE lighting evaluation per pixel: building the same shape from
 555     /// a Boss plus an inset Recess stacks two shading passes (double specular /
 556     /// shoulder terms at the crest) and reads far hotter than a plate edge.
 557     Ridge { rect: Rect, radii: Radii, depth: f32, edges: (bool, bool, bool, bool) },
 558     /// The sunken twin of [`Prim::Ridge`]: a VALLEY riding the rect's boundary —
 559     /// a bump profile straddling the outline (span ±depth/2), falling from the
 560     /// surrounding surface to a trough on the boundary and rising back to the
 561     /// same level inside, so both faces sit at the underlying surface's own
 562     /// level. This is the seam a flush inset control leaves ([`PaintCtx::inset_plate`]).
 563     ///
 564     /// Same reason to exist as `Ridge`, measured: building this from a `Recess`
 565     /// on an outset rect plus a `Boss` on the rect (what `inset_plate` used to
 566     /// emit) stacks two independent shading passes. At depth 4.8 that read as a
 567     /// band 15px wide instead of 8 with THREE lobes — bright, dark, brighter —
 568     /// because the recess ring's own lit rim lands ~depth outside the control
 569     /// instead of merging into one wall, and the highlight peaked 22% hotter
 570     /// than a single evaluation of the same depth. It looked like two concentric
 571     /// rings, which is what it was.
 572     ///
 573     /// `edges` and the host-box fade behave exactly as [`Prim::Recess`]'s.
 574     /// SDF path only; the legacy banded tessellation approximates it with the
 575     /// old two-step stack (like `Ridge`, which approximates itself there).
 576     ///
 577     /// `tint` lights the rim like [`Prim::Recess`]'s — the focus treatment of a
 578     /// flush control plate (`PaintCtx::control_plate`).
 579     Trough { rect: Rect, radii: Radii, depth: f32, edges: (bool, bool, bool, bool), tint: Option<[f32; 3]> },
 580     /// The window's glass slab: a rounded fill plus a rolled, lit edge around its whole
 581     /// perimeter, drawn at full size. Distinct from `Bevel`, which insets its fill by
 582     /// `depth` — a plate must fill the window exactly, or the compositor's rounded window
 583     /// corners would show a gap. `depth` is the width of the roll-off in px, not a color
 584     /// offset (the shading amplitude is the DE-wide `bevel_depth`).
 585     ///
 586     /// `shape` overrides the DE-wide corner exponent (`layout::corner_shape`)
 587     /// for this one plate — `Some(2.0)` is circular arcs, so a plate whose
 588     /// radii reach its half-extent is a true circle regardless of the
 589     /// squircle the rest of the DE wears. `None` follows the DE.
 590     Plate { rect: Rect, radii: Radii, material: Material, depth: f32, shape: Option<f32> },
 591     Arc { cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4] },
 592     /// A ring band with radial color interpolation — inner rim → crest
 593     /// (centerline) → outer rim — for rounded rim bevels (the Ramp's key
 594     /// rings). `radius` is the stroke's outer edge, like `Arc`.
 595     ArcShaded { cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, inner: [f32; 4], crest: [f32; 4], outer: [f32; 4] },
 596     Vector { x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, color: [f32; 4], cap: Cap },
 597     Circle { cx: f32, cy: f32, radius: f32, color: [f32; 4] },
 598     /// A feathered aura around (and over) a rounded rect: the interior fills
 599     /// at the color's full alpha, and outside the boundary the alpha falls
 600     /// off smoothly to zero across `reach` px. Tessellated as concentric
 601     /// per-vertex-alpha rings the GPU interpolates, so the gradient is
 602     /// per-pixel smooth — no stacked-layer banding. Highlights and soft
 603     /// focus auras (the designer's drop-target glow) are the intended use;
 604     /// no relief shading, no light involvement.
 605     Glow { rect: Rect, radius: f32, reach: f32, color: [f32; 4] },
 606     /// A `Circle` lit as a ball: the disc is shaded per pixel as a hemisphere
 607     /// under the DE's plate light (same ambient/diffuse/specular model), so it
 608     /// reads as a sphere sitting on the surface — the slider thumb's look. The
 609     /// color is the sphere's face color exactly at the lit center, like a
 610     /// plate's face keeps the app's color. Falls back to a flat circle on the
 611     /// legacy (`bevel_shader 0`) path.
 612     Sphere { cx: f32, cy: f32, radius: f32, material: Material },
 613     /// A hanging water droplet clinging to the TOP edge of `rect`, lit per pixel
 614     /// by shader mode 10: the silhouette is a smooth union of a film "sheet"
 615     /// attached to the top edge (square top corners — the attach line) and a
 616     /// belly capsule resting on the rect's bottom, blended metaball-style so a
 617     /// waist forms where the sides pull up. Shaded as a glass dome under the
 618     /// DE's plate light — same ambient/diffuse and decoupled specular as the
 619     /// plates, plus a fresnel rim crest and a thin-edge clarity falloff (tint
 620     /// opacity drops toward the silhouette, so the frosted backdrop shows
 621     /// through clearer at the rim, which is what reads as water rather than
 622     /// plastic). Shape knobs in [`DropletSpec`]. On the legacy (`bevel_shader
 623     /// 0`) path it degrades to the flat hanging capsule — square top, round
 624     /// bottom — rather than vanishing.
 625     Droplet { rect: Rect, material: Material, spec: DropletSpec },
 626     /// The same silhouette as [`Prim::Droplet`] under the same [`DropletSpec`],
 627     /// filled FLAT and feathered inward: opaque through the interior, fading
 628     /// to nothing over `feather` px as it approaches the drop's edge. A
 629     /// vignette shaped exactly like the drop, for grounding text drawn on top
 630     /// of one — not a second lit body, so it carries no dome, rim, gleam or
 631     /// contact shadow.
 632     ///
 633     /// It shares the droplet's shader path rather than approximating the
 634     /// outline with a rounded rect, so the two can never disagree about where
 635     /// the drop's edge is. On the legacy (`bevel_shader 0`) path it degrades
 636     /// to the same flat rounded-rect outline `Prim::Droplet` falls back to.
 637     DropletScrim { rect: Rect, material: Material, spec: DropletSpec, feather: f32 },
 638     /// A concave inside-corner fillet for composed carves: a quarter-arc wall
 639     /// whose centre `(cx, cy)` sits out in the corner's pocket, shaded with the
 640     /// same step profile as a `Recess`/`Boss` wall (`raised` flips the sign).
 641     /// `start` is the wedge's start angle (quarter span, hard-cut at the
 642     /// tangent lines — the neighboring straight walls continue the profile
 643     /// exactly there). Box radii can only round convex corners; this is the
 644     /// missing concave piece. SDF path only (no legacy fallback).
 645     ConcaveFillet { cx: f32, cy: f32, radius: f32, depth: f32, start: f32, raised: bool },
 646     /// An engraved line: a groove of half-width `width / 2` running along the
 647     /// segment `a`–`b`, cut into whatever is painted beneath. Like [`Prim::Recess`]
 648     /// it emits only shading, never a fill — but its shape is a SLAB (a band about
 649     /// an arbitrary line) rather than a box, which is what lets it run at an angle.
 650     /// A box SDF can only carve axis-aligned walls; this is the diagonal case.
 651     ///
 652     /// Both walls come from ONE profile evaluation on `|distance to the line|`, so
 653     /// the groove carries a single specular/shoulder term — the same reason
 654     /// [`Prim::Ridge`] exists instead of stacking a boss on a recess.
 655     /// `width` 0 makes the two walls meet in a V.
 656     ///
 657     /// `depth` is the transition width in px (the wall's run), matching
 658     /// [`Prim::Recess`]. `host` is the surface the groove is engraved into: the
 659     /// shading fades out across that box's perimeter roll, so a seam cut across a
 660     /// plate dies into the plate's own rolled edge instead of ending on a hard line.
 661     /// SDF path only — the legacy banded tessellation draws nothing (like `Ridge`).
 662     Groove { a: (f32, f32), b: (f32, f32), width: f32, depth: f32, host: Rect },
 663     /// A periodic field of identical rounded-box wells — every cell of a grid
 664     /// carved into whatever is painted beneath, as ONE surface. The wells
 665     /// repeat every `period` (x, y) with one cell centred at `origin`, each
 666     /// `cell` (w, h) big with `radius` corners; the wall runs from the cell
 667     /// edge OUTWARD over `depth` px (floor at the edge, plateau one run out),
 668     /// so a rail between two cells carries one wall from each side and the
 669     /// rail face is whatever the runs leave. Shading lands only inside `rect`.
 670     /// The wall's outer edge is MITRED, not offset: it is the cell grown by
 671     /// the run at the same `radius`, so a crossing keeps the cell's corner
 672     /// rounding instead of sweeping at `radius + depth`, and the four walls
 673     /// meet on the diagonals.
 674     ///
 675     /// This exists because a lattice drawn as one [`Prim::Recess`] per cell is
 676     /// N independent overlays: where four rounded rings meet at a crossing
 677     /// their shadings stack in colour space and read as overlapping effects,
 678     /// not a junction. Here the pixel is folded into the period and the
 679     /// distance is to the NEAREST cell — the union of every well — evaluated
 680     /// once, so the rail centre lines and the diagonals at each crossing are
 681     /// true mitres, and the cost is one draw regardless of how many cells the
 682     /// surface holds (a free carve per cell also runs into the per-frame
 683     /// feature budget long before a zoomed-out grid does). SDF path only.
 684     Lattice { rect: Rect, period: (f32, f32), origin: (f32, f32), cell: (f32, f32), radius: f32, depth: f32 },
 685     /// `color`, flat, everywhere inside `rect` that is OUTSIDE a periodic
 686     /// field of rounded cells — the same field [`Prim::Lattice`] carves
 687     /// (`period`, one cell centred at `origin`, each `cell` big with `radius`
 688     /// corners), painted as grout rather than shaded. One draw for the whole
 689     /// grid, with the cells' superellipse corners exact: what a graph's grid
 690     /// lines are when the cells are the surface beneath showing through.
 691     /// SDF path only — the legacy banded tessellation draws nothing.
 692     Grout { rect: Rect, period: (f32, f32), origin: (f32, f32), cell: (f32, f32), radius: f32, color: [f32; 4] },
 693     /// A flat fill of a MATERIAL: `rect` at `radii`, no roll, no rim — the
 694     /// material's tint, frosted at the material's own recipe when it is
 695     /// frosted. What a frosted `RoundedRect` promotes to, except that the
 696     /// recipe is the material's rather than the DE default's, so a fill can
 697     /// compress harder (or softer) than the pane it sits on. Opens no carve
 698     /// host: carves emitted after it overlay it, as they overlay any flat
 699     /// geometry. An opaque material draws as a plain rounded fill.
 700     Fill { rect: Rect, radii: Radii, material: Material },
 701     /// Several rounded boxes carved (`raised` false) or raised (`raised`
 702     /// true) as ONE shape: the union of the boxes is the well, and its wall
 703     /// follows the union's outline — straddling it by ±`depth`/2 like every
 704     /// carve boundary — through a single profile evaluation per pixel. An L,
 705     /// a T, a plus, a slot with a round end: any outline boxes can compose.
 706     ///
 707     /// The alternative, one [`Prim::Recess`] per box, is N overlays that
 708     /// each shade their own full outline: where two boxes overlap, each
 709     /// draws a wall straight through the other's interior, and where their
 710     /// walls cross the shadings stack in colour space — the junction reads
 711     /// as two effects laid over each other, not one shape. Here the pixel's
 712     /// distance is to the NEAREST box (the union SDF), so a box's wall
 713     /// vanishes wherever it runs inside another, and an inside corner is a
 714     /// sharp mitre (round it with [`Prim::ConcaveFillet`] if it must be
 715     /// concave-rounded — the union has no radius there by construction).
 716     /// Outer corners are mitred like [`Prim::Lattice`]'s: the wall band runs
 717     /// between each box shrunk and grown by half the run at the box's own
 718     /// radius, so a corner keeps its radius instead of sweeping wider.
 719     ///
 720     /// The boxes ride the frame's plate-feature buffer (the same slots CSG
 721     /// carves use, 64 per frame), so a union costs one draw plus one slot
 722     /// per box. When the budget cannot hold all of a union's boxes the
 723     /// tessellator keeps as many as fit — a degraded shape rather than none —
 724     /// and says so under `CCE_PLATE_DEBUG`. SDF path only.
 725     CarveUnion { boxes: Vec<(Rect, Radii)>, depth: f32, raised: bool },
 726     /// Text in sRGB u8 (the `TextLabel` convention). `font` is a font string for
 727     /// `get_text_buffer` (family, or "family:size"); `bounds` is a logical `[l, t, r, b]` clip
 728     /// for the glyph pass (Phase 6: the backend renders these through the glyph pass when the app
 729     /// opts in via `Application::display_list_text`; the paint walk's clip additionally
 730     /// applies through the item's `clip`). `attrs` carries the optional shaping attributes
 731     /// beyond family+size (the font picker's italic/weight preview variants). `layout`, when
 732     /// `Some`, requests box layout — word-wrap at a width and horizontal/vertical alignment
 733     /// within a box (the placed-text-box case, e.g. cce-layout-interface's canvas elements);
 734     /// `None` is the ordinary single-run label. `alpha` fades the glyphs (1.0 = opaque) —
 735     /// the color stays sRGB u8, so translucent text doesn't need a color-type change.
 736     Text { text: String, x: f32, y: f32, font_size: f32, color: [u8; 3], alpha: f32, font: Option<String>, bounds: Option<[f32; 4]>, attrs: TextAttrs, layout: Option<TextLayout> },
 737     /// A user image (id from `cce_ui::vk::upload_rgba`) drawn as a quad, in
 738     /// display-list order like any other primitive. The paint walk's clip
 739     /// applies through the item's `clip` as usual.
 740     Image { image: u32, rect: Rect, alpha: f32 },
 741 }
 742 
 743 /// Horizontal alignment of laid-out (boxed) text — the toolkit-plain mirror of
 744 /// `cosmic_text::Align`, mapped at shape time.
 745 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
 746 pub enum AlignH {
 747     #[default]
 748     Left,
 749     Center,
 750     Right,
 751 }
 752 
 753 /// Vertical alignment of laid-out text within its box.
 754 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
 755 pub enum AlignV {
 756     #[default]
 757     Top,
 758     Middle,
 759     Bottom,
 760 }
 761 
 762 /// Box layout for a [`Prim::Text`]: word-wrap width (`Some` ⇒ multiline wrap; `None` ⇒ single
 763 /// run) and horizontal/vertical alignment within a box of `box_height`. All lengths are logical.
 764 /// The backend shapes an uncached buffer (`get_text_buffer_laid_out`) so the wrap/align do not
 765 /// pollute the shared single-run cache, and applies the vertical offset from the shaped height.
 766 #[derive(Clone, Copy, Debug, PartialEq)]
 767 pub struct TextLayout {
 768     pub wrap_width: Option<f32>,
 769     pub box_height: f32,
 770     pub align_h: AlignH,
 771     pub align_v: AlignV,
 772 }
 773 
 774 /// Optional shaping attributes for a [`Prim::Text`] — the subset a widget can request beyond
 775 /// family + size. `weight` is the OpenType weight (400 regular, 700 bold); `None` leaves the
 776 /// family default. Kept toolkit-plain (no cosmic-text types) like the rest of the scene layer;
 777 /// the backend maps them onto `cosmic_text::Style`/`Weight` at shape time.
 778 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
 779 pub struct TextAttrs {
 780     pub italic: bool,
 781     pub weight: Option<u16>,
 782 }
 783 
 784 /// A primitive plus the scissor rect it must be clipped to (`None` = unclipped), an
 785 /// optional circular clip `[cx, cy, r]` in logical pixels (`None` = unclipped) — the
 786 /// per-vertex circle clip the tessellators already support, for round panes (the designer's
 787 /// circular network pane) — and an optional rounded-rect clip `[cx, cy, bx, by, r]`
 788 /// (center, SDF half-extents = half-size minus radius, corner radius; logical px) so a
 789 /// plate's children cut off at its rounded corners. The clips compose: the scissor is GPU
 790 /// state, the circle rides the vertices, the rounded rect is per-draw-batch state.
 791 #[derive(Clone, Debug, PartialEq)]
 792 pub struct PaintItem {
 793     pub prim: Prim,
 794     pub clip: Option<Rect>,
 795     pub clip_circle: Option<[f32; 3]>,
 796     pub clip_rrect: Option<[f32; 5]>,
 797 }
 798 
 799 /// An ordered list of clipped primitives — the single source of truth for a frame's geometry.
 800 #[derive(Clone, Debug, Default, PartialEq)]
 801 pub struct DisplayList {
 802     pub items: Vec<PaintItem>,
 803 }
 804 
 805 impl DisplayList {
 806     pub fn new() -> Self {
 807         DisplayList { items: Vec::new() }
 808     }
 809     pub fn len(&self) -> usize {
 810         self.items.len()
 811     }
 812     pub fn is_empty(&self) -> bool {
 813         self.items.is_empty()
 814     }
 815 }
 816 
 817 /// Intersection of two rects, clamped so width/height never go negative (an empty clip is a
 818 /// zero-size rect — nothing draws under it).
 819 fn intersect(a: Rect, b: Rect) -> Rect {
 820     let x0 = a.x.max(b.x);
 821     let y0 = a.y.max(b.y);
 822     let x1 = (a.x + a.width).min(b.x + b.width);
 823     let y1 = (a.y + a.height).min(b.y + b.height);
 824     Rect { x: x0, y: y0, width: (x1 - x0).max(0.0), height: (y1 - y0).max(0.0) }
 825 }
 826 
 827 /// Accumulates a [`DisplayList`] while a paint walk pushes/pops clips and translations.
 828 ///
 829 /// Coordinates passed to the emit methods (and to [`push_clip`](PaintCtx::push_clip)) are in the
 830 /// **current** local space; the active translation is applied so everything recorded is absolute.
 831 pub struct PaintCtx {
 832     list: DisplayList,
 833     /// Each entry is the effective (already-intersected, absolute) clip at that depth.
 834     clip_stack: Vec<Rect>,
 835     /// Active circular clips; primitives record the innermost (`last`). Circles don't
 836     /// intersect analytically like rects, so nesting keeps the innermost only.
 837     clip_circle_stack: Vec<[f32; 3]>,
 838     /// Active rounded-rect clips `[cx, cy, bx, by, r]`; innermost wins, like circles.
 839     clip_rrect_stack: Vec<[f32; 5]>,
 840     /// Saved offsets for nesting; `offset` is the current cumulative translation.
 841     offset_stack: Vec<(f32, f32)>,
 842     offset: (f32, f32),
 843 }
 844 
 845 impl Default for PaintCtx {
 846     fn default() -> Self {
 847         Self::new()
 848     }
 849 }
 850 
 851 impl PaintCtx {
 852     pub fn new() -> Self {
 853         PaintCtx {
 854             list: DisplayList::new(),
 855             clip_stack: Vec::new(),
 856             clip_circle_stack: Vec::new(),
 857             clip_rrect_stack: Vec::new(),
 858             offset_stack: Vec::new(),
 859             offset: (0.0, 0.0),
 860         }
 861     }
 862 
 863     /// The scissor rect primitives are currently recorded under.
 864     pub fn current_clip(&self) -> Option<Rect> {
 865         self.clip_stack.last().copied()
 866     }
 867 
 868     /// Push a clip (in current local space); it is translated to absolute and intersected with the
 869     /// enclosing clip. Pair with [`pop_clip`](PaintCtx::pop_clip), or prefer [`clip`](PaintCtx::clip).
 870     pub fn push_clip(&mut self, rect: Rect) {
 871         let r = self.apply_offset(rect);
 872         let effective = match self.clip_stack.last() {
 873             Some(cur) => intersect(*cur, r),
 874             None => r,
 875         };
 876         self.clip_stack.push(effective);
 877     }
 878 
 879     pub fn pop_clip(&mut self) {
 880         self.clip_stack.pop();
 881     }
 882 
 883     /// Run `f` with `rect` pushed as a clip, popping it afterward.
 884     pub fn clip<R>(&mut self, rect: Rect, f: impl FnOnce(&mut Self) -> R) -> R {
 885         self.push_clip(rect);
 886         let out = f(self);
 887         self.pop_clip();
 888         out
 889     }
 890 
 891     /// Push a circular clip `[cx, cy, r]` (current local space, translated to absolute).
 892     /// Primitives emitted while it is active record it and tessellate with the per-vertex
 893     /// circle clip. Pair with [`pop_clip_circle`](PaintCtx::pop_clip_circle), or prefer
 894     /// [`clip_circle`](PaintCtx::clip_circle).
 895     pub fn push_clip_circle(&mut self, c: [f32; 3]) {
 896         self.clip_circle_stack.push([c[0] + self.offset.0, c[1] + self.offset.1, c[2]]);
 897     }
 898 
 899     pub fn pop_clip_circle(&mut self) {
 900         self.clip_circle_stack.pop();
 901     }
 902 
 903     /// Run `f` with `[cx, cy, r]` pushed as a circular clip, popping it afterward.
 904     pub fn clip_circle<R>(&mut self, c: [f32; 3], f: impl FnOnce(&mut Self) -> R) -> R {
 905         self.push_clip_circle(c);
 906         let out = f(self);
 907         self.pop_clip_circle();
 908         out
 909     }
 910 
 911     /// Push a rounded-rect clip: `rect` (current local space) with corner radius `radius`,
 912     /// so children of a rounded plate cut off at its corners. Pushes the rect as a scissor
 913     /// too — the scissor handles the straight edges (and keeps batching), the SDF trims the
 914     /// corners. A radius of zero degenerates to the plain rect clip. Pair with
 915     /// [`pop_clip_rounded`](PaintCtx::pop_clip_rounded), or prefer
 916     /// [`clip_rounded`](PaintCtx::clip_rounded).
 917     pub fn push_clip_rounded(&mut self, rect: Rect, radius: f32) {
 918         self.push_clip(rect);
 919         let r = radius.max(0.0);
 920         if r > 0.0 {
 921             let abs = self.apply_offset(rect);
 922             self.clip_rrect_stack.push([
 923                 abs.x + abs.width / 2.0,
 924                 abs.y + abs.height / 2.0,
 925                 (abs.width / 2.0 - r).max(0.0),
 926                 (abs.height / 2.0 - r).max(0.0),
 927                 r,
 928             ]);
 929         } else {
 930             // Keep push/pop balanced regardless of radius.
 931             self.clip_rrect_stack.push([0.0; 5]);
 932         }
 933     }
 934 
 935     pub fn pop_clip_rounded(&mut self) {
 936         self.clip_rrect_stack.pop();
 937         self.pop_clip();
 938     }
 939 
 940     /// Run `f` with `rect` (radius `radius`) pushed as a rounded clip, popping it afterward.
 941     pub fn clip_rounded<R>(&mut self, rect: Rect, radius: f32, f: impl FnOnce(&mut Self) -> R) -> R {
 942         self.push_clip_rounded(rect, radius);
 943         let out = f(self);
 944         self.pop_clip_rounded();
 945         out
 946     }
 947 
 948     /// Run `f` with an additional translation applied to all emitted coordinates.
 949     /// Imperative translate pair for spans too large to wrap in
 950     /// [`translate`](Self::translate)'s closure (an app bracketing its whole
 951     /// frame in the overflow-margin shift). Must balance before `finish`.
 952     pub fn push_translate(&mut self, dx: f32, dy: f32) {
 953         self.offset_stack.push(self.offset);
 954         self.offset.0 += dx;
 955         self.offset.1 += dy;
 956     }
 957 
 958     /// See [`push_translate`](Self::push_translate).
 959     pub fn pop_translate(&mut self) {
 960         self.offset = self.offset_stack.pop().expect("translate stack underflow");
 961     }
 962 
 963     pub fn translate<R>(&mut self, dx: f32, dy: f32, f: impl FnOnce(&mut Self) -> R) -> R {
 964         self.offset_stack.push(self.offset);
 965         self.offset = (self.offset.0 + dx, self.offset.1 + dy);
 966         let out = f(self);
 967         self.offset = self.offset_stack.pop().expect("translate stack underflow");
 968         out
 969     }
 970 
 971     fn apply_offset(&self, r: Rect) -> Rect {
 972         Rect { x: r.x + self.offset.0, y: r.y + self.offset.1, width: r.width, height: r.height }
 973     }
 974 
 975     fn push(&mut self, prim: Prim) {
 976         let clip = self.current_clip();
 977         let clip_circle = self.clip_circle_stack.last().copied();
 978         // r == 0 entries are balance placeholders (a zero-radius rounded clip is just its
 979         // scissor rect) — record no rounded clip so batches keep merging.
 980         let clip_rrect = self.clip_rrect_stack.last().copied().filter(|c| c[4] > 0.0);
 981         self.list.items.push(PaintItem { prim, clip, clip_circle, clip_rrect });
 982     }
 983 
 984     pub fn quad(&mut self, rect: Rect, color: [f32; 4]) {
 985         let rect = self.apply_offset(rect);
 986         self.push(Prim::Quad { rect, color });
 987     }
 988 
 989     /// A user image (id from `cce_ui::vk::upload_rgba`) drawn at `rect`.
 990     pub fn image(&mut self, image: u32, rect: Rect, alpha: f32) {
 991         let rect = self.apply_offset(rect);
 992         self.push(Prim::Image { image, rect, alpha });
 993     }
 994 
 995     pub fn rounded_rect(&mut self, rect: Rect, radius: f32, corners: (bool, bool, bool, bool), color: [f32; 4]) {
 996         let rect = self.apply_offset(rect);
 997         self.push(Prim::RoundedRect { rect, radius, corners, color });
 998     }
 999 
1000     /// Feathered aura over a rounded rect (see [`Prim::Glow`]): interior at
1001     /// the color's alpha, smooth per-pixel falloff to zero across `reach` px
1002     /// outside the boundary.
1003     pub fn glow(&mut self, rect: Rect, radius: f32, reach: f32, color: [f32; 4]) {
1004         let rect = self.apply_offset(rect);
1005         self.push(Prim::Glow { rect, radius, reach, color });
1006     }
1007 
1008     pub fn vector(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, color: [f32; 4], cap: Cap) {
1009         let (ox, oy) = self.offset;
1010         self.push(Prim::Vector { x1: x1 + ox, y1: y1 + oy, x2: x2 + ox, y2: y2 + oy, thickness, color, cap });
1011     }
1012 
1013     pub fn circle(&mut self, cx: f32, cy: f32, radius: f32, color: [f32; 4]) {
1014         let (ox, oy) = self.offset;
1015         self.push(Prim::Circle { cx: cx + ox, cy: cy + oy, radius, color });
1016     }
1017 
1018     /// A sphere-lit circle — see `Prim::Sphere`.
1019     pub fn sphere(&mut self, cx: f32, cy: f32, radius: f32, material: &Material) {
1020         let (ox, oy) = self.offset;
1021         self.push(Prim::Sphere { cx: cx + ox, cy: cy + oy, radius, material: *material });
1022     }
1023 
1024     /// A hanging water droplet clinging to `rect`'s top edge — see
1025     /// [`Prim::Droplet`] and [`DropletSpec`].
1026     /// See [`Prim::DropletScrim`]. `feather` is how far in from the drop's
1027     /// edge the fill reaches full opacity, in logical px.
1028     pub fn droplet_scrim(&mut self, rect: Rect, material: &Material, spec: DropletSpec, feather: f32) {
1029         let rect = self.apply_offset(rect);
1030         self.push(Prim::DropletScrim { rect, material: *material, spec, feather });
1031     }
1032 
1033     /// The drop's finish is the material's (see [`DropletSpec::finish`]).
1034     pub fn droplet(&mut self, rect: Rect, material: &Material, spec: DropletSpec) {
1035         let rect = self.apply_offset(rect);
1036         self.push(Prim::Droplet { rect, material: *material, spec });
1037     }
1038 
1039     /// A concave inside-corner fillet — see `Prim::ConcaveFillet`. `start` is
1040     /// the quarter wedge's start angle; the arc's centre sits in the corner's
1041     /// pocket and the wall descends (or rises, `raised`) away from it.
1042     pub fn concave_fillet(&mut self, cx: f32, cy: f32, radius: f32, depth: f32, start: f32, raised: bool) {
1043         let (ox, oy) = self.offset;
1044         self.push(Prim::ConcaveFillet { cx: cx + ox, cy: cy + oy, radius, depth, start, raised });
1045     }
1046 
1047     /// Re-emit an already-built [`Prim`] through this context, so it re-records the
1048     /// current clip and translate state. This is the **single** place that has to
1049     /// learn a new `Prim` variant: a nested paint walk builds a scratch list and
1050     /// replays it into the real one, and that forwarding match used to exist
1051     /// verbatim in two crates ([`crate::widget::model`] and cce-cloud's
1052     /// `json_layout`) — adding `Prim::Groove` compiled against one and broke the
1053     /// other, caught only by a full workspace build.
1054     ///
1055     /// [`Prim::Text`] is NOT emitted: it is returned untouched, because the two
1056     /// callers disagree about it (a subtree painter authors its own text and wants
1057     /// it forwarded; everyone else drops it in favour of the widget's own label
1058     /// bridge). Every other variant is emitted and `None` comes back.
1059     #[must_use = "a returned Text prim was not emitted — drop or forward it explicitly"]
1060     pub fn replay(&mut self, prim: Prim) -> Option<Prim> {
1061         match prim {
1062             Prim::Text { .. } => return Some(prim),
1063             Prim::Quad { rect, color } => self.quad(rect, color),
1064             Prim::RoundedRect { rect, radius, corners, color } => {
1065                 self.rounded_rect(rect, radius, corners, color)
1066             }
1067             Prim::Border { rect, radii, fill, border, thickness } => {
1068                 self.border(rect, radii, fill, border, thickness)
1069             }
1070             Prim::Bevel { rect, radii, material, depth, tint } => {
1071                 self.bevel_tinted(rect, radii, &material, depth, tint)
1072             }
1073             Prim::Recess { rect, radii, depth, edges, tint } => match tint {
1074                 Some(t) => self.recess_tinted(rect, radii, depth, t),
1075                 None => self.recess_edges(rect, radii, depth, edges),
1076             },
1077             Prim::Boss { rect, radii, depth, edges, tint } => match tint {
1078                 Some(t) => self.boss_edges_tinted(rect, radii, depth, edges, t),
1079                 None => self.boss_edges(rect, radii, depth, edges),
1080             },
1081             Prim::Ridge { rect, radii, depth, edges } => self.ridge_edges(rect, radii, depth, edges),
1082             Prim::Trough { rect, radii, depth, edges, tint } => match tint {
1083                 Some(t) => self.trough_tinted(rect, radii, depth, t),
1084                 None => self.trough_edges(rect, radii, depth, edges),
1085             },
1086             Prim::Plate { rect, radii, material, depth, shape } => {
1087                 self.plate_shaped(rect, radii, &material, depth, shape)
1088             }
1089             Prim::Arc { cx, cy, radius, thickness, start, end, color } => {
1090                 self.arc(cx, cy, radius, thickness, start, end, color)
1091             }
1092             Prim::ArcShaded { cx, cy, radius, thickness, start, end, inner, crest, outer } => {
1093                 self.arc_shaded(cx, cy, radius, thickness, start, end, inner, crest, outer)
1094             }
1095             Prim::Vector { x1, y1, x2, y2, thickness, color, cap } => {
1096                 self.vector(x1, y1, x2, y2, thickness, color, cap)
1097             }
1098             Prim::Circle { cx, cy, radius, color } => self.circle(cx, cy, radius, color),
1099             Prim::Sphere { cx, cy, radius, material } => self.sphere(cx, cy, radius, &material),
1100             Prim::Glow { rect, radius, reach, color } => self.glow(rect, radius, reach, color),
1101             Prim::Droplet { rect, material, spec } => self.droplet(rect, &material, spec),
1102             Prim::DropletScrim { rect, material, spec, feather } => {
1103                 self.droplet_scrim(rect, &material, spec, feather)
1104             }
1105             Prim::ConcaveFillet { cx, cy, radius, depth, start, raised } => {
1106                 self.concave_fillet(cx, cy, radius, depth, start, raised)
1107             }
1108             Prim::Groove { a, b, width, depth, host } => self.groove(a, b, width, depth, host),
1109             Prim::Lattice { rect, period, origin, cell, radius, depth } => {
1110                 self.lattice(rect, period, origin, cell, radius, depth)
1111             }
1112             Prim::Grout { rect, period, origin, cell, radius, color } => {
1113                 self.grout(rect, period, origin, cell, radius, color)
1114             }
1115             Prim::Fill { rect, radii, material } => self.fill_material(rect, radii, &material),
1116             Prim::CarveUnion { boxes, depth, raised } => self.carve_union(boxes, depth, raised),
1117             Prim::Image { image, rect, alpha } => self.image(image, rect, alpha),
1118         }
1119         None
1120     }
1121 
1122     /// An engraved line from `a` to `b` cut into `host` — see [`Prim::Groove`].
1123     pub fn groove(&mut self, a: (f32, f32), b: (f32, f32), width: f32, depth: f32, host: Rect) {
1124         let (ox, oy) = self.offset;
1125         let host = self.apply_offset(host);
1126         self.push(Prim::Groove {
1127             a: (a.0 + ox, a.1 + oy),
1128             b: (b.0 + ox, b.1 + oy),
1129             width,
1130             depth,
1131             host,
1132         });
1133     }
1134 
1135     /// A periodic field of rounded wells carved as one surface — see
1136     /// [`Prim::Lattice`]. `origin` is any one cell's centre; `rect` bounds the
1137     /// shading. All logical px, like every other carve.
1138     pub fn lattice(
1139         &mut self,
1140         rect: Rect,
1141         period: (f32, f32),
1142         origin: (f32, f32),
1143         cell: (f32, f32),
1144         radius: f32,
1145         depth: f32,
1146     ) {
1147         let (ox, oy) = self.offset;
1148         let rect = self.apply_offset(rect);
1149         self.push(Prim::Lattice { rect, period, origin: (origin.0 + ox, origin.1 + oy), cell, radius, depth });
1150     }
1151 
1152     /// A flat fill of `material` — see [`Prim::Fill`].
1153     pub fn fill_material(&mut self, rect: Rect, radii: Radii, material: &Material) {
1154         let rect = self.apply_offset(rect);
1155         self.push(Prim::Fill { rect, radii, material: *material });
1156     }
1157 
1158     /// Grout between a periodic field of rounded cells — see [`Prim::Grout`].
1159     /// `origin` is any one cell's centre; `rect` bounds the paint.
1160     pub fn grout(&mut self, rect: Rect, period: (f32, f32), origin: (f32, f32), cell: (f32, f32), radius: f32, color: [f32; 4]) {
1161         let (ox, oy) = self.offset;
1162         let rect = self.apply_offset(rect);
1163         self.push(Prim::Grout { rect, period, origin: (origin.0 + ox, origin.1 + oy), cell, radius, color });
1164     }
1165 
1166     /// Carve (or raise, with `raised`) the union of `boxes` as one shape with
1167     /// one wall — see [`Prim::CarveUnion`]. `depth` is the wall's run in px,
1168     /// as for [`PaintCtx::recess`].
1169     pub fn carve_union(&mut self, boxes: Vec<(Rect, Radii)>, depth: f32, raised: bool) {
1170         let boxes: Vec<(Rect, Radii)> = boxes.into_iter().map(|(r, radii)| (self.apply_offset(r), radii)).collect();
1171         if boxes.is_empty() {
1172             return;
1173         }
1174         self.push(Prim::CarveUnion { boxes, depth, raised });
1175     }
1176 
1177     pub fn border(&mut self, rect: Rect, radii: Radii, fill: [f32; 4], border: [f32; 4], thickness: f32) {
1178         let rect = self.apply_offset(rect);
1179         self.push(Prim::Border { rect, radii, fill, border, thickness });
1180     }
1181 
1182     pub fn bevel(&mut self, rect: Rect, radii: Radii, material: &Material, depth: f32) {
1183         self.bevel_tinted(rect, radii, material, depth, [1.0, 1.0, 1.0]);
1184     }
1185 
1186     /// `bevel` with a specular tint — see `Prim::Bevel::tint`.
1187     pub fn bevel_tinted(&mut self, rect: Rect, radii: Radii, material: &Material, depth: f32, tint: [f32; 3]) {
1188         let rect = self.apply_offset(rect);
1189         self.push(Prim::Bevel { rect, radii, material: *material, depth, tint });
1190     }
1191 
1192     /// Carve a recess into the already-painted surface below. Unlike `bevel`, this fills
1193     /// nothing — the shading is an overlay, so it composes over whatever was painted.
1194     pub fn recess(&mut self, rect: Rect, radii: Radii, depth: f32) {
1195         self.recess_edges(rect, radii, depth, (true, true, true, true));
1196     }
1197 
1198     /// [`PaintCtx::recess`] with the lit rim tinted — see `Prim::Recess::tint`
1199     /// (the focused-well treatment).
1200     pub fn recess_tinted(&mut self, rect: Rect, radii: Radii, depth: f32, tint: [f32; 3]) {
1201         let rect = self.apply_offset(rect);
1202         self.push(Prim::Recess { rect, radii, depth, edges: (true, true, true, true), tint: Some(tint) });
1203     }
1204 
1205     /// Raise a plateau out of the already-painted surface below — the inverse of
1206     /// [`PaintCtx::recess`]. Only the edges are shaded; the face stays the surface
1207     /// beneath, so the raised region inherits the root plate's color. `depth` is the
1208     /// roll width in px (pass [`crate::layout::bevel_width`] unless the widget
1209     /// needs a tighter lip).
1210     pub fn boss(&mut self, rect: Rect, radii: Radii, depth: f32) {
1211         self.boss_edges(rect, radii, depth, (true, true, true, true));
1212     }
1213 
1214     /// [`PaintCtx::boss`] with only some of the walls — see `Prim::Boss`.
1215     pub fn boss_edges(
1216         &mut self,
1217         rect: Rect,
1218         radii: Radii,
1219         depth: f32,
1220         edges: (bool, bool, bool, bool),
1221     ) {
1222         let rect = self.apply_offset(rect);
1223         self.push(Prim::Boss { rect, radii, depth, edges, tint: None });
1224     }
1225 
1226     /// [`PaintCtx::boss_edges`] with a specular tint on the lit rim — see
1227     /// `Prim::Boss::tint`.
1228     pub fn boss_edges_tinted(
1229         &mut self,
1230         rect: Rect,
1231         radii: Radii,
1232         depth: f32,
1233         edges: (bool, bool, bool, bool),
1234         tint: [f32; 3],
1235     ) {
1236         let rect = self.apply_offset(rect);
1237         self.push(Prim::Boss { rect, radii, depth, edges, tint: Some(tint) });
1238     }
1239 
1240     /// Paint a control plate — see [`ControlPlate`]. The ONE place a control face's
1241     /// relief is composed: raised with a face is a `bevel` on the footprint;
1242     /// raised without one carves inside and raises a `boss`; flush carves
1243     /// inside and lays an `inset_plate` (trough plus face).
1244     pub fn control_plate(&mut self, plate: &ControlPlate) {
1245         match plate.stance {
1246             PlateStance::Raised => {
1247                 if let Some(face) = plate.faced() {
1248                     match plate.tint {
1249                         Some(t) => self.bevel_tinted(plate.rect, plate.radii, face, plate.depth, t),
1250                         None => self.bevel(plate.rect, plate.radii, face, plate.depth),
1251                     }
1252                 } else {
1253                     let (plateau, radii) = crate::layout::carve_inside(plate.rect, plate.radii, plate.depth);
1254                     match plate.tint {
1255                         Some(t) => self.boss_edges_tinted(plateau, radii, plate.depth, (true, true, true, true), t),
1256                         None => self.boss(plateau, radii, plate.depth),
1257                     }
1258                 }
1259             }
1260             PlateStance::Flush => {
1261                 let (trough, radii) = crate::layout::carve_inside(plate.rect, plate.radii, plate.depth);
1262                 match plate.tint {
1263                     Some(t) => self.inset_plate_tinted(trough, radii, plate.faced(), plate.depth, t),
1264                     None => self.inset_plate(trough, radii, plate.faced(), plate.depth),
1265                 }
1266             }
1267             PlateStance::Flat => {
1268                 if let Some(face) = plate.faced() {
1269                     // A QUAD deliberately, not the `Border` the relief stances
1270                     // fill through: carrying the blur-behind sentinel is half
1271                     // the point of this stance, and only quads reach it.
1272                     self.rounded_rect(
1273                         plate.rect,
1274                         plate.radii.0,
1275                         (true, true, true, true),
1276                         face.fill(PlateRole::Nested),
1277                     );
1278                 }
1279                 if let Some(t) = plate.tint {
1280                     // No relief, so no rim to light: the focus ring is drawn as
1281                     // one, over the face and keeping the per-corner silhouette.
1282                     self.border(plate.rect, plate.radii, [0.0; 4], [t[0], t[1], t[2], 1.0], 1.0);
1283                 }
1284             }
1285         }
1286     }
1287 
1288     /// A section's well — the settings app's union carve, the ONE shape a
1289     /// section or a [`crate::widget::Group`] is cut into the plate with: the
1290     /// `body` carved as a recess with `radii` (TL, TR, BR, BL), and when there
1291     /// is a title `tab` (flush on the body's top edge, at its left), the tab
1292     /// carved WITH it as one shape — the tab bottom-open, one piece owning the
1293     /// body's whole right run so its corners are real turns, a left piece
1294     /// carrying the left wall, the pieces extending past their interior seam by
1295     /// `depth` so the walls crossfade there instead of notching — and the
1296     /// throat's inside corner rounded by a concave fillet.
1297     pub fn section_well(&mut self, body: Rect, tab: Option<Rect>, radii: Radii, depth: f32) {
1298         let (cx, cy, cw, ch) = (body.x, body.y, body.width, body.height);
1299         let (tl, tr, br, bl) = radii;
1300         let Some(t) = tab else {
1301             self.recess_edges(body, radii, depth, (true, true, true, true));
1302             return;
1303         };
1304         let (tx, ty, tw, th) = (t.x, t.y, t.width, t.height);
1305         let rt = tl.max(tr).min(th * 0.45);
1306         let throat_r = tx + tw;
1307         // The designer's SECTION_FILLET_R.
1308         let rho = 10.0f32;
1309         let body_lr = |x_run: f32, pc: &mut Self| {
1310             pc.recess_edges(
1311                 Rect { x: x_run, y: cy, width: cx + cw - x_run, height: ch },
1312                 (0.0, tr, br, 0.0),
1313                 depth,
1314                 (true, true, true, false),
1315             );
1316             pc.recess_edges(
1317                 Rect { x: cx, y: cy, width: x_run + depth - cx, height: ch },
1318                 (0.0, 0.0, 0.0, bl),
1319                 depth,
1320                 (false, false, true, true),
1321             );
1322         };
1323         if cx + cw > throat_r + 2.0 * rho {
1324             // Filleted throat: the tab's right wall ends at the fillet's vertical
1325             // tangent, a left-only bridge carries the left wall across the span.
1326             self.recess_edges(
1327                 Rect { x: tx, y: ty, width: tw, height: (cy - rho) - ty + depth },
1328                 (rt, rt, 0.0, 0.0),
1329                 depth,
1330                 (true, true, false, true),
1331             );
1332             self.recess_edges(
1333                 Rect { x: tx, y: cy - rho, width: tw, height: rho + depth },
1334                 (0.0, 0.0, 0.0, 0.0),
1335                 depth,
1336                 (false, false, false, true),
1337             );
1338             body_lr(throat_r + rho - depth, self);
1339             self.concave_fillet(throat_r + rho, cy - rho, rho, depth, std::f32::consts::FRAC_PI_2, false);
1340         } else if cx + cw > throat_r + 0.5 {
1341             // Too narrow for the fillet: the plain square throat.
1342             self.recess_edges(
1343                 Rect { x: tx, y: ty, width: tw, height: (cy - ty) + depth },
1344                 (rt, rt, 0.0, 0.0),
1345                 depth,
1346                 (true, true, false, true),
1347             );
1348             body_lr(throat_r - depth, self);
1349         } else {
1350             // The tab spans the body: no top wall at all.
1351             self.recess_edges(
1352                 Rect { x: tx, y: ty, width: tw, height: (cy - ty) + depth },
1353                 (rt, rt, 0.0, 0.0),
1354                 depth,
1355                 (true, true, false, true),
1356             );
1357             self.recess_edges(
1358                 Rect { x: cx, y: cy, width: cw, height: ch },
1359                 (0.0, 0.0, br, bl),
1360                 depth,
1361                 (false, true, true, true),
1362             );
1363         }
1364     }
1365 
1366     /// A flush inset control: `rect`'s plate sits SUNKEN into the surface with
1367     /// its face level with it — a valley seam runs the boundary, the surface
1368     /// falling into it on the way out and the control's own face rising back
1369     /// out of it inside. The face never leaves the surface plane; the seam is
1370     /// the only thing saying it is a separate part. `depth` is the full width
1371     /// of that valley, which straddles the boundary by ±depth/2.
1372     ///
1373     /// One [`Prim::Trough`] — ONE lighting evaluation. This used to emit a
1374     /// `Recess` on a rect outset by depth/2 plus a `Boss` on the rect, whose
1375     /// walls overlapped over half their width and shaded twice; see
1376     /// `Prim::Trough` for what that measured as. Do not re-expand this into its
1377     /// parts.
1378     ///
1379     /// An opaque `color` fills the face; transparent leaves the surface below
1380     /// showing through as the face.
1381     pub fn inset_plate(&mut self, rect: Rect, radii: Radii, face: Option<&Material>, depth: f32) {
1382         // A transparent material is no face either — only a visible tint
1383         // fills; a frosted one fills with the sentinel.
1384         if let Some(face) = face.filter(|m| m.tint[3] > 0.001) {
1385             // Flat fill only — the relief is the trough's, so the face must not
1386             // carry a lip of its own (that lip WAS the second wall).
1387             //
1388             // Deliberately a zero-stroke `Border` and NOT `rounded_rect`: this
1389             // fill used to be a `Bevel`, and the legacy reverse bridges
1390             // (`all_rounded_quads` and friends in `widget/model.rs`) extract
1391             // `Prim::RoundedRect` but neither `Bevel` nor `Border`. Emitting a
1392             // RoundedRect here would newly leak every raised control's face into
1393             // those getters — a change to the legacy surface that has nothing to
1394             // do with the relief. Border also keeps all four radii, which
1395             // `Prim::RoundedRect`'s single radius cannot.
1396             self.border(rect, radii, face.fill(PlateRole::Nested), [0.0; 4], 0.0);
1397         }
1398         self.trough(rect, radii, depth);
1399     }
1400 
1401     /// [`inset_plate`](Self::inset_plate) with the rim lit — the focused flush
1402     /// control plate's ring (`ControlPlate::with_tint`); the face fill as
1403     /// there, the trough tinted.
1404     pub fn inset_plate_tinted(&mut self, rect: Rect, radii: Radii, face: Option<&Material>, depth: f32, tint: [f32; 3]) {
1405         if let Some(face) = face.filter(|m| m.tint[3] > 0.001) {
1406             self.border(rect, radii, face.fill(PlateRole::Nested), [0.0; 4], 0.0);
1407         }
1408         self.trough_tinted(rect, radii, depth, tint);
1409     }
1410 
1411     /// A canvas well's floor — the opening you look into or draw in (a
1412     /// Trackpad, a Slider2D pad, a bevel or ramp preview) — cut into `host`,
1413     /// the material of the plate it sits on (`Material::pane()` for a pane).
1414     /// `lifted` is a clickable canvas's hover cue: the floor rises toward
1415     /// the plate.
1416     ///
1417     /// An opaque host's floor is that plate darkened, drawn as the darkening
1418     /// itself ([`crate::colors::WELL_FLOOR`] over whatever the plate resolved
1419     /// to — exact at any plate alpha, and what every floor drew before
1420     /// materials). A FROSTED host's floor is deeper glass
1421     /// ([`Material::floor`]: the host's material with the tint darkened,
1422     /// frost and finish carried), so a well in glass blurs and compresses
1423     /// what is under it again instead of being the one opaque patch in a
1424     /// frosted pane (RFC material § 11 (3)).
1425     pub fn well_floor(&mut self, rect: Rect, radius: f32, host: &Material, lifted: bool) {
1426         let fill = if host.frost.is_frosted() {
1427             host.floor(lifted).fill(PlateRole::Nested)
1428         } else if lifted {
1429             crate::colors::WELL_FLOOR_LIFTED
1430         } else {
1431             crate::colors::WELL_FLOOR
1432         };
1433         self.rounded_rect(rect, radius, (true, true, true, true), fill);
1434     }
1435 
1436     /// A canvas well's rim, drawn AFTER the content so the wall's shading falls
1437     /// over whatever runs to the edge. Under `relief` it is the recess carved
1438     /// inside `rect` ([`crate::layout::carve_inside`], the wall the DE width
1439     /// capped at a fifth of the height — every well's rule); flat, the
1440     /// hairline frame every well shares ([`crate::colors::well_frame_color`]).
1441     pub fn well_rim(&mut self, rect: Rect, radius: f32, relief: bool) {
1442         let radii = (radius, radius, radius, radius);
1443         if relief {
1444             let depth = crate::layout::bevel_width().min(rect.height * 0.2);
1445             let (well, radii) = crate::layout::carve_inside(rect, radii, depth);
1446             self.recess(well, radii, depth);
1447         } else {
1448             self.border(rect, radii, [0.0; 4], crate::colors::well_frame_color(false, false), 1.0);
1449         }
1450     }
1451 
1452     /// [`well_floor`](Self::well_floor) then [`well_rim`](Self::well_rim) in
1453     /// one call — a canvas whose content is drawn over the rim (a Trackpad's
1454     /// fingers). Content that should slide under the wall draws between the two.
1455     pub fn canvas_well(&mut self, rect: Rect, radius: f32, host: &Material, relief: bool, lifted: bool) {
1456         self.well_floor(rect, radius, host, lifted);
1457         self.well_rim(rect, radius, relief);
1458     }
1459 
1460     /// Emit one [`crate::layout::ReliefCarve`]. The shared application point:
1461     /// a widget's `paint` carves through here, and a flat host re-emits the
1462     /// carves it collected through here too, so the two can only ever draw the
1463     /// same prim.
1464     ///
1465     /// A tinted recess takes `recess_tinted`, which lights the whole rim — it
1466     /// is the focus treatment, and every tinted carve the toolkit emits is a
1467     /// full ring. A partial ring falls back to the untinted walls rather than
1468     /// silently tinting walls the caller suppressed.
1469     pub fn carve(&mut self, c: &crate::layout::ReliefCarve) {
1470         let rect = Rect { x: c.x, y: c.y, width: c.w, height: c.h };
1471         match c.kind {
1472             crate::layout::CarveKind::Boss { tint: Some(t) } if c.edges == (true, true, true, true) => {
1473                 self.boss_edges_tinted(rect, c.radii, c.depth, c.edges, t)
1474             }
1475             crate::layout::CarveKind::Boss { .. } => self.boss_edges(rect, c.radii, c.depth, c.edges),
1476             crate::layout::CarveKind::Recess { tint: Some(t) }
1477                 if c.edges == (true, true, true, true) =>
1478             {
1479                 self.recess_tinted(rect, c.radii, c.depth, t)
1480             }
1481             crate::layout::CarveKind::Recess { .. } => {
1482                 self.recess_edges(rect, c.radii, c.depth, c.edges)
1483             }
1484             crate::layout::CarveKind::Trough => self.trough_edges(rect, c.radii, c.depth, c.edges),
1485         }
1486     }
1487 
1488     /// Sink a valley along `rect`'s boundary — see [`Prim::Trough`]. `depth` is
1489     /// the full width of the seam (it straddles the outline by ±depth/2).
1490     pub fn trough(&mut self, rect: Rect, radii: Radii, depth: f32) {
1491         self.trough_edges(rect, radii, depth, (true, true, true, true));
1492     }
1493 
1494     /// [`PaintCtx::trough`] with only some of the walls — see [`Prim::Trough`].
1495     pub fn trough_edges(
1496         &mut self,
1497         rect: Rect,
1498         radii: Radii,
1499         depth: f32,
1500         edges: (bool, bool, bool, bool),
1501     ) {
1502         let rect = self.apply_offset(rect);
1503         self.push(Prim::Trough { rect, radii, depth, edges, tint: None });
1504     }
1505 
1506     /// [`PaintCtx::trough`] with the rim lit — see `Prim::Trough::tint` (the
1507     /// focused flush control plate).
1508     pub fn trough_tinted(&mut self, rect: Rect, radii: Radii, depth: f32, tint: [f32; 3]) {
1509         let rect = self.apply_offset(rect);
1510         self.push(Prim::Trough { rect, radii, depth, edges: (true, true, true, true), tint: Some(tint) });
1511     }
1512 
1513     /// Raise a rim along `rect`'s boundary — see `Prim::Ridge`. `depth` is the
1514     /// full width of the bump (it straddles the outline by ±depth/2).
1515     pub fn ridge(&mut self, rect: Rect, radii: Radii, depth: f32) {
1516         self.ridge_edges(rect, radii, depth, (true, true, true, true));
1517     }
1518 
1519     /// [`PaintCtx::ridge`] with only some of the walls — see `Prim::Ridge`.
1520     pub fn ridge_edges(
1521         &mut self,
1522         rect: Rect,
1523         radii: Radii,
1524         depth: f32,
1525         edges: (bool, bool, bool, bool),
1526     ) {
1527         let rect = self.apply_offset(rect);
1528         self.push(Prim::Ridge { rect, radii, depth, edges });
1529     }
1530 
1531     /// [`PaintCtx::recess`] with only some of the walls — see `Prim::Recess`.
1532     pub fn recess_edges(
1533         &mut self, rect: Rect, radii: Radii, depth: f32,
1534         edges: (bool, bool, bool, bool),
1535     ) {
1536         let rect = self.apply_offset(rect);
1537         self.push(Prim::Recess { rect, radii, depth, edges, tint: None });
1538     }
1539 
1540     /// The window's glass slab: rounded fill at full size plus a rolled, lit perimeter.
1541     /// `depth` is the roll-off width in px — pass [`crate::layout::bevel_width`] unless the
1542     /// window wants a shallower edge than the DE default.
1543     ///
1544     /// A NEGATIVE `depth` is the fill-less sentinel: no fill is drawn, and the
1545     /// rolled perimeter (width `-depth`) renders as an overlay — translucent
1546     /// white screen / black multiply — over whatever is beneath, for a root
1547     /// plate whose face is not a fill (the designer's full-bleed 3D canvas).
1548     /// `material` is ignored; the roll profile, crest and specular are exactly the
1549     /// positive-depth plate's.
1550     pub fn plate(&mut self, rect: Rect, radii: Radii, material: &Material, depth: f32) {
1551         self.plate_shaped(rect, radii, material, depth, None);
1552     }
1553 
1554     /// [`plate`](Self::plate) with an explicit corner exponent — see
1555     /// [`Prim::Plate`]'s `shape`. `Some(2.0)` on a plate whose radii are its
1556     /// half-extent draws a circle; `None` is exactly `plate`.
1557     pub fn plate_shaped(&mut self, rect: Rect, radii: Radii, material: &Material, depth: f32, shape: Option<f32>) {
1558         let rect = self.apply_offset(rect);
1559         self.push(Prim::Plate { rect, radii, material: *material, depth, shape });
1560     }
1561 
1562     /// Emit the plate a [`PlateSpec`] describes: role-resolved per-corner
1563     /// radii and role-encoded frost (RFC Phase 7b).
1564     ///
1565     /// The spec's radii are FINAL on-screen values (a window corner already
1566     /// wears the full silhouette span), but `Prim::Plate` speaks the older
1567     /// convention — NOMINAL radii, span applied downstream by
1568     /// `plate_push_raised(scale_corners = true)`, which the unmigrated
1569     /// hand-rolled plates (cce-cloud, the test-interface gallery shim) still
1570     /// rely on. So divide the span back out here and let the push multiply
1571     /// reconstruct the spec's exact values.
1572     ///
1573     /// Feeding the final radii straight through double-spanned every window
1574     /// corner (12 → ~100 logical at corner_shape 4.5): the plate arc pulled
1575     /// away from the compositor's clip, the black window background showed
1576     /// through as a corner crescent, and the corners stopped matching the
1577     /// desktop grid — the original 7b-2 report of this looking like "the arc
1578     /// correction" was the regression itself.
1579     pub fn plate_spec(&mut self, spec: &PlateSpec) {
1580         let f = crate::layout::corner_span_factor();
1581         let (tl, tr, br, bl) = spec.radii();
1582         self.plate(spec.rect, (tl / f, tr / f, br / f, bl / f), &spec.material.for_role(spec.role()), spec.depth);
1583     }
1584 
1585     /// The standard root plate of a `width` x `height` window —
1586     /// [`PlateSpec::window`] emitted. The first prim of a standard cce app's
1587     /// frame: everything else is laid on this surface (pane plates atop it,
1588     /// bands and wells carved into it), starting
1589     /// [`crate::layout::root_plate_inset`] in from each window edge.
1590     pub fn root_plate(&mut self, width: f32, height: f32) {
1591         self.plate_spec(&PlateSpec::window(width, height));
1592     }
1593 
1594     pub fn arc(&mut self, cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4]) {
1595         let (ox, oy) = self.offset;
1596         self.push(Prim::Arc { cx: cx + ox, cy: cy + oy, radius, thickness, start, end, color });
1597     }
1598 
1599     /// A radially-shaded ring band — see [`Prim::ArcShaded`].
1600     #[allow(clippy::too_many_arguments)]
1601     pub fn arc_shaded(
1602         &mut self,
1603         cx: f32,
1604         cy: f32,
1605         radius: f32,
1606         thickness: f32,
1607         start: f32,
1608         end: f32,
1609         inner: [f32; 4],
1610         crest: [f32; 4],
1611         outer: [f32; 4],
1612     ) {
1613         let (ox, oy) = self.offset;
1614         self.push(Prim::ArcShaded {
1615             cx: cx + ox,
1616             cy: cy + oy,
1617             radius,
1618             thickness,
1619             start,
1620             end,
1621             inner,
1622             crest,
1623             outer,
1624         });
1625     }
1626 
1627     pub fn text(&mut self, text: impl Into<String>, x: f32, y: f32, font_size: f32, color: [u8; 3]) {
1628         self.text_with(text, x, y, font_size, color, None, None);
1629     }
1630 
1631     /// Text with a per-label font and clip rect (`[l, t, r, b]`, local space) — what the
1632     /// legacy `text_labels_with_font_and_bounds` tuples carry, expressible in the display
1633     /// list since Phase 6.
1634     pub fn text_with(
1635         &mut self,
1636         text: impl Into<String>,
1637         x: f32,
1638         y: f32,
1639         font_size: f32,
1640         color: [u8; 3],
1641         font: Option<String>,
1642         bounds: Option<[f32; 4]>,
1643     ) {
1644         self.text_attrs(text, x, y, font_size, color, font, bounds, TextAttrs::default());
1645     }
1646 
1647     /// [`text_with`](PaintCtx::text_with) plus shaping attributes (italic / weight) — what the
1648     /// font picker's style-variant previews need beyond family + size.
1649     #[allow(clippy::too_many_arguments)]
1650     pub fn text_attrs(
1651         &mut self,
1652         text: impl Into<String>,
1653         x: f32,
1654         y: f32,
1655         font_size: f32,
1656         color: [u8; 3],
1657         font: Option<String>,
1658         bounds: Option<[f32; 4]>,
1659         attrs: TextAttrs,
1660     ) {
1661         let (ox, oy) = self.offset;
1662         let bounds = bounds.map(|[l, t, r, b]| [l + ox, t + oy, r + ox, b + oy]);
1663         self.push(Prim::Text { text: text.into(), x: x + ox, y: y + oy, font_size, color, alpha: 1.0, font, bounds, attrs, layout: None });
1664     }
1665 
1666     /// [`text_with`](PaintCtx::text_with) plus a glyph alpha (1.0 = opaque) — translucent
1667     /// labels (a pane fading out) without changing the sRGB u8 color convention.
1668     #[allow(clippy::too_many_arguments)]
1669     pub fn text_faded(
1670         &mut self,
1671         text: impl Into<String>,
1672         x: f32,
1673         y: f32,
1674         font_size: f32,
1675         color: [u8; 3],
1676         alpha: f32,
1677         font: Option<String>,
1678         bounds: Option<[f32; 4]>,
1679     ) {
1680         let (ox, oy) = self.offset;
1681         let bounds = bounds.map(|[l, t, r, b]| [l + ox, t + oy, r + ox, b + oy]);
1682         self.push(Prim::Text {
1683             text: text.into(),
1684             x: x + ox,
1685             y: y + oy,
1686             font_size,
1687             color,
1688             alpha,
1689             font,
1690             bounds,
1691             attrs: TextAttrs::default(),
1692             layout: None,
1693         });
1694     }
1695 
1696     /// Boxed text: word-wrap + horizontal/vertical alignment within a box (a placed text box).
1697     /// Unlike [`text_with`](PaintCtx::text_with), the backend shapes this uncached with the box
1698     /// layout applied. `x, y` are the box's top-left; the backend applies the vertical offset.
1699     #[allow(clippy::too_many_arguments)]
1700     pub fn text_boxed(
1701         &mut self,
1702         text: impl Into<String>,
1703         x: f32,
1704         y: f32,
1705         font_size: f32,
1706         color: [u8; 3],
1707         font: Option<String>,
1708         bounds: Option<[f32; 4]>,
1709         attrs: TextAttrs,
1710         layout: TextLayout,
1711     ) {
1712         let (ox, oy) = self.offset;
1713         let bounds = bounds.map(|[l, t, r, b]| [l + ox, t + oy, r + ox, b + oy]);
1714         self.push(Prim::Text {
1715             text: text.into(),
1716             x: x + ox,
1717             y: y + oy,
1718             font_size,
1719             color,
1720             alpha: 1.0,
1721             font,
1722             bounds,
1723             attrs,
1724             layout: Some(layout),
1725         });
1726     }
1727 
1728     /// Consume the context and return the accumulated display list.
1729     pub fn finish(self) -> DisplayList {
1730         debug_assert!(self.clip_stack.is_empty(), "unbalanced push_clip/pop_clip");
1731         debug_assert!(self.offset_stack.is_empty(), "unbalanced translate");
1732         self.list
1733     }
1734 }
1735 
1736 /// `PaintCtx` as a popover render target: display-list hosts pass their frame
1737 /// ctx straight into `render_popover`, so popovers draw REAL prims — relief
1738 /// plates, rounded rects, bounded text — instead of the flattened
1739 /// `PopoverCollector` view (which stays for legacy tuple hosts).
1740 impl crate::layout::RenderTarget for PaintCtx {
1741     fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
1742         self.quad(Rect { x, y, width: w, height: h }, color);
1743     }
1744     fn rect_with_radius(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32) {
1745         self.rounded_rect(Rect { x, y, width: w, height: h }, radius, (true, true, true, true), color);
1746     }
1747     fn rect_with_radius_corners(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, corners: (bool, bool, bool, bool)) {
1748         self.rounded_rect(Rect { x, y, width: w, height: h }, radius, corners, color);
1749     }
1750     fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
1751         let c = [
1752             (color[0] * 255.0).clamp(0.0, 255.0) as u8,
1753             (color[1] * 255.0).clamp(0.0, 255.0) as u8,
1754             (color[2] * 255.0).clamp(0.0, 255.0) as u8,
1755         ];
1756         PaintCtx::text(self, content, x, y, size, c);
1757     }
1758     fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
1759         crate::layout::RenderTarget::text_with_font_and_bounds(self, content, x, y, size, color, font, None);
1760     }
1761     fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
1762         let c = [
1763             (color[0] * 255.0).clamp(0.0, 255.0) as u8,
1764             (color[1] * 255.0).clamp(0.0, 255.0) as u8,
1765             (color[2] * 255.0).clamp(0.0, 255.0) as u8,
1766         ];
1767         self.text_with(content, x, y, size, c, None, bounds);
1768     }
1769     fn text_with_font_and_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, bounds: Option<[f32; 4]>) {
1770         let c = [
1771             (color[0] * 255.0).clamp(0.0, 255.0) as u8,
1772             (color[1] * 255.0).clamp(0.0, 255.0) as u8,
1773             (color[2] * 255.0).clamp(0.0, 255.0) as u8,
1774         ];
1775         self.text_with(content, x, y, size, c, Some(font.to_string()), bounds);
1776     }
1777     fn push_clip_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
1778         self.push_clip(Rect { x, y, width: w, height: h });
1779     }
1780     fn pop_clip_rect(&mut self) {
1781         self.pop_clip();
1782     }
1783     fn inset_plate(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32) {
1784         PaintCtx::inset_plate(self, Rect { x, y, width: w, height: h }, (radius, radius, radius, radius), Material::face(color).as_ref(), depth);
1785     }
1786     fn inset_plate_tinted(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, tint: [f32; 3]) {
1787         PaintCtx::inset_plate_tinted(self, Rect { x, y, width: w, height: h }, (radius, radius, radius, radius), Material::face(color).as_ref(), depth, tint);
1788     }
1789     fn relief_carve(&mut self, carve: &crate::layout::ReliefCarve) {
1790         PaintCtx::carve(self, carve);
1791     }
1792 }
1793 
1794 #[cfg(test)]
1795 mod tests {
1796     use super::*;
1797     use crate::scene::material::Frost;
1798 
1799     /// RFC Phase 7b: PlateSpec role mechanics — flag derivation from window
1800     /// geometry, silhouette-vs-nominal radii selection, and the role-encoded
1801     /// frost (root positive-alpha, nested negative-alpha sentinel).
1802     #[test]
1803     fn plate_spec_roles() {
1804         // Flags: a full-window rect is root; an inset pane has none; a pane
1805         // flush to the window's right edge owns the two right corners.
1806         let root_flags = PlateSpec::window_corner_flags(
1807             Rect { x: 0.0, y: 0.0, width: 800.0, height: 600.0 }, 800.0, 600.0);
1808         assert_eq!(root_flags, (true, true, true, true));
1809         let inset = PlateSpec::window_corner_flags(
1810             Rect { x: 20.0, y: 20.0, width: 100.0, height: 100.0 }, 800.0, 600.0);
1811         assert_eq!(inset, (false, false, false, false));
1812         let right_pane = PlateSpec::window_corner_flags(
1813             Rect { x: 500.0, y: 0.0, width: 300.0, height: 600.0 }, 800.0, 600.0);
1814         assert_eq!(right_pane, (false, true, true, false));
1815 
1816         // Radii: flagged corners wear the shared silhouette curve, interior
1817         // ones the nominal plate radius (compared against the same getters,
1818         // so the assertion holds for any configured values).
1819         let window_r =
1820             crate::layout::window_corner_radius() * crate::layout::corner_span_factor();
1821         let nominal = crate::layout::plate_corner_radius();
1822         let r = PlateSpec::radii_for((false, true, true, false));
1823         assert_eq!(r, (nominal, window_r, window_r, nominal));
1824 
1825         // Frost encoding by role.
1826         let frosted = Frost::Frosted { compression: 0.0, refraction: 0.0, radius: Frost::DEFAULT_RADIUS };
1827         let mut spec = PlateSpec {
1828             rect: Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 },
1829             material: Material::opaque([0.1, 0.2, 0.3, 0.8]).with_frost(frosted),
1830             window_corners: (true, true, true, true),
1831             depth: 3.0,
1832         };
1833         assert!(spec.is_root());
1834         assert_eq!(spec.role(), PlateRole::Root);
1835         assert!(spec.fill()[3] > 0.0, "root frost is the compositor's; alpha stays positive");
1836         spec.window_corners = (false, true, true, false);
1837         assert!(!spec.is_root());
1838         assert_eq!(spec.role(), PlateRole::Nested);
1839         assert!(spec.fill()[3] < 0.0, "nested frost = negative-alpha sentinel");
1840         spec.material.frost = Frost::Opaque;
1841         assert_eq!(spec.fill()[3], 0.8, "no frost, no encoding");
1842 
1843         // The detach role flip (RFC 7c): a frosted nested pane becomes a
1844         // root — silhouette corners, and the frost regime flips from the
1845         // in-app sentinel to the compositor's (alpha back to positive).
1846         spec.material.frost = frosted;
1847         assert!(spec.fill()[3] < 0.0);
1848         let det = spec.detached();
1849         assert!(det.is_root());
1850         assert!(det.fill()[3] > 0.0, "root frost is the compositor's again");
1851         let wr = crate::layout::window_silhouette_radius();
1852         assert_eq!(det.radii(), (wr, wr, wr, wr));
1853     }
1854 
1855     fn r(x: f32, y: f32, w: f32, h: f32) -> Rect {
1856         Rect { x, y, width: w, height: h }
1857     }
1858 
1859     /// The emission round-trip: `plate_spec` pre-divides by the span factor so
1860     /// `plate_push_raised(scale_corners = true)` lands each corner at exactly
1861     /// the spec's final radius. Guards the double-span regression (7b-2), and
1862     /// holds for any configured corner_shape because both sides use the same
1863     /// factor.
1864     #[test]
1865     fn plate_spec_emission_round_trips_the_span() {
1866         let spec = PlateSpec {
1867             rect: r(0.0, 0.0, 400.0, 300.0),
1868             material: Material::opaque([0.1, 0.2, 0.3, 0.8]),
1869             window_corners: (true, true, false, false),
1870             depth: 4.0,
1871         };
1872         let mut pc = PaintCtx::new();
1873         pc.plate_spec(&spec);
1874         let f = crate::layout::corner_span_factor();
1875         let emitted = pc
1876             .finish()
1877             .items
1878             .iter()
1879             .find_map(|it| match &it.prim {
1880                 Prim::Plate { radii, .. } => Some(radii.clone()),
1881                 _ => None,
1882             })
1883             .expect("plate_spec emits a Prim::Plate");
1884         let want = spec.radii();
1885         let got = (emitted.0 * f, emitted.1 * f, emitted.2 * f, emitted.3 * f);
1886         for (g, w) in [(got.0, want.0), (got.1, want.1), (got.2, want.2), (got.3, want.3)] {
1887             assert!((g - w).abs() < 1e-3, "span round-trip drifted: {g} vs {w}");
1888         }
1889     }
1890 
1891     #[test]
1892     fn emits_in_order_unclipped() {
1893         let mut ctx = PaintCtx::new();
1894         ctx.quad(r(0.0, 0.0, 10.0, 10.0), [1.0, 0.0, 0.0, 1.0]);
1895         ctx.quad(r(5.0, 5.0, 10.0, 10.0), [0.0, 1.0, 0.0, 1.0]);
1896         let list = ctx.finish();
1897         assert_eq!(list.len(), 2);
1898         assert_eq!(list.items[0].clip, None);
1899         assert!(matches!(list.items[0].prim, Prim::Quad { color, .. } if color[0] == 1.0));
1900         assert!(matches!(list.items[1].prim, Prim::Quad { color, .. } if color[1] == 1.0));
1901     }
1902 
1903     #[test]
1904     fn clip_is_recorded_and_popped() {
1905         let mut ctx = PaintCtx::new();
1906         ctx.clip(r(0.0, 0.0, 50.0, 50.0), |ctx| {
1907             ctx.quad(r(10.0, 10.0, 5.0, 5.0), [0.0; 4]);
1908         });
1909         ctx.quad(r(60.0, 60.0, 5.0, 5.0), [0.0; 4]); // outside any clip now
1910         let list = ctx.finish();
1911         assert_eq!(list.items[0].clip, Some(r(0.0, 0.0, 50.0, 50.0)));
1912         assert_eq!(list.items[1].clip, None, "clip popped after the closure");
1913     }
1914 
1915     #[test]
1916     fn nested_clips_intersect() {
1917         let mut ctx = PaintCtx::new();
1918         ctx.clip(r(0.0, 0.0, 100.0, 100.0), |ctx| {
1919             ctx.clip(r(50.0, 50.0, 100.0, 100.0), |ctx| {
1920                 ctx.quad(r(0.0, 0.0, 1.0, 1.0), [0.0; 4]);
1921             });
1922         });
1923         // Intersection of (0,0,100,100) and (50,50,100,100) = (50,50,50,50).
1924         assert_eq!(ctx.finish().items[0].clip, Some(r(50.0, 50.0, 50.0, 50.0)));
1925     }
1926 
1927     #[test]
1928     fn non_overlapping_clips_produce_empty_scissor() {
1929         let mut ctx = PaintCtx::new();
1930         ctx.clip(r(0.0, 0.0, 10.0, 10.0), |ctx| {
1931             ctx.clip(r(100.0, 100.0, 10.0, 10.0), |ctx| {
1932                 ctx.quad(r(0.0, 0.0, 1.0, 1.0), [0.0; 4]);
1933             });
1934         });
1935         let clip = ctx.finish().items[0].clip.unwrap();
1936         assert_eq!((clip.width, clip.height), (0.0, 0.0), "empty intersection");
1937     }
1938 
1939     #[test]
1940     fn translate_applies_to_coordinates_and_restores() {
1941         let mut ctx = PaintCtx::new();
1942         ctx.translate(100.0, 200.0, |ctx| {
1943             ctx.quad(r(0.0, 0.0, 5.0, 5.0), [0.0; 4]);
1944         });
1945         ctx.quad(r(0.0, 0.0, 5.0, 5.0), [0.0; 4]); // back at origin
1946         let list = ctx.finish();
1947         assert!(matches!(list.items[0].prim, Prim::Quad { rect, .. } if rect.x == 100.0 && rect.y == 200.0));
1948         assert!(matches!(list.items[1].prim, Prim::Quad { rect, .. } if rect.x == 0.0 && rect.y == 0.0));
1949     }
1950 
1951     #[test]
1952     fn nested_translate_is_cumulative() {
1953         let mut ctx = PaintCtx::new();
1954         ctx.translate(10.0, 10.0, |ctx| {
1955             ctx.translate(5.0, 5.0, |ctx| {
1956                 ctx.circle(0.0, 0.0, 3.0, [0.0; 4]);
1957             });
1958         });
1959         assert!(matches!(ctx.finish().items[0].prim, Prim::Circle { cx, cy, .. } if cx == 15.0 && cy == 15.0));
1960     }
1961 
1962     #[test]
1963     fn clip_pushed_under_translation_is_absolute() {
1964         let mut ctx = PaintCtx::new();
1965         ctx.translate(20.0, 20.0, |ctx| {
1966             ctx.clip(r(0.0, 0.0, 30.0, 30.0), |ctx| {
1967                 ctx.quad(r(0.0, 0.0, 5.0, 5.0), [0.0; 4]);
1968             });
1969         });
1970         let item = &ctx.finish().items[0];
1971         // Clip translated to absolute (20,20,30,30); prim likewise at (20,20).
1972         assert_eq!(item.clip, Some(r(20.0, 20.0, 30.0, 30.0)));
1973         assert!(matches!(item.prim, Prim::Quad { rect, .. } if rect.x == 20.0 && rect.y == 20.0));
1974     }
1975 
1976     #[test]
1977     fn all_primitive_kinds_emit() {
1978         let mut ctx = PaintCtx::new();
1979         ctx.quad(r(0.0, 0.0, 1.0, 1.0), [0.0; 4]);
1980         ctx.rounded_rect(r(0.0, 0.0, 1.0, 1.0), 2.0, (true, false, true, false), [0.0; 4]);
1981         ctx.border(r(0.0, 0.0, 10.0, 10.0), (2.0, 2.0, 2.0, 2.0), [0.1; 4], [0.9; 4], 1.5);
1982         ctx.bevel(r(0.0, 0.0, 10.0, 10.0), (2.0, 2.0, 2.0, 2.0), &Material::opaque([0.3; 4]), 2.0);
1983         ctx.arc(5.0, 5.0, 4.0, 1.0, 0.0, 3.14, [0.0; 4]);
1984         ctx.vector(0.0, 0.0, 10.0, 0.0, 1.0, [0.0; 4], Cap::Arrow);
1985         ctx.circle(5.0, 5.0, 3.0, [0.0; 4]);
1986         ctx.text("hi", 1.0, 2.0, 12.0, [255, 255, 255]);
1987         assert_eq!(ctx.finish().len(), 8);
1988     }
1989 
1990     #[test]
1991     fn border_and_bevel_are_offset() {
1992         let mut ctx = PaintCtx::new();
1993         ctx.translate(10.0, 20.0, |ctx| {
1994             ctx.border(r(0.0, 0.0, 5.0, 5.0), (1.0, 1.0, 1.0, 1.0), [0.0; 4], [1.0; 4], 1.0);
1995             ctx.bevel(r(0.0, 0.0, 5.0, 5.0), (1.0, 1.0, 1.0, 1.0), &Material::opaque([0.0; 4]), 1.0);
1996         });
1997         let list = ctx.finish();
1998         assert!(matches!(list.items[0].prim, Prim::Border { rect, .. } if rect.x == 10.0 && rect.y == 20.0));
1999         assert!(matches!(list.items[1].prim, Prim::Bevel { rect, .. } if rect.x == 10.0 && rect.y == 20.0));
2000     }
2001     #[test]
2002     fn text_with_translates_position_and_bounds() {
2003         let mut ctx = PaintCtx::new();
2004         ctx.translate(10.0, 20.0, |ctx| {
2005             ctx.text_with("hi", 1.0, 2.0, 12.0, [1, 2, 3], Some("Mono".into()), Some([0.0, 0.0, 50.0, 30.0]));
2006             ctx.text("plain", 3.0, 4.0, 10.0, [9, 9, 9]);
2007         });
2008         let list = ctx.finish();
2009         match &list.items[0].prim {
2010             Prim::Text { x, y, font, bounds, .. } => {
2011                 assert_eq!((*x, *y), (11.0, 22.0), "position translated");
2012                 assert_eq!(font.as_deref(), Some("Mono"));
2013                 assert_eq!(*bounds, Some([10.0, 20.0, 60.0, 50.0]), "bounds translated");
2014             }
2015             other => panic!("expected Text, got {other:?}"),
2016         }
2017         match &list.items[1].prim {
2018             Prim::Text { font, bounds, .. } => {
2019                 assert_eq!(*font, None, "plain text carries no font");
2020                 assert_eq!(*bounds, None);
2021             }
2022             other => panic!("expected Text, got {other:?}"),
2023         }
2024     }
2025 
2026 }