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 }