GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/scene/paint.rs (108.5K)
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 /// How far past a [`Prim::Field`]'s end its run's end is put to say the run
221 /// REACHES that end and there is no well on that side: far enough that the
222 /// blend across the seam and the seam's own wall land nowhere near the
223 /// field. An encoding of the prim's; a [`Field`] says it by its form.
224 pub const FIELD_RUN_ONLY: f32 = 1.0e4;
225
226 /// How near a run's end may come to the field's end and still be taken as
227 /// reaching it — the shader's own tolerance (half a pixel, `MODE_FIELD`).
228 const FIELD_REACH: f32 = 0.5;
229
230 /// A FIELD: a well cut into a plate with a flush plate, the RUN, standing
231 /// in it, one outline round both. One object in every form a control takes
232 /// — they differ only in where the run is:
233 ///
234 /// | form | constructor | run | well |
235 /// | --- | --- | --- | --- |
236 /// | text box | [`Field::well`] | none | the whole field |
237 /// | flush control plate (dropdown, button, …) | [`Field::run`] | the whole field | none |
238 /// | text row with its picker, spinbox | [`Field::ending_in_run`] | the right end | left of it |
239 /// | toggle | [`Field::sliding_run`] | part of the field, anywhere | either side of it |
240 ///
241 /// Painted by [`PaintCtx::field`], as a [`Prim::Field`] — except a field
242 /// with no run, which is a [`Prim::Recess`]: that is what a plain well has
243 /// always been, and a recess groups into the plate under it where a field
244 /// prim never does. The rect is the field's OUTLINE (the carve's boundary;
245 /// a widget takes it through [`crate::layout::carve_inside`] from its
246 /// footprint), and `depth` the wall width.
247 #[derive(Debug, Clone, Copy, PartialEq)]
248 pub struct Field {
249 pub rect: Rect,
250 pub radii: Radii,
251 pub depth: f32,
252 /// The run's span in x, clamped to the outline; `None` for a well.
253 run: Option<(f32, f32)>,
254 /// Lights the rim — the focus and hover treatment.
255 pub tint: Option<[f32; 3]>,
256 }
257
258 impl Field {
259 /// All well, no run: a text box.
260 pub fn well(rect: Rect, radii: Radii, depth: f32) -> Self {
261 Field { rect, radii, depth, run: None, tint: None }
262 }
263
264 /// All run, no well: the edge of a flush control plate — a dropdown
265 /// trigger, a button ([`PaintCtx::inset_plate`]).
266 pub fn run(rect: Rect, radii: Radii, depth: f32) -> Self {
267 Self::spanning(rect, radii, depth, rect.x, rect.x + rect.width)
268 }
269
270 /// A well ending in a run from `split` to the field's right end: a text
271 /// row with its completion picker, a spinbox with its -/+ run.
272 pub fn ending_in_run(rect: Rect, radii: Radii, depth: f32, split: f32) -> Self {
273 Self::spanning(rect, radii, depth, split, rect.x + rect.width)
274 }
275
276 /// A run `width` wide, at `t` of its travel along the field: 0 is the
277 /// left end, 1 the right, a well either side between — a toggle, whose
278 /// run glides from off to on.
279 pub fn sliding_run(rect: Rect, radii: Radii, depth: f32, width: f32, t: f32) -> Self {
280 let width = width.clamp(0.0, rect.width);
281 let x = rect.x + t.clamp(0.0, 1.0) * (rect.width - width);
282 Self::spanning(rect, radii, depth, x, x + width)
283 }
284
285 /// A run from `a` to `b`, anywhere in the field — the general form the
286 /// others are; clamped to the outline.
287 pub fn spanning(rect: Rect, radii: Radii, depth: f32, a: f32, b: f32) -> Self {
288 let (l, r) = (rect.x, rect.x + rect.width);
289 let a = a.clamp(l, r);
290 let b = b.clamp(a, r);
291 Field { rect, radii, depth, run: Some((a, b)), tint: None }
292 }
293
294 /// The rim lit in `tint` (focus, hover), or not.
295 pub fn with_tint(mut self, tint: Option<[f32; 3]>) -> Self {
296 self.tint = tint;
297 self
298 }
299
300 /// Where the run is, `(left, right)` within the outline; `None` for a
301 /// field that is all well.
302 pub fn run_span(&self) -> Option<(f32, f32)> {
303 self.run
304 }
305
306 /// Whether any of the field is well — false for a field that is all run.
307 pub fn has_well(&self) -> bool {
308 match self.run {
309 None => true,
310 Some((a, b)) => a > self.rect.x + FIELD_REACH || b < self.rect.x + self.rect.width - FIELD_REACH,
311 }
312 }
313
314 /// The run as [`Prim::Field`] encodes it, `(split, end)`: an end that
315 /// reaches the field's is put [`FIELD_RUN_ONLY`] past it.
316 fn prim_span(&self) -> Option<(f32, f32)> {
317 let (a, b) = self.run?;
318 let (l, r) = (self.rect.x, self.rect.x + self.rect.width);
319 let split = if a <= l + FIELD_REACH { l - FIELD_RUN_ONLY } else { a };
320 let end = if b >= r - FIELD_REACH { r + FIELD_RUN_ONLY } else { b };
321 Some((split, end))
322 }
323 }
324
325 /// A control plate: the thing you press, at the control rung of the plate
326 /// ladder. One description for every control face — Button, Dropdown,
327 /// FontSelector, Breadcrumb, a ButtonStrip's selected plateau — so their
328 /// carve-inside, radius, depth and transparent-face rules cannot drift.
329 /// Painted by [`PaintCtx::control_plate`]. The root and pane rungs of the
330 /// ladder are [`PlateSpec`]; this is the same idea one rung down.
331 ///
332 /// `rect` is the plate's footprint, the OUTER edge of its silhouette; the
333 /// carve is taken inside it ([`crate::layout::carve_inside`]), so the gap
334 /// beside the plate is the gap. `radii` is the silhouette, per corner (a
335 /// Dropdown nested concentrically in a frame corner adjusts each). `face`
336 /// is the plate's own material; `None` means the surface below IS the face
337 /// (edges only), and a frosted material is a real face — the frost carried
338 /// where the stance can (`Flat`; see [`PlateStance`]). `depth` is the
339 /// relief's wall width — [`ControlPlate::control`] takes the DE relief width
340 /// capped at a fifth of the height.
341 #[derive(Debug, Clone, Copy, PartialEq)]
342 pub struct ControlPlate {
343 pub rect: Rect,
344 pub radii: Radii,
345 pub stance: PlateStance,
346 pub face: Option<Material>,
347 pub depth: f32,
348 /// The rim's light and shadow tinted this colour: the keyboard-focus
349 /// ring, drawn on the plate's own relief rather than as extra geometry.
350 /// `None` untinted.
351 pub tint: Option<[f32; 3]>,
352 }
353
354 impl ControlPlate {
355 /// A control plate at `rect` with a uniform corner `radius`: depth from
356 /// the DE relief width, capped at a fifth of the plate's height.
357 pub fn control(rect: Rect, radius: f32, stance: PlateStance, face: Option<Material>) -> Self {
358 let depth = crate::layout::bevel_width().min(rect.height * 0.2);
359 Self { rect, radii: (radius, radius, radius, radius), stance, face, depth, tint: None }
360 }
361
362
363 /// Light the rim — the focus ring on the plate's silhouette. Pass the
364 /// highlight colour while the control holds keyboard focus, `None` otherwise.
365 pub fn with_tint(mut self, tint: Option<[f32; 3]>) -> Self {
366 self.tint = tint;
367 self
368 }
369
370 /// The DE's focus-ring colour for a plate rim: the highlight accent, the
371 /// same the wells light their rims with while editing.
372 pub fn focus_tint() -> [f32; 3] {
373 let c = crate::color::highlight_primary_color();
374 [c[0], c[1], c[2]]
375 }
376
377 /// Per-corner silhouette (a concentric corner-frame adjustment).
378 pub fn with_radii(mut self, radii: Radii) -> Self {
379 self.radii = radii;
380 self
381 }
382
383 /// An explicit wall width — a plate that shares its depth with the well
384 /// it stands in, or one capped by its short side rather than its height.
385 pub fn with_depth(mut self, depth: f32) -> Self {
386 self.depth = depth;
387 self
388 }
389
390 /// The face a stance draws: `Some` only for a material with a visible
391 /// tint — a transparent one is the surface below showing through, the
392 /// same as `None`.
393 pub fn faced(&self) -> Option<&Material> {
394 self.face.as_ref().filter(|m| m.tint[3] > 0.001)
395 }
396
397 /// The face as the encoded fill the flat-path bridges consume
398 /// (`Button::inset_face`, cce-system-interface's `ControlCarve`):
399 /// transparent for no face, else the material's nested fill.
400 pub fn face_fill(&self) -> [f32; 4] {
401 self.face.map_or([0.0; 4], |m| m.fill(PlateRole::Nested))
402 }
403 }
404
405 // Call the family **relief primitives** — see `Prim`. (This note sat above `DropletSpec`,
406 // which moved to `cce_core::droplet`.)
407 // Call the family **relief primitives**, not "bevel primitives": `Bevel` is one
408 // specific member — a filled rounded rect plus a lit roll on its lip — and a
409 // groove, a fillet or a sphere is not a bevel in any sense. "Relief" is also
410 // what the rest of the stack already says: `layout::control_relief` gates the
411 // whole family, and the config node is `relief`. The name **bevel** is reserved
412 // for two things: the `Bevel` prim, and the shared *edge treatment* every
413 // relief primitive is shaded with (`bevel_width`, `bevel_depth`,
414 // `bevel_shader`, `bevel_profile` — the lit roll, not the shape).
415 pub use cce_core::droplet::DropletSpec;
416
417 /// What a droplet's material is shaded with — an extension, since [`DropletSpec`] lives in
418 /// `cce_core`, which knows no [`Finish`]. `use cce_ui::scene::paint::DropletFinish;` at a
419 /// call site that writes `spec.finish()`.
420 pub trait DropletFinish {
421 /// The drop's finish: its own gleam, shine and rim in the specular,
422 /// shininess and curvature slots of a [`Finish`] (a drop is wetter than
423 /// the DE's plates), the shading strength the DE's. The material a
424 /// droplet is emitted with carries this — `Material::from_fill(c)
425 /// .with_finish(spec.finish())` — and the tessellator reads it from
426 /// there like any plate's, instead of packing the slots by hand.
427 fn finish(&self) -> Finish;
428 }
429
430 impl DropletFinish for DropletSpec {
431 fn finish(&self) -> Finish {
432 Finish { spec: self.gleam, shininess: self.shine, curvature: self.rim, ..Finish::from_style() }
433 }
434 }
435
436 #[derive(Clone, Debug, PartialEq)]
437 pub enum Prim {
438 Quad { rect: Rect, color: [f32; 4] },
439 RoundedRect { rect: Rect, radius: f32, corners: (bool, bool, bool, bool), color: [f32; 4] },
440 /// A rounded fill plus a solid border stroke — a widget's own "plate"
441 /// (`append_widget_plate`'s non-bevel branch).
442 Border { rect: Rect, radii: Radii, fill: [f32; 4], border: [f32; 4], thickness: f32 },
443 /// A beveled plate: a rounded fill at full size plus a light/shadow overlay lip
444 /// (`append_widget_plate`'s bevel branch). `tint` colours the
445 /// roll's light and shadow — neutral white normally; a host sets it to a
446 /// highlight color to mark the plate (the focused-pane treatment) without a
447 /// separate border ring: the light goes to the tint, the shadow to a dark
448 /// tint, so the relief still reads. Shader-plates path only; the legacy
449 /// banded tessellation ignores it.
450 Bevel { rect: Rect, radii: Radii, material: Material, depth: f32, tint: [f32; 3] },
451 /// A [`Prim::Bevel`] turned inside out: the plate's face is everything in
452 /// `rect` OUTSIDE `hole`, and its rolled edge runs round the hole's
453 /// outline, falling INTO the hole. So a corner of the hole is an inside
454 /// corner of the plate — a cove, rounded at the hole's radius in the DE's
455 /// corner family — which a box can only round convex. A band of a
456 /// window's edge with an opening above it (the designer's playbar shelf):
457 /// its top edge and both coves are ONE outline, one profile evaluation,
458 /// with no join to stack two shadings at. `rect` bounds the face (its
459 /// own edges are not rolled — lay them past the window or under
460 /// something), and it is a carve host like a Bevel: carves inside `rect`
461 /// group into it. Shader-plates path; the legacy path fills `rect`.
462 Frame { rect: Rect, hole: Rect, hole_radii: Radii, material: Material, depth: f32 },
463 /// A recess carved into whatever is already painted underneath — the inverse of
464 /// `Bevel`. Emits ONLY the shaded edges, never a fill, so the surface below shows
465 /// through the middle: a relief cut into the root plate rather than a plate laid on
466 /// top of it. The light vector is negated relative to `Bevel`, so the edges facing
467 /// `light_source_position` fall into shadow and the far edges catch the light —
468 /// which is what reads as "lower" instead of "raised".
469 ///
470 /// The shading is a translucent light/shadow overlay, so the carve needs no knowledge
471 /// of what it carves: fills, gradients, and translucency below all show through
472 /// modulated rather than repainted.
473 /// `edges` is (top, right, bottom, left): which walls of the carve actually exist.
474 /// A region flush with the plate's own edge is a step, not a trough — see
475 /// `push_bevel_edge_vertices_banded`.
476 /// `tint` colours the wall's light and shadow — the same focused-pane
477 /// treatment as [`Prim::Bevel`]'s tint, for carved wells instead of raised
478 /// plates. A tinted
479 /// recess never groups into a host plate's CSG features (a feature carries no
480 /// color), so it always renders as the free-carve overlay. Shader-plates path
481 /// only; the legacy banded tessellation ignores it.
482 Recess { rect: Rect, radii: Radii, depth: f32, edges: (bool, bool, bool, bool), tint: Option<[f32; 3]> },
483 /// The inverse of [`Prim::Recess`]: a plateau RAISED out of the surface below.
484 /// Like `Recess` it emits only the shaded edges, never a fill — the face is the
485 /// untouched surface underneath — so a region outlined by raised rolled bumps
486 /// keeps the root plate's own color and translucency. Same wall semantics as
487 /// `Recess` (`edges` = top/right/bottom/left); the lighting is the raised sign,
488 /// so the edges facing `light_source_position` catch the light. `tint` colors
489 /// the lit rim like [`Prim::Recess`]'s — the focused-pane treatment for a
490 /// rim-only pane (a fill-less surface can't carry [`Prim::Bevel`]'s tint).
491 /// Like a tinted recess it never groups into a host plate's CSG features.
492 Boss { rect: Rect, radii: Radii, depth: f32, edges: (bool, bool, bool, bool), tint: Option<[f32; 3]> },
493 /// A raised RIM riding the rect's boundary: a bump profile straddling the
494 /// outline (span ±depth/2), rising from the surrounding surface to a crest on
495 /// the boundary and falling back to the same level inside — an elevated border
496 /// around a channel, both faces at the underlying surface's own level. One
497 /// primitive, ONE lighting evaluation per pixel: building the same shape from
498 /// a Boss plus an inset Recess stacks two shading passes (double specular /
499 /// shoulder terms at the crest) and reads far hotter than a plate edge.
500 Ridge { rect: Rect, radii: Radii, depth: f32, edges: (bool, bool, bool, bool) },
501 /// The sunken twin of [`Prim::Ridge`]: a VALLEY riding the rect's boundary —
502 /// a bump profile straddling the outline (span ±depth/2), falling from the
503 /// surrounding surface to a trough on the boundary and rising back to the
504 /// same level inside, so both faces sit at the underlying surface's own
505 /// level. This is the seam a flush inset control leaves ([`PaintCtx::inset_plate`]).
506 ///
507 /// Same reason to exist as `Ridge`, measured: building this from a `Recess`
508 /// on an outset rect plus a `Boss` on the rect (what `inset_plate` used to
509 /// emit) stacks two independent shading passes. At depth 4.8 that read as a
510 /// band 15px wide instead of 8 with THREE lobes — bright, dark, brighter —
511 /// because the recess ring's own lit rim lands ~depth outside the control
512 /// instead of merging into one wall, and the highlight peaked 22% hotter
513 /// than a single evaluation of the same depth. It looked like two concentric
514 /// rings, which is what it was.
515 ///
516 /// `edges` and the host-box fade behave exactly as [`Prim::Recess`]'s.
517 /// SDF path only; the legacy banded tessellation approximates it with the
518 /// old two-step stack (like `Ridge`, which approximates itself there).
519 ///
520 /// `tint` lights the rim like [`Prim::Recess`]'s — the focus treatment of a
521 /// flush control plate (`PaintCtx::control_plate`).
522 Trough { rect: Rect, radii: Radii, depth: f32, edges: (bool, bool, bool, bool), tint: Option<[f32; 3]> },
523 /// A sunken well holding a FLUSH run: one field, one outer contour.
524 /// Between `split` and `end` (xs in the same space as `rect`) the
525 /// interior is back at the surface's level, as a flush control plate's
526 /// face, inside a valley on the outline; on either side of that it is a
527 /// [`Prim::Recess`] — the interior one step down. A run that reaches an
528 /// end of the field (`split` left of it, or `end` right of it — by
529 /// [`FIELD_RUN_ONLY`]) has no well on that side. The forms in use: a
530 /// parameter pane's text row with its completion picker and a spinbox's
531 /// value with its -/+ run (the run at the right end), a flush control
532 /// plate (all run), a toggle (a run half the field wide, at the left end
533 /// off and the right end on, gliding between).
534 ///
535 /// Why one prim and not the two it replaces, side by side: each of those
536 /// shades its OWN box, so at the seam the outline breaks — each box
537 /// turns its own square corner there, and the strong line of the edge
538 /// jumps from the recess's outer rim to the trough's inner lip. Here the
539 /// outline is evaluated once (the whole field, its own radii), its wall
540 /// blends from the step to the valley across a wall's width about the
541 /// seam — the two agree on the outer half, falling from the surface to
542 /// half the step, and part on the inner half — and the seam is the
543 /// well's floor rising to the run's face: a step wall along `split`,
544 /// fading to nothing where it meets the outer wall, the one place the
545 /// two sides stand at the same height.
546 ///
547 /// SDF path only; the legacy banded tessellation and flat hosts draw the
548 /// two-box form it replaced. `tint` lights it like a recess's.
549 Field { rect: Rect, radii: Radii, depth: f32, split: f32, end: f32, tint: Option<[f32; 3]> },
550 /// The window's glass slab: a rounded fill plus a rolled, lit edge around its whole
551 /// perimeter, drawn at full size. Distinct from `Bevel`, which insets its fill by
552 /// `depth` — a plate must fill the window exactly, or the compositor's rounded window
553 /// corners would show a gap. `depth` is the width of the roll-off in px, not a color
554 /// offset (the shading amplitude is the DE-wide `bevel_depth`).
555 ///
556 /// `shape` overrides the DE-wide corner exponent (`layout::corner_shape`)
557 /// for this one plate — `Some(2.0)` is circular arcs, so a plate whose
558 /// radii reach its half-extent is a true circle regardless of the
559 /// squircle the rest of the DE wears. `None` follows the DE.
560 Plate { rect: Rect, radii: Radii, material: Material, depth: f32, shape: Option<f32> },
561 Arc { cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4] },
562 /// A ring band with radial color interpolation — inner rim → crest
563 /// (centerline) → outer rim — for rounded rim bevels (the Ramp's key
564 /// rings). `radius` is the stroke's outer edge, like `Arc`.
565 ArcShaded { cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, inner: [f32; 4], crest: [f32; 4], outer: [f32; 4] },
566 Vector { x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, color: [f32; 4], cap: Cap },
567 Circle { cx: f32, cy: f32, radius: f32, color: [f32; 4] },
568 /// A feathered aura around (and over) a rounded rect: the interior fills
569 /// at the color's full alpha, and outside the boundary the alpha falls
570 /// off smoothly to zero across `reach` px. Tessellated as concentric
571 /// per-vertex-alpha rings the GPU interpolates, so the gradient is
572 /// per-pixel smooth — no stacked-layer banding. Highlights and soft
573 /// focus auras (the designer's drop-target glow) are the intended use;
574 /// no relief shading, no light involvement.
575 Glow { rect: Rect, radius: f32, reach: f32, color: [f32; 4] },
576 /// A `Circle` lit as a ball: the disc is shaded per pixel as a hemisphere
577 /// under the DE's plate light (same ambient/diffuse/specular model), so it
578 /// reads as a sphere sitting on the surface — the slider thumb's look. The
579 /// color is the sphere's face color exactly at the lit center, like a
580 /// plate's face keeps the app's color. Falls back to a flat circle on the
581 /// legacy (`bevel_shader 0`) path.
582 Sphere { cx: f32, cy: f32, radius: f32, material: Material },
583 /// A hanging water droplet clinging to the TOP edge of `rect`, lit per pixel
584 /// by shader mode 10: the silhouette is a smooth union of a film "sheet"
585 /// attached to the top edge (square top corners — the attach line) and a
586 /// belly capsule resting on the rect's bottom, blended metaball-style so a
587 /// waist forms where the sides pull up. Shaded as a glass dome under the
588 /// DE's plate light — same ambient/diffuse and decoupled specular as the
589 /// plates, plus a fresnel rim crest and a thin-edge clarity falloff (tint
590 /// opacity drops toward the silhouette, so the frosted backdrop shows
591 /// through clearer at the rim, which is what reads as water rather than
592 /// plastic). Shape knobs in [`DropletSpec`]. On the legacy (`bevel_shader
593 /// 0`) path it degrades to the flat hanging capsule — square top, round
594 /// bottom — rather than vanishing.
595 Droplet { rect: Rect, material: Material, spec: DropletSpec },
596 /// The same silhouette as [`Prim::Droplet`] under the same [`DropletSpec`],
597 /// filled FLAT and feathered inward: opaque through the interior, fading
598 /// to nothing over `feather` px as it approaches the drop's edge. A
599 /// vignette shaped exactly like the drop, for grounding text drawn on top
600 /// of one — not a second lit body, so it carries no dome, rim, gleam or
601 /// contact shadow.
602 ///
603 /// It shares the droplet's shader path rather than approximating the
604 /// outline with a rounded rect, so the two can never disagree about where
605 /// the drop's edge is. On the legacy (`bevel_shader 0`) path it degrades
606 /// to the same flat rounded-rect outline `Prim::Droplet` falls back to.
607 DropletScrim { rect: Rect, material: Material, spec: DropletSpec, feather: f32 },
608 /// A concave inside-corner fillet for composed carves: a quarter-arc wall
609 /// whose centre `(cx, cy)` sits out in the corner's pocket, shaded with the
610 /// same step profile as a `Recess`/`Boss` wall (`raised` flips the sign).
611 /// `start` is the wedge's start angle (quarter span, hard-cut at the
612 /// tangent lines — the neighboring straight walls continue the profile
613 /// exactly there). Box radii can only round convex corners; this is the
614 /// missing concave piece. SDF path only (no legacy fallback).
615 ConcaveFillet { cx: f32, cy: f32, radius: f32, depth: f32, start: f32, raised: bool },
616 /// An engraved line: a groove of half-width `width / 2` running along the
617 /// segment `a`–`b`, cut into whatever is painted beneath. Like [`Prim::Recess`]
618 /// it emits only shading, never a fill — but its shape is a SLAB (a band about
619 /// an arbitrary line) rather than a box, which is what lets it run at an angle.
620 /// A box SDF can only carve axis-aligned walls; this is the diagonal case.
621 ///
622 /// Both walls come from ONE profile evaluation on `|distance to the line|`, so
623 /// the groove carries a single specular/shoulder term — the same reason
624 /// [`Prim::Ridge`] exists instead of stacking a boss on a recess.
625 /// `width` 0 makes the two walls meet in a V.
626 ///
627 /// `depth` is the transition width in px (the wall's run), matching
628 /// [`Prim::Recess`]. `host` is the surface the groove is engraved into: the
629 /// shading fades out across that box's perimeter roll, so a seam cut across a
630 /// plate dies into the plate's own rolled edge instead of ending on a hard line.
631 /// SDF path only — the legacy banded tessellation draws nothing (like `Ridge`).
632 ///
633 /// `strength` scales the groove's shading, specular and AO — 1.0 is the
634 /// DE's finish, 0.0 no groove at all — which is how a carve with no colour
635 /// of its own fades ([`Prim::faded`]).
636 Groove { a: (f32, f32), b: (f32, f32), width: f32, depth: f32, host: Rect, strength: f32 },
637 /// A periodic field of identical rounded-box wells — every cell of a grid
638 /// carved into whatever is painted beneath, as ONE surface. The wells
639 /// repeat every `period` (x, y) with one cell centred at `origin`, each
640 /// `cell` (w, h) big with `radius` corners; the wall runs from the cell
641 /// edge OUTWARD over `depth` px (floor at the edge, plateau one run out),
642 /// so a rail between two cells carries one wall from each side and the
643 /// rail face is whatever the runs leave. Shading lands only inside `rect`.
644 /// The wall's outer edge is MITRED, not offset: it is the cell grown by
645 /// the run at the same `radius`, so a crossing keeps the cell's corner
646 /// rounding instead of sweeping at `radius + depth`, and the four walls
647 /// meet on the diagonals.
648 ///
649 /// This exists because a lattice drawn as one [`Prim::Recess`] per cell is
650 /// N independent overlays: where four rounded rings meet at a crossing
651 /// their shadings stack in colour space and read as overlapping effects,
652 /// not a junction. Here the pixel is folded into the period and the
653 /// distance is to the NEAREST cell — the union of every well — evaluated
654 /// once, so the rail centre lines and the diagonals at each crossing are
655 /// true mitres, and the cost is one draw regardless of how many cells the
656 /// surface holds (a free carve per cell also runs into the per-frame
657 /// feature budget long before a zoomed-out grid does). SDF path only.
658 Lattice { rect: Rect, period: (f32, f32), origin: (f32, f32), cell: (f32, f32), radius: f32, depth: f32 },
659 /// `color`, flat, everywhere inside `rect` that is OUTSIDE a periodic
660 /// field of rounded cells — the same field [`Prim::Lattice`] carves
661 /// (`period`, one cell centred at `origin`, each `cell` big with `radius`
662 /// corners), painted as grout rather than shaded. One draw for the whole
663 /// grid, with the cells' superellipse corners exact: what a graph's grid
664 /// lines are when the cells are the surface beneath showing through.
665 /// SDF path only — the legacy banded tessellation draws nothing.
666 Grout { rect: Rect, period: (f32, f32), origin: (f32, f32), cell: (f32, f32), radius: f32, color: [f32; 4] },
667 /// A flat fill of a MATERIAL: `rect` at `radii`, no roll, no rim — the
668 /// material's tint, frosted at the material's own recipe when it is
669 /// frosted. What a frosted `RoundedRect` promotes to, except that the
670 /// recipe is the material's rather than the DE default's, so a fill can
671 /// compress harder (or softer) than the pane it sits on. Opens no carve
672 /// host: carves emitted after it overlay it, as they overlay any flat
673 /// geometry. An opaque material draws as a plain rounded fill.
674 Fill { rect: Rect, radii: Radii, material: Material },
675 /// Several rounded boxes carved (`raised` false) or raised (`raised`
676 /// true) as ONE shape: the union of the boxes is the well, and its wall
677 /// follows the union's outline — straddling it by ±`depth`/2 like every
678 /// carve boundary — through a single profile evaluation per pixel. An L,
679 /// a T, a plus, a slot with a round end: any outline boxes can compose.
680 ///
681 /// The alternative, one [`Prim::Recess`] per box, is N overlays that
682 /// each shade their own full outline: where two boxes overlap, each
683 /// draws a wall straight through the other's interior, and where their
684 /// walls cross the shadings stack in colour space — the junction reads
685 /// as two effects laid over each other, not one shape. Here the pixel's
686 /// distance is to the NEAREST box (the union SDF), so a box's wall
687 /// vanishes wherever it runs inside another, and an inside corner is a
688 /// sharp mitre (round it with [`Prim::ConcaveFillet`] if it must be
689 /// concave-rounded — the union has no radius there by construction).
690 /// Outer corners are mitred like [`Prim::Lattice`]'s: the wall band runs
691 /// between each box shrunk and grown by half the run at the box's own
692 /// radius, so a corner keeps its radius instead of sweeping wider.
693 ///
694 /// The boxes ride the frame's plate-feature buffer (the same slots CSG
695 /// carves use, 64 per frame), so a union costs one draw plus one slot
696 /// per box. When the budget cannot hold all of a union's boxes the
697 /// tessellator keeps as many as fit — a degraded shape rather than none —
698 /// and says so under `CCE_PLATE_DEBUG`. SDF path only.
699 CarveUnion { boxes: Vec<(Rect, Radii)>, depth: f32, raised: bool },
700 /// Text in sRGB u8 (the `TextLabel` convention). `font` is a font string for
701 /// `get_text_buffer` (family, or "family:size"); `bounds` is a logical `[l, t, r, b]` clip
702 /// for the glyph pass (Phase 6: the backend renders these through the glyph pass when the app
703 /// opts in via `Application::display_list_text`; the paint walk's clip additionally
704 /// applies through the item's `clip`). `attrs` carries the optional shaping attributes
705 /// beyond family+size (the font picker's italic/weight preview variants). `layout`, when
706 /// `Some`, requests box layout — word-wrap at a width and horizontal/vertical alignment
707 /// within a box (the placed-text-box case, e.g. cce-layout-interface's canvas elements);
708 /// `None` is the ordinary single-run label. `alpha` fades the glyphs (1.0 = opaque) —
709 /// the color stays sRGB u8, so translucent text doesn't need a color-type change.
710 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> },
711 /// A user image (id from `cce_ui::vk::upload_rgba`) drawn as a quad, in
712 /// display-list order like any other primitive. The paint walk's clip
713 /// applies through the item's `clip` as usual.
714 Image { image: u32, rect: Rect, alpha: f32 },
715 }
716
717 impl Prim {
718 /// This prim at `alpha` of its strength (0..1): a colour's alpha scaled,
719 /// a text's or an image's alpha, a groove's shading. What a host fading a
720 /// part of its drawing out or in replays it through (the context menu's
721 /// page turn). The relief prims with no colour or strength of their own —
722 /// the walls, plates and materials — come back as they are: they are
723 /// drawn whole or not at all.
724 pub fn faded(self, alpha: f32) -> Prim {
725 let a = alpha.clamp(0.0, 1.0);
726 let f = |c: [f32; 4]| [c[0], c[1], c[2], c[3] * a];
727 match self {
728 Prim::Quad { rect, color } => Prim::Quad { rect, color: f(color) },
729 Prim::RoundedRect { rect, radius, corners, color } => Prim::RoundedRect { rect, radius, corners, color: f(color) },
730 Prim::Border { rect, radii, fill, border, thickness } => Prim::Border { rect, radii, fill: f(fill), border: f(border), thickness },
731 Prim::Arc { cx, cy, radius, thickness, start, end, color } => Prim::Arc { cx, cy, radius, thickness, start, end, color: f(color) },
732 Prim::ArcShaded { cx, cy, radius, thickness, start, end, inner, crest, outer } => {
733 Prim::ArcShaded { cx, cy, radius, thickness, start, end, inner: f(inner), crest: f(crest), outer: f(outer) }
734 }
735 Prim::Vector { x1, y1, x2, y2, thickness, color, cap } => Prim::Vector { x1, y1, x2, y2, thickness, color: f(color), cap },
736 Prim::Circle { cx, cy, radius, color } => Prim::Circle { cx, cy, radius, color: f(color) },
737 Prim::Glow { rect, radius, reach, color } => Prim::Glow { rect, radius, reach, color: f(color) },
738 Prim::Grout { rect, period, origin, cell, radius, color } => Prim::Grout { rect, period, origin, cell, radius, color: f(color) },
739 Prim::Groove { a: p, b, width, depth, host, strength } => Prim::Groove { a: p, b, width, depth, host, strength: strength * a },
740 Prim::Text { text, x, y, font_size, color, alpha, font, bounds, attrs, layout } => {
741 Prim::Text { text, x, y, font_size, color, alpha: alpha * a, font, bounds, attrs, layout }
742 }
743 Prim::Image { image, rect, alpha } => Prim::Image { image, rect, alpha: alpha * a },
744 other => other,
745 }
746 }
747 }
748
749 /// Horizontal alignment of laid-out (boxed) text — the toolkit-plain mirror of
750 /// `cosmic_text::Align`, mapped at shape time.
751 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
752 pub enum AlignH {
753 #[default]
754 Left,
755 Center,
756 Right,
757 }
758
759 /// Vertical alignment of laid-out text within its box.
760 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
761 pub enum AlignV {
762 #[default]
763 Top,
764 Middle,
765 Bottom,
766 }
767
768 /// Box layout for a [`Prim::Text`]: word-wrap width (`Some` ⇒ multiline wrap; `None` ⇒ single
769 /// run) and horizontal/vertical alignment within a box of `box_height`. All lengths are logical.
770 /// The backend shapes it with `get_text_buffer_laid_out`, cached under the box as well as the
771 /// text, and applies the vertical offset from the shaped height.
772 #[derive(Clone, Copy, Debug, PartialEq)]
773 pub struct TextLayout {
774 pub wrap_width: Option<f32>,
775 pub box_height: f32,
776 pub align_h: AlignH,
777 pub align_v: AlignV,
778 }
779
780 /// Optional shaping attributes for a [`Prim::Text`] — the subset a widget can request beyond
781 /// family + size. `weight` is the OpenType weight (400 regular, 700 bold); `None` leaves the
782 /// family default. `stretch` is the OpenType width class (`usWidthClass`, 1 ultra-condensed
783 /// … 5 normal … 9 ultra-expanded); `None` is normal width — what picks a family's Narrow or
784 /// Condensed cut over its normal-width sibling at the same weight. Kept toolkit-plain (no
785 /// cosmic-text types) like the rest of the scene layer; the backend maps them onto
786 /// `cosmic_text::Style`/`Weight`/`Stretch` at shape time.
787 ///
788 /// Build one with `..Default::default()` after the fields you set, so a field added here
789 /// does not break every literal in the sibling crates.
790 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
791 pub struct TextAttrs {
792 pub italic: bool,
793 pub weight: Option<u16>,
794 pub stretch: Option<u16>,
795 }
796
797 /// A primitive plus the scissor rect it must be clipped to (`None` = unclipped), an
798 /// optional circular clip `[cx, cy, r]` in logical pixels (`None` = unclipped) — the
799 /// per-vertex circle clip the tessellators already support, for round panes (the designer's
800 /// circular network pane) — and an optional rounded-rect clip `[cx, cy, bx, by, r]`
801 /// (center, SDF half-extents = half-size minus radius, corner radius; logical px) so a
802 /// plate's children cut off at its rounded corners. The clips compose: the scissor is GPU
803 /// state, the circle rides the vertices, the rounded rect is per-draw-batch state.
804 #[derive(Clone, Debug, PartialEq)]
805 pub struct PaintItem {
806 pub prim: Prim,
807 pub clip: Option<Rect>,
808 pub clip_circle: Option<[f32; 3]>,
809 pub clip_rrect: Option<[f32; 5]>,
810 }
811
812 /// An ordered list of clipped primitives — the single source of truth for a frame's geometry.
813 #[derive(Clone, Debug, Default, PartialEq)]
814 pub struct DisplayList {
815 pub items: Vec<PaintItem>,
816 }
817
818 impl DisplayList {
819 pub fn new() -> Self {
820 DisplayList { items: Vec::new() }
821 }
822 pub fn len(&self) -> usize {
823 self.items.len()
824 }
825 pub fn is_empty(&self) -> bool {
826 self.items.is_empty()
827 }
828 }
829
830 /// Intersection of two rects, clamped so width/height never go negative (an empty clip is a
831 /// zero-size rect — nothing draws under it).
832 fn intersect(a: Rect, b: Rect) -> Rect {
833 let x0 = a.x.max(b.x);
834 let y0 = a.y.max(b.y);
835 let x1 = (a.x + a.width).min(b.x + b.width);
836 let y1 = (a.y + a.height).min(b.y + b.height);
837 Rect { x: x0, y: y0, width: (x1 - x0).max(0.0), height: (y1 - y0).max(0.0) }
838 }
839
840 /// Accumulates a [`DisplayList`] while a paint walk pushes/pops clips and translations.
841 ///
842 /// Coordinates passed to the emit methods (and to [`push_clip`](PaintCtx::push_clip)) are in the
843 /// **current** local space; the active translation is applied so everything recorded is absolute.
844 pub struct PaintCtx {
845 list: DisplayList,
846 /// Each entry is the effective (already-intersected, absolute) clip at that depth.
847 clip_stack: Vec<Rect>,
848 /// Active circular clips; primitives record the innermost (`last`). Circles don't
849 /// intersect analytically like rects, so nesting keeps the innermost only.
850 clip_circle_stack: Vec<[f32; 3]>,
851 /// Active rounded-rect clips `[cx, cy, bx, by, r]`; innermost wins, like circles.
852 clip_rrect_stack: Vec<[f32; 5]>,
853 /// Saved offsets for nesting; `offset` is the current cumulative translation.
854 offset_stack: Vec<(f32, f32)>,
855 offset: (f32, f32),
856 }
857
858 impl Default for PaintCtx {
859 fn default() -> Self {
860 Self::new()
861 }
862 }
863
864 impl PaintCtx {
865 pub fn new() -> Self {
866 PaintCtx {
867 list: DisplayList::new(),
868 clip_stack: Vec::new(),
869 clip_circle_stack: Vec::new(),
870 clip_rrect_stack: Vec::new(),
871 offset_stack: Vec::new(),
872 offset: (0.0, 0.0),
873 }
874 }
875
876 /// The scissor rect primitives are currently recorded under.
877 pub fn current_clip(&self) -> Option<Rect> {
878 self.clip_stack.last().copied()
879 }
880
881 /// Push a clip (in current local space); it is translated to absolute and intersected with the
882 /// enclosing clip. Pair with [`pop_clip`](PaintCtx::pop_clip), or prefer [`clip`](PaintCtx::clip).
883 pub fn push_clip(&mut self, rect: Rect) {
884 let r = self.apply_offset(rect);
885 let effective = match self.clip_stack.last() {
886 Some(cur) => intersect(*cur, r),
887 None => r,
888 };
889 self.clip_stack.push(effective);
890 }
891
892 pub fn pop_clip(&mut self) {
893 self.clip_stack.pop();
894 }
895
896 /// Run `f` with `rect` pushed as a clip, popping it afterward.
897 pub fn clip<R>(&mut self, rect: Rect, f: impl FnOnce(&mut Self) -> R) -> R {
898 self.push_clip(rect);
899 let out = f(self);
900 self.pop_clip();
901 out
902 }
903
904 /// Push a circular clip `[cx, cy, r]` (current local space, translated to absolute).
905 /// Primitives emitted while it is active record it and tessellate with the per-vertex
906 /// circle clip. Pair with [`pop_clip_circle`](PaintCtx::pop_clip_circle), or prefer
907 /// [`clip_circle`](PaintCtx::clip_circle).
908 pub fn push_clip_circle(&mut self, c: [f32; 3]) {
909 self.clip_circle_stack.push([c[0] + self.offset.0, c[1] + self.offset.1, c[2]]);
910 }
911
912 pub fn pop_clip_circle(&mut self) {
913 self.clip_circle_stack.pop();
914 }
915
916 /// Run `f` with `[cx, cy, r]` pushed as a circular clip, popping it afterward.
917 pub fn clip_circle<R>(&mut self, c: [f32; 3], f: impl FnOnce(&mut Self) -> R) -> R {
918 self.push_clip_circle(c);
919 let out = f(self);
920 self.pop_clip_circle();
921 out
922 }
923
924 /// Push a rounded-rect clip: `rect` (current local space) with corner radius `radius`,
925 /// so children of a rounded plate cut off at its corners. Pushes the rect as a scissor
926 /// too — the scissor handles the straight edges (and keeps batching), the SDF trims the
927 /// corners. A radius of zero degenerates to the plain rect clip. Pair with
928 /// [`pop_clip_rounded`](PaintCtx::pop_clip_rounded), or prefer
929 /// [`clip_rounded`](PaintCtx::clip_rounded).
930 pub fn push_clip_rounded(&mut self, rect: Rect, radius: f32) {
931 self.push_clip(rect);
932 let r = radius.max(0.0);
933 if r > 0.0 {
934 let abs = self.apply_offset(rect);
935 self.clip_rrect_stack.push([
936 abs.x + abs.width / 2.0,
937 abs.y + abs.height / 2.0,
938 (abs.width / 2.0 - r).max(0.0),
939 (abs.height / 2.0 - r).max(0.0),
940 r,
941 ]);
942 } else {
943 // Keep push/pop balanced regardless of radius.
944 self.clip_rrect_stack.push([0.0; 5]);
945 }
946 }
947
948 pub fn pop_clip_rounded(&mut self) {
949 self.clip_rrect_stack.pop();
950 self.pop_clip();
951 }
952
953 /// Run `f` with `rect` (radius `radius`) pushed as a rounded clip, popping it afterward.
954 pub fn clip_rounded<R>(&mut self, rect: Rect, radius: f32, f: impl FnOnce(&mut Self) -> R) -> R {
955 self.push_clip_rounded(rect, radius);
956 let out = f(self);
957 self.pop_clip_rounded();
958 out
959 }
960
961 /// Run `f` with an additional translation applied to all emitted coordinates.
962 /// Imperative translate pair for spans too large to wrap in
963 /// [`translate`](Self::translate)'s closure (an app bracketing its whole
964 /// frame in the overflow-margin shift). Must balance before `finish`.
965 pub fn push_translate(&mut self, dx: f32, dy: f32) {
966 self.offset_stack.push(self.offset);
967 self.offset.0 += dx;
968 self.offset.1 += dy;
969 }
970
971 /// See [`push_translate`](Self::push_translate).
972 pub fn pop_translate(&mut self) {
973 self.offset = self.offset_stack.pop().expect("translate stack underflow");
974 }
975
976 pub fn translate<R>(&mut self, dx: f32, dy: f32, f: impl FnOnce(&mut Self) -> R) -> R {
977 self.offset_stack.push(self.offset);
978 self.offset = (self.offset.0 + dx, self.offset.1 + dy);
979 let out = f(self);
980 self.offset = self.offset_stack.pop().expect("translate stack underflow");
981 out
982 }
983
984 /// The translation applied to what is emitted now: a widget's own rect
985 /// plus this is where it lands in the window.
986 pub fn offset(&self) -> (f32, f32) {
987 self.offset
988 }
989
990 fn apply_offset(&self, r: Rect) -> Rect {
991 Rect { x: r.x + self.offset.0, y: r.y + self.offset.1, width: r.width, height: r.height }
992 }
993
994 fn push(&mut self, prim: Prim) {
995 let clip = self.current_clip();
996 let clip_circle = self.clip_circle_stack.last().copied();
997 // r == 0 entries are balance placeholders (a zero-radius rounded clip is just its
998 // scissor rect) — record no rounded clip so batches keep merging.
999 let clip_rrect = self.clip_rrect_stack.last().copied().filter(|c| c[4] > 0.0);
1000 self.list.items.push(PaintItem { prim, clip, clip_circle, clip_rrect });
1001 }
1002
1003 /// Append items another context painted — a nested paint walk spliced into this list,
1004 /// as a host does that keeps a walk's text for a pass of its own. Each item keeps its own
1005 /// clips and is clipped by this context's current scissor too; its own circular or
1006 /// rounded clip wins over the current one (the innermost, as when pushing). Items are
1007 /// absolute already, so this context's offset does not apply.
1008 pub fn append_items(&mut self, items: impl IntoIterator<Item = PaintItem>) {
1009 let cur = self.current_clip();
1010 let cur_circle = self.clip_circle_stack.last().copied();
1011 let cur_rrect = self.clip_rrect_stack.last().copied().filter(|c| c[4] > 0.0);
1012 for mut item in items {
1013 item.clip = match (cur, item.clip) {
1014 (Some(a), Some(b)) => Some(intersect(a, b)),
1015 (a, b) => a.or(b),
1016 };
1017 item.clip_circle = item.clip_circle.or(cur_circle);
1018 item.clip_rrect = item.clip_rrect.or(cur_rrect);
1019 self.list.items.push(item);
1020 }
1021 }
1022
1023 pub fn quad(&mut self, rect: Rect, color: [f32; 4]) {
1024 let rect = self.apply_offset(rect);
1025 self.push(Prim::Quad { rect, color });
1026 }
1027
1028 /// A user image (id from `cce_ui::vk::upload_rgba`) drawn at `rect`.
1029 pub fn image(&mut self, image: u32, rect: Rect, alpha: f32) {
1030 let rect = self.apply_offset(rect);
1031 self.push(Prim::Image { image, rect, alpha });
1032 }
1033
1034 /// A bundled cce-icons glyph (`cce-icons/svg/<name>.svg`) drawn at
1035 /// `rect`, tinted `color` — given as a text colour is, raw sRGB, so a
1036 /// glyph and the label beside it match — with the colour's alpha as the
1037 /// image's. Rasterized at twice the rect's longer side so it stays crisp
1038 /// on a 2x output, and cached (see [`crate::upload_icon_tinted`]).
1039 /// `false`, and nothing drawn, when the glyph is missing.
1040 ///
1041 /// The ONE way the toolkit draws a symbol: a chevron, a mark, a + or a
1042 /// − is this, never a character in whatever face the font falls back to.
1043 /// A `weather-*` glyph carries its own colours; draw it with
1044 /// [`icon_untinted`](Self::icon_untinted).
1045 pub fn icon(&mut self, name: &str, rect: Rect, color: [f32; 4]) -> bool {
1046 let px = (rect.width.max(rect.height) * 2.0).ceil().max(1.0) as u32;
1047 match crate::upload_icon_tinted(name, px, crate::icon_tint(color)) {
1048 Some((id, _, _)) => {
1049 self.image(id, rect, color[3]);
1050 true
1051 }
1052 None => false,
1053 }
1054 }
1055
1056 /// [`icon`](Self::icon) for a glyph that carries its own colours (the
1057 /// `weather-*` family): drawn as it is, at `alpha`.
1058 pub fn icon_untinted(&mut self, name: &str, rect: Rect, alpha: f32) -> bool {
1059 let px = (rect.width.max(rect.height) * 2.0).ceil().max(1.0) as u32;
1060 match crate::upload_icon(name, px) {
1061 Some((id, _, _)) => {
1062 self.image(id, rect, alpha);
1063 true
1064 }
1065 None => false,
1066 }
1067 }
1068
1069 pub fn rounded_rect(&mut self, rect: Rect, radius: f32, corners: (bool, bool, bool, bool), color: [f32; 4]) {
1070 let rect = self.apply_offset(rect);
1071 self.push(Prim::RoundedRect { rect, radius, corners, color });
1072 }
1073
1074 /// Feathered aura over a rounded rect (see [`Prim::Glow`]): interior at
1075 /// the color's alpha, smooth per-pixel falloff to zero across `reach` px
1076 /// outside the boundary.
1077 pub fn glow(&mut self, rect: Rect, radius: f32, reach: f32, color: [f32; 4]) {
1078 let rect = self.apply_offset(rect);
1079 self.push(Prim::Glow { rect, radius, reach, color });
1080 }
1081
1082 pub fn vector(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, color: [f32; 4], cap: Cap) {
1083 let (ox, oy) = self.offset;
1084 self.push(Prim::Vector { x1: x1 + ox, y1: y1 + oy, x2: x2 + ox, y2: y2 + oy, thickness, color, cap });
1085 }
1086
1087 pub fn circle(&mut self, cx: f32, cy: f32, radius: f32, color: [f32; 4]) {
1088 let (ox, oy) = self.offset;
1089 self.push(Prim::Circle { cx: cx + ox, cy: cy + oy, radius, color });
1090 }
1091
1092 /// A sphere-lit circle — see `Prim::Sphere`.
1093 pub fn sphere(&mut self, cx: f32, cy: f32, radius: f32, material: &Material) {
1094 let (ox, oy) = self.offset;
1095 self.push(Prim::Sphere { cx: cx + ox, cy: cy + oy, radius, material: *material });
1096 }
1097
1098 /// A hanging water droplet clinging to `rect`'s top edge — see
1099 /// [`Prim::Droplet`] and [`DropletSpec`].
1100 /// See [`Prim::DropletScrim`]. `feather` is how far in from the drop's
1101 /// edge the fill reaches full opacity, in logical px.
1102 pub fn droplet_scrim(&mut self, rect: Rect, material: &Material, spec: DropletSpec, feather: f32) {
1103 let rect = self.apply_offset(rect);
1104 self.push(Prim::DropletScrim { rect, material: *material, spec, feather });
1105 }
1106
1107 /// The drop's finish is the material's (see [`DropletSpec::finish`]).
1108 pub fn droplet(&mut self, rect: Rect, material: &Material, spec: DropletSpec) {
1109 let rect = self.apply_offset(rect);
1110 self.push(Prim::Droplet { rect, material: *material, spec });
1111 }
1112
1113 /// A concave inside-corner fillet — see `Prim::ConcaveFillet`. `start` is
1114 /// the quarter wedge's start angle; the arc's centre sits in the corner's
1115 /// pocket and the wall descends (or rises, `raised`) away from it.
1116 pub fn concave_fillet(&mut self, cx: f32, cy: f32, radius: f32, depth: f32, start: f32, raised: bool) {
1117 let (ox, oy) = self.offset;
1118 self.push(Prim::ConcaveFillet { cx: cx + ox, cy: cy + oy, radius, depth, start, raised });
1119 }
1120
1121 /// Re-emit an already-built [`Prim`] through this context, so it re-records the
1122 /// current clip and translate state. This is the **single** place that has to
1123 /// learn a new `Prim` variant: a nested paint walk builds a scratch list and
1124 /// replays it into the real one, and that forwarding match used to exist
1125 /// verbatim in two crates ([`crate::widget::model`] and cce-cloud's
1126 /// `json_layout`) — adding `Prim::Groove` compiled against one and broke the
1127 /// other, caught only by a full workspace build.
1128 ///
1129 /// [`Prim::Text`] is NOT emitted: it is returned untouched, because the two
1130 /// callers disagree about it (a subtree painter authors its own text and wants
1131 /// it forwarded; everyone else drops it in favour of the widget's own label
1132 /// bridge). Every other variant is emitted and `None` comes back.
1133 #[must_use = "a returned Text prim was not emitted — drop or forward it explicitly"]
1134 pub fn replay(&mut self, prim: Prim) -> Option<Prim> {
1135 match prim {
1136 Prim::Text { .. } => return Some(prim),
1137 Prim::Quad { rect, color } => self.quad(rect, color),
1138 Prim::RoundedRect { rect, radius, corners, color } => {
1139 self.rounded_rect(rect, radius, corners, color)
1140 }
1141 Prim::Border { rect, radii, fill, border, thickness } => {
1142 self.border(rect, radii, fill, border, thickness)
1143 }
1144 Prim::Bevel { rect, radii, material, depth, tint } => {
1145 self.bevel_tinted(rect, radii, &material, depth, tint)
1146 }
1147 Prim::Frame { rect, hole, hole_radii, material, depth } => {
1148 self.frame(rect, hole, hole_radii, &material, depth)
1149 }
1150 Prim::Recess { rect, radii, depth, edges, tint } => match tint {
1151 Some(t) => self.recess_tinted(rect, radii, depth, t),
1152 None => self.recess_edges(rect, radii, depth, edges),
1153 },
1154 Prim::Boss { rect, radii, depth, edges, tint } => match tint {
1155 Some(t) => self.boss_edges_tinted(rect, radii, depth, edges, t),
1156 None => self.boss_edges(rect, radii, depth, edges),
1157 },
1158 Prim::Ridge { rect, radii, depth, edges } => self.ridge_edges(rect, radii, depth, edges),
1159 Prim::Trough { rect, radii, depth, edges, tint } => match tint {
1160 Some(t) => self.trough_tinted(rect, radii, depth, t),
1161 None => self.trough_edges(rect, radii, depth, edges),
1162 },
1163 Prim::Field { rect, radii, depth, split, end, tint } => {
1164 self.field(&Field::spanning(rect, radii, depth, split, end).with_tint(tint))
1165 }
1166 Prim::Plate { rect, radii, material, depth, shape } => {
1167 self.plate_shaped(rect, radii, &material, depth, shape)
1168 }
1169 Prim::Arc { cx, cy, radius, thickness, start, end, color } => {
1170 self.arc(cx, cy, radius, thickness, start, end, color)
1171 }
1172 Prim::ArcShaded { cx, cy, radius, thickness, start, end, inner, crest, outer } => {
1173 self.arc_shaded(cx, cy, radius, thickness, start, end, inner, crest, outer)
1174 }
1175 Prim::Vector { x1, y1, x2, y2, thickness, color, cap } => {
1176 self.vector(x1, y1, x2, y2, thickness, color, cap)
1177 }
1178 Prim::Circle { cx, cy, radius, color } => self.circle(cx, cy, radius, color),
1179 Prim::Sphere { cx, cy, radius, material } => self.sphere(cx, cy, radius, &material),
1180 Prim::Glow { rect, radius, reach, color } => self.glow(rect, radius, reach, color),
1181 Prim::Droplet { rect, material, spec } => self.droplet(rect, &material, spec),
1182 Prim::DropletScrim { rect, material, spec, feather } => {
1183 self.droplet_scrim(rect, &material, spec, feather)
1184 }
1185 Prim::ConcaveFillet { cx, cy, radius, depth, start, raised } => {
1186 self.concave_fillet(cx, cy, radius, depth, start, raised)
1187 }
1188 Prim::Groove { a, b, width, depth, host, strength } => self.groove_strength(a, b, width, depth, host, strength),
1189 Prim::Lattice { rect, period, origin, cell, radius, depth } => {
1190 self.lattice(rect, period, origin, cell, radius, depth)
1191 }
1192 Prim::Grout { rect, period, origin, cell, radius, color } => {
1193 self.grout(rect, period, origin, cell, radius, color)
1194 }
1195 Prim::Fill { rect, radii, material } => self.fill_material(rect, radii, &material),
1196 Prim::CarveUnion { boxes, depth, raised } => self.carve_union(boxes, depth, raised),
1197 Prim::Image { image, rect, alpha } => self.image(image, rect, alpha),
1198 }
1199 None
1200 }
1201
1202 /// An engraved line from `a` to `b` cut into `host` — see [`Prim::Groove`].
1203 pub fn groove(&mut self, a: (f32, f32), b: (f32, f32), width: f32, depth: f32, host: Rect) {
1204 self.groove_strength(a, b, width, depth, host, 1.0);
1205 }
1206
1207 /// [`Self::groove`] at a fraction of its strength — see [`Prim::Groove`].
1208 pub fn groove_strength(&mut self, a: (f32, f32), b: (f32, f32), width: f32, depth: f32, host: Rect, strength: f32) {
1209 let (ox, oy) = self.offset;
1210 let host = self.apply_offset(host);
1211 self.push(Prim::Groove {
1212 a: (a.0 + ox, a.1 + oy),
1213 b: (b.0 + ox, b.1 + oy),
1214 width,
1215 depth,
1216 host,
1217 strength,
1218 });
1219 }
1220
1221 /// A periodic field of rounded wells carved as one surface — see
1222 /// [`Prim::Lattice`]. `origin` is any one cell's centre; `rect` bounds the
1223 /// shading. All logical px, like every other carve.
1224 pub fn lattice(
1225 &mut self,
1226 rect: Rect,
1227 period: (f32, f32),
1228 origin: (f32, f32),
1229 cell: (f32, f32),
1230 radius: f32,
1231 depth: f32,
1232 ) {
1233 let (ox, oy) = self.offset;
1234 let rect = self.apply_offset(rect);
1235 self.push(Prim::Lattice { rect, period, origin: (origin.0 + ox, origin.1 + oy), cell, radius, depth });
1236 }
1237
1238 /// A flat fill of `material` — see [`Prim::Fill`].
1239 pub fn fill_material(&mut self, rect: Rect, radii: Radii, material: &Material) {
1240 let rect = self.apply_offset(rect);
1241 self.push(Prim::Fill { rect, radii, material: *material });
1242 }
1243
1244 /// Grout between a periodic field of rounded cells — see [`Prim::Grout`].
1245 /// `origin` is any one cell's centre; `rect` bounds the paint.
1246 pub fn grout(&mut self, rect: Rect, period: (f32, f32), origin: (f32, f32), cell: (f32, f32), radius: f32, color: [f32; 4]) {
1247 let (ox, oy) = self.offset;
1248 let rect = self.apply_offset(rect);
1249 self.push(Prim::Grout { rect, period, origin: (origin.0 + ox, origin.1 + oy), cell, radius, color });
1250 }
1251
1252 /// Carve (or raise, with `raised`) the union of `boxes` as one shape with
1253 /// one wall — see [`Prim::CarveUnion`]. `depth` is the wall's run in px,
1254 /// as for [`PaintCtx::recess`].
1255 pub fn carve_union(&mut self, boxes: Vec<(Rect, Radii)>, depth: f32, raised: bool) {
1256 let boxes: Vec<(Rect, Radii)> = boxes.into_iter().map(|(r, radii)| (self.apply_offset(r), radii)).collect();
1257 if boxes.is_empty() {
1258 return;
1259 }
1260 self.push(Prim::CarveUnion { boxes, depth, raised });
1261 }
1262
1263 pub fn border(&mut self, rect: Rect, radii: Radii, fill: [f32; 4], border: [f32; 4], thickness: f32) {
1264 let rect = self.apply_offset(rect);
1265 self.push(Prim::Border { rect, radii, fill, border, thickness });
1266 }
1267
1268 pub fn bevel(&mut self, rect: Rect, radii: Radii, material: &Material, depth: f32) {
1269 self.bevel_tinted(rect, radii, material, depth, [1.0, 1.0, 1.0]);
1270 }
1271
1272 /// A plate whose face is `rect` outside `hole` — see [`Prim::Frame`].
1273 pub fn frame(&mut self, rect: Rect, hole: Rect, hole_radii: Radii, material: &Material, depth: f32) {
1274 let rect = self.apply_offset(rect);
1275 let hole = self.apply_offset(hole);
1276 self.push(Prim::Frame { rect, hole, hole_radii, material: *material, depth });
1277 }
1278
1279 /// `bevel` with a specular tint — see `Prim::Bevel::tint`.
1280 pub fn bevel_tinted(&mut self, rect: Rect, radii: Radii, material: &Material, depth: f32, tint: [f32; 3]) {
1281 let rect = self.apply_offset(rect);
1282 self.push(Prim::Bevel { rect, radii, material: *material, depth, tint });
1283 }
1284
1285 /// Carve a recess into the already-painted surface below. Unlike `bevel`, this fills
1286 /// nothing — the shading is an overlay, so it composes over whatever was painted.
1287 pub fn recess(&mut self, rect: Rect, radii: Radii, depth: f32) {
1288 self.recess_edges(rect, radii, depth, (true, true, true, true));
1289 }
1290
1291 /// [`PaintCtx::recess`] with the lit rim tinted — see `Prim::Recess::tint`
1292 /// (the focused-well treatment).
1293 pub fn recess_tinted(&mut self, rect: Rect, radii: Radii, depth: f32, tint: [f32; 3]) {
1294 let rect = self.apply_offset(rect);
1295 self.push(Prim::Recess { rect, radii, depth, edges: (true, true, true, true), tint: Some(tint) });
1296 }
1297
1298 /// Raise a plateau out of the already-painted surface below — the inverse of
1299 /// [`PaintCtx::recess`]. Only the edges are shaded; the face stays the surface
1300 /// beneath, so the raised region inherits the root plate's color. `depth` is the
1301 /// roll width in px (pass [`crate::layout::bevel_width`] unless the widget
1302 /// needs a tighter lip).
1303 pub fn boss(&mut self, rect: Rect, radii: Radii, depth: f32) {
1304 self.boss_edges(rect, radii, depth, (true, true, true, true));
1305 }
1306
1307 /// [`PaintCtx::boss`] with only some of the walls — see `Prim::Boss`.
1308 pub fn boss_edges(
1309 &mut self,
1310 rect: Rect,
1311 radii: Radii,
1312 depth: f32,
1313 edges: (bool, bool, bool, bool),
1314 ) {
1315 let rect = self.apply_offset(rect);
1316 self.push(Prim::Boss { rect, radii, depth, edges, tint: None });
1317 }
1318
1319 /// [`PaintCtx::boss_edges`] with a specular tint on the lit rim — see
1320 /// `Prim::Boss::tint`.
1321 pub fn boss_edges_tinted(
1322 &mut self,
1323 rect: Rect,
1324 radii: Radii,
1325 depth: f32,
1326 edges: (bool, bool, bool, bool),
1327 tint: [f32; 3],
1328 ) {
1329 let rect = self.apply_offset(rect);
1330 self.push(Prim::Boss { rect, radii, depth, edges, tint: Some(tint) });
1331 }
1332
1333 /// Paint a control plate — see [`ControlPlate`]. The ONE place a control face's
1334 /// relief is composed: raised with a face is a `bevel` on the footprint;
1335 /// raised without one carves inside and raises a `boss`; flush carves
1336 /// inside and lays an `inset_plate` (trough plus face).
1337 pub fn control_plate(&mut self, plate: &ControlPlate) {
1338 match plate.stance {
1339 PlateStance::Raised => {
1340 if let Some(face) = plate.faced() {
1341 match plate.tint {
1342 Some(t) => self.bevel_tinted(plate.rect, plate.radii, face, plate.depth, t),
1343 None => self.bevel(plate.rect, plate.radii, face, plate.depth),
1344 }
1345 } else {
1346 let (plateau, radii) = crate::layout::carve_inside(plate.rect, plate.radii, plate.depth);
1347 match plate.tint {
1348 Some(t) => self.boss_edges_tinted(plateau, radii, plate.depth, (true, true, true, true), t),
1349 None => self.boss(plateau, radii, plate.depth),
1350 }
1351 }
1352 }
1353 PlateStance::Flush => {
1354 let (trough, radii) = crate::layout::carve_inside(plate.rect, plate.radii, plate.depth);
1355 match plate.tint {
1356 Some(t) => self.inset_plate_tinted(trough, radii, plate.faced(), plate.depth, t),
1357 None => self.inset_plate(trough, radii, plate.faced(), plate.depth),
1358 }
1359 }
1360 PlateStance::Flat => {
1361 if let Some(face) = plate.faced() {
1362 // A QUAD deliberately, not the `Border` the relief stances
1363 // fill through: carrying the blur-behind sentinel is half
1364 // the point of this stance, and only quads reach it.
1365 self.rounded_rect(
1366 plate.rect,
1367 plate.radii.0,
1368 (true, true, true, true),
1369 face.fill(PlateRole::Nested),
1370 );
1371 }
1372 if let Some(t) = plate.tint {
1373 // No relief, so no rim to light: the focus ring is drawn as
1374 // one, over the face and keeping the per-corner silhouette.
1375 self.border(plate.rect, plate.radii, [0.0; 4], [t[0], t[1], t[2], 1.0], 1.0);
1376 }
1377 }
1378 }
1379 }
1380
1381 /// A section's well — the settings app's union carve, the ONE shape a
1382 /// section or a [`crate::widget::Group`] is cut into the plate with: the
1383 /// `body` carved as a recess with `radii` (TL, TR, BR, BL), and when there
1384 /// is a title `tab` (flush on the body's top edge, at its left), the tab
1385 /// carved WITH it as one shape — the tab bottom-open, one piece owning the
1386 /// body's whole right run so its corners are real turns, a left piece
1387 /// carrying the left wall, the pieces extending past their interior seam by
1388 /// `depth` so the walls crossfade there instead of notching — and the
1389 /// throat's inside corner rounded by a concave fillet.
1390 pub fn section_well(&mut self, body: Rect, tab: Option<Rect>, radii: Radii, depth: f32) {
1391 let (cx, cy, cw, ch) = (body.x, body.y, body.width, body.height);
1392 let (tl, tr, br, bl) = radii;
1393 let Some(t) = tab else {
1394 self.recess_edges(body, radii, depth, (true, true, true, true));
1395 return;
1396 };
1397 let (tx, ty, tw, th) = (t.x, t.y, t.width, t.height);
1398 let rt = tl.max(tr).min(th * 0.45);
1399 let throat_r = tx + tw;
1400 // The designer's SECTION_FILLET_R.
1401 let rho = 10.0f32;
1402 let body_lr = |x_run: f32, pc: &mut Self| {
1403 pc.recess_edges(
1404 Rect { x: x_run, y: cy, width: cx + cw - x_run, height: ch },
1405 (0.0, tr, br, 0.0),
1406 depth,
1407 (true, true, true, false),
1408 );
1409 pc.recess_edges(
1410 Rect { x: cx, y: cy, width: x_run + depth - cx, height: ch },
1411 (0.0, 0.0, 0.0, bl),
1412 depth,
1413 (false, false, true, true),
1414 );
1415 };
1416 if cx + cw > throat_r + 2.0 * rho {
1417 // Filleted throat: the tab's right wall ends at the fillet's vertical
1418 // tangent, a left-only bridge carries the left wall across the span.
1419 self.recess_edges(
1420 Rect { x: tx, y: ty, width: tw, height: (cy - rho) - ty + depth },
1421 (rt, rt, 0.0, 0.0),
1422 depth,
1423 (true, true, false, true),
1424 );
1425 self.recess_edges(
1426 Rect { x: tx, y: cy - rho, width: tw, height: rho + depth },
1427 (0.0, 0.0, 0.0, 0.0),
1428 depth,
1429 (false, false, false, true),
1430 );
1431 body_lr(throat_r + rho - depth, self);
1432 self.concave_fillet(throat_r + rho, cy - rho, rho, depth, std::f32::consts::FRAC_PI_2, false);
1433 } else if cx + cw > throat_r + 0.5 {
1434 // Too narrow for the fillet: the plain square throat.
1435 self.recess_edges(
1436 Rect { x: tx, y: ty, width: tw, height: (cy - ty) + depth },
1437 (rt, rt, 0.0, 0.0),
1438 depth,
1439 (true, true, false, true),
1440 );
1441 body_lr(throat_r - depth, self);
1442 } else {
1443 // The tab spans the body: no top wall at all.
1444 self.recess_edges(
1445 Rect { x: tx, y: ty, width: tw, height: (cy - ty) + depth },
1446 (rt, rt, 0.0, 0.0),
1447 depth,
1448 (true, true, false, true),
1449 );
1450 self.recess_edges(
1451 Rect { x: cx, y: cy, width: cw, height: ch },
1452 (0.0, 0.0, br, bl),
1453 depth,
1454 (false, true, true, true),
1455 );
1456 }
1457 }
1458
1459 /// A flush inset control: `rect`'s plate sits SUNKEN into the surface with
1460 /// its face level with it — a valley seam runs the boundary, the surface
1461 /// falling into it on the way out and the control's own face rising back
1462 /// out of it inside. The face never leaves the surface plane; the seam is
1463 /// the only thing saying it is a separate part. `depth` is the full width
1464 /// of that valley, which straddles the boundary by ±depth/2.
1465 ///
1466 /// One [`Prim::Trough`] — ONE lighting evaluation. This used to emit a
1467 /// `Recess` on a rect outset by depth/2 plus a `Boss` on the rect, whose
1468 /// walls overlapped over half their width and shaded twice; see
1469 /// `Prim::Trough` for what that measured as. Do not re-expand this into its
1470 /// parts.
1471 ///
1472 /// An opaque `color` fills the face; transparent leaves the surface below
1473 /// showing through as the face.
1474 pub fn inset_plate(&mut self, rect: Rect, radii: Radii, face: Option<&Material>, depth: f32) {
1475 // A transparent material is no face either — only a visible tint
1476 // fills; a frosted one fills with the sentinel.
1477 if let Some(face) = face.filter(|m| m.tint[3] > 0.001) {
1478 // Flat fill only — the relief is the trough's, so the face must not
1479 // carry a lip of its own (that lip WAS the second wall).
1480 //
1481 // Deliberately a zero-stroke `Border` and NOT `rounded_rect`: this
1482 // fill used to be a `Bevel`, and the legacy reverse bridges
1483 // (`all_rounded_quads` and friends in `widget/model.rs`) extract
1484 // `Prim::RoundedRect` but neither `Bevel` nor `Border`. Emitting a
1485 // RoundedRect here would newly leak every raised control's face into
1486 // those getters — a change to the legacy surface that has nothing to
1487 // do with the relief. Border also keeps all four radii, which
1488 // `Prim::RoundedRect`'s single radius cannot.
1489 self.border(rect, radii, face.fill(PlateRole::Nested), [0.0; 4], 0.0);
1490 }
1491 // The edge is a field's RUN (a field with no well, its seam put
1492 // [`FIELD_RUN_ONLY`] px to the left): the outer half a well's own
1493 // fall, the inner half that fall mirrored back up to the face — so
1494 // every flush control has the edge of the run at the end of a text
1495 // row's field, and the well beside it. Until 2026-10-02 this was a
1496 // [`Prim::Trough`], whose outer half is a compressed copy of a step;
1497 // the dropdown, button, breadcrumb, font selector and menubar
1498 // triggers had been switched to the run's edge one by one the day
1499 // before, and the apps' own flush plates (the calendar's, cce-cloud's,
1500 // cce-files', the system interface's) kept the trough until here.
1501 self.field(&Field::run(rect, radii, depth));
1502 }
1503
1504 /// [`inset_plate`](Self::inset_plate) with the rim lit — the focused flush
1505 /// control plate's ring (`ControlPlate::with_tint`); the face fill as
1506 /// there, the trough tinted.
1507 pub fn inset_plate_tinted(&mut self, rect: Rect, radii: Radii, face: Option<&Material>, depth: f32, tint: [f32; 3]) {
1508 if let Some(face) = face.filter(|m| m.tint[3] > 0.001) {
1509 self.border(rect, radii, face.fill(PlateRole::Nested), [0.0; 4], 0.0);
1510 }
1511 self.field(&Field::run(rect, radii, depth).with_tint(Some(tint)));
1512 }
1513
1514 /// A canvas well's floor — the opening you look into or draw in (a
1515 /// Trackpad, a Slider2D pad, a bevel or ramp preview) — cut into `host`,
1516 /// the material of the plate it sits on (`Material::pane()` for a pane).
1517 /// `lifted` is a clickable canvas's hover cue: the floor rises toward
1518 /// the plate.
1519 ///
1520 /// An opaque host's floor is that plate darkened, drawn as the darkening
1521 /// itself ([`crate::colors::WELL_FLOOR`] over whatever the plate resolved
1522 /// to — exact at any plate alpha, and what every floor drew before
1523 /// materials). A FROSTED host's floor is deeper glass
1524 /// ([`Material::floor`]: the host's material with the tint darkened,
1525 /// frost and finish carried), so a well in glass blurs and compresses
1526 /// what is under it again instead of being the one opaque patch in a
1527 /// frosted pane (RFC material § 11 (3)).
1528 pub fn well_floor(&mut self, rect: Rect, radius: f32, host: &Material, lifted: bool) {
1529 let fill = if host.frost.is_frosted() {
1530 host.floor(lifted).fill(PlateRole::Nested)
1531 } else if lifted {
1532 crate::colors::WELL_FLOOR_LIFTED
1533 } else {
1534 crate::colors::WELL_FLOOR
1535 };
1536 self.rounded_rect(rect, radius, (true, true, true, true), fill);
1537 }
1538
1539 /// A canvas well's rim, drawn AFTER the content so the wall's shading falls
1540 /// over whatever runs to the edge. Under `relief` it is the recess carved
1541 /// inside `rect` ([`crate::layout::carve_inside`], the wall the DE width
1542 /// capped at a fifth of the height — every well's rule); flat, the
1543 /// hairline frame every well shares ([`crate::colors::well_frame_color`]).
1544 pub fn well_rim(&mut self, rect: Rect, radius: f32, relief: bool) {
1545 let radii = (radius, radius, radius, radius);
1546 if relief {
1547 let depth = crate::layout::bevel_width().min(rect.height * 0.2);
1548 let (well, radii) = crate::layout::carve_inside(rect, radii, depth);
1549 self.recess(well, radii, depth);
1550 } else {
1551 self.border(rect, radii, [0.0; 4], crate::colors::well_frame_color(false, false), 1.0);
1552 }
1553 }
1554
1555 /// [`well_floor`](Self::well_floor) then [`well_rim`](Self::well_rim) in
1556 /// one call — a canvas whose content is drawn over the rim (a Trackpad's
1557 /// fingers). Content that should slide under the wall draws between the two.
1558 pub fn canvas_well(&mut self, rect: Rect, radius: f32, host: &Material, relief: bool, lifted: bool) {
1559 self.well_floor(rect, radius, host, lifted);
1560 self.well_rim(rect, radius, relief);
1561 }
1562
1563 /// Emit one [`crate::layout::ReliefCarve`]. The shared application point:
1564 /// a widget's `paint` carves through here, and a flat host re-emits the
1565 /// carves it collected through here too, so the two can only ever draw the
1566 /// same prim.
1567 ///
1568 /// A tinted recess takes `recess_tinted`, which lights the whole rim — it
1569 /// is the focus treatment, and every tinted carve the toolkit emits is a
1570 /// full ring. A partial ring falls back to the untinted walls rather than
1571 /// silently tinting walls the caller suppressed.
1572 pub fn carve(&mut self, c: &crate::layout::ReliefCarve) {
1573 let rect = Rect { x: c.x, y: c.y, width: c.w, height: c.h };
1574 match c.kind {
1575 crate::layout::CarveKind::Boss { tint: Some(t) } if c.edges == (true, true, true, true) => {
1576 self.boss_edges_tinted(rect, c.radii, c.depth, c.edges, t)
1577 }
1578 crate::layout::CarveKind::Boss { .. } => self.boss_edges(rect, c.radii, c.depth, c.edges),
1579 crate::layout::CarveKind::Recess { tint: Some(t) }
1580 if c.edges == (true, true, true, true) =>
1581 {
1582 self.recess_tinted(rect, c.radii, c.depth, t)
1583 }
1584 crate::layout::CarveKind::Recess { .. } => {
1585 self.recess_edges(rect, c.radii, c.depth, c.edges)
1586 }
1587 crate::layout::CarveKind::Trough => self.trough_edges(rect, c.radii, c.depth, c.edges),
1588 }
1589 }
1590
1591 /// Sink a valley along `rect`'s boundary — see [`Prim::Trough`]. `depth` is
1592 /// the full width of the seam (it straddles the outline by ±depth/2).
1593 pub fn trough(&mut self, rect: Rect, radii: Radii, depth: f32) {
1594 self.trough_edges(rect, radii, depth, (true, true, true, true));
1595 }
1596
1597 /// [`PaintCtx::trough`] with only some of the walls — see [`Prim::Trough`].
1598 pub fn trough_edges(
1599 &mut self,
1600 rect: Rect,
1601 radii: Radii,
1602 depth: f32,
1603 edges: (bool, bool, bool, bool),
1604 ) {
1605 let rect = self.apply_offset(rect);
1606 self.push(Prim::Trough { rect, radii, depth, edges, tint: None });
1607 }
1608
1609 /// [`PaintCtx::trough`] with the rim lit — see `Prim::Trough::tint` (the
1610 /// focused flush control plate).
1611 pub fn trough_tinted(&mut self, rect: Rect, radii: Radii, depth: f32, tint: [f32; 3]) {
1612 let rect = self.apply_offset(rect);
1613 self.push(Prim::Trough { rect, radii, depth, edges: (true, true, true, true), tint: Some(tint) });
1614 }
1615
1616 /// [`PaintCtx::trough_edges`] with a specular tint on the lit rim — see
1617 /// `Prim::Trough::tint`.
1618 pub fn trough_edges_tinted(
1619 &mut self,
1620 rect: Rect,
1621 radii: Radii,
1622 depth: f32,
1623 edges: (bool, bool, bool, bool),
1624 tint: [f32; 3],
1625 ) {
1626 let rect = self.apply_offset(rect);
1627 self.push(Prim::Trough { rect, radii, depth, edges, tint: Some(tint) });
1628 }
1629
1630 /// Paint a [`Field`] — see it for the forms. One with a run is a
1631 /// [`Prim::Field`]; one that is all well a [`Prim::Recess`], which groups
1632 /// into the plate under it.
1633 pub fn field(&mut self, field: &Field) {
1634 let Field { rect, radii, depth, tint, .. } = *field;
1635 match field.prim_span() {
1636 None => match tint {
1637 Some(t) => self.recess_tinted(rect, radii, depth, t),
1638 None => self.recess(rect, radii, depth),
1639 },
1640 Some((split, end)) => {
1641 let (split, end) = (split + self.offset.0, end + self.offset.0);
1642 let rect = self.apply_offset(rect);
1643 self.push(Prim::Field { rect, radii, depth, split, end, tint });
1644 }
1645 }
1646 }
1647
1648 /// Raise a rim along `rect`'s boundary — see `Prim::Ridge`. `depth` is the
1649 /// full width of the bump (it straddles the outline by ±depth/2).
1650 pub fn ridge(&mut self, rect: Rect, radii: Radii, depth: f32) {
1651 self.ridge_edges(rect, radii, depth, (true, true, true, true));
1652 }
1653
1654 /// [`PaintCtx::ridge`] with only some of the walls — see `Prim::Ridge`.
1655 pub fn ridge_edges(
1656 &mut self,
1657 rect: Rect,
1658 radii: Radii,
1659 depth: f32,
1660 edges: (bool, bool, bool, bool),
1661 ) {
1662 let rect = self.apply_offset(rect);
1663 self.push(Prim::Ridge { rect, radii, depth, edges });
1664 }
1665
1666 /// [`PaintCtx::recess`] with only some of the walls — see `Prim::Recess`.
1667 pub fn recess_edges(
1668 &mut self, rect: Rect, radii: Radii, depth: f32,
1669 edges: (bool, bool, bool, bool),
1670 ) {
1671 let rect = self.apply_offset(rect);
1672 self.push(Prim::Recess { rect, radii, depth, edges, tint: None });
1673 }
1674
1675 /// [`PaintCtx::recess_edges`] with a specular tint on the lit rim — see
1676 /// `Prim::Recess::tint`. A tinted carve never groups into its host plate
1677 /// (the tint could only land on the whole plate's specular), so it
1678 /// shades through the overlay path with its own lit rim.
1679 pub fn recess_edges_tinted(
1680 &mut self, rect: Rect, radii: Radii, depth: f32,
1681 edges: (bool, bool, bool, bool),
1682 tint: [f32; 3],
1683 ) {
1684 let rect = self.apply_offset(rect);
1685 self.push(Prim::Recess { rect, radii, depth, edges, tint: Some(tint) });
1686 }
1687
1688 /// The window's glass slab: rounded fill at full size plus a rolled, lit perimeter.
1689 /// `depth` is the roll-off width in px — pass [`crate::layout::bevel_width`] unless the
1690 /// window wants a shallower edge than the DE default.
1691 ///
1692 /// A NEGATIVE `depth` is the fill-less sentinel: no fill is drawn, and the
1693 /// rolled perimeter (width `-depth`) renders as an overlay — translucent
1694 /// white screen / black multiply — over whatever is beneath, for a root
1695 /// plate whose face is not a fill (the designer's full-bleed 3D canvas).
1696 /// `material` is ignored; the roll profile, crest and specular are exactly the
1697 /// positive-depth plate's.
1698 pub fn plate(&mut self, rect: Rect, radii: Radii, material: &Material, depth: f32) {
1699 self.plate_shaped(rect, radii, material, depth, None);
1700 }
1701
1702 /// [`plate`](Self::plate) with an explicit corner exponent — see
1703 /// [`Prim::Plate`]'s `shape`. `Some(2.0)` on a plate whose radii are its
1704 /// half-extent draws a circle; `None` is exactly `plate`.
1705 pub fn plate_shaped(&mut self, rect: Rect, radii: Radii, material: &Material, depth: f32, shape: Option<f32>) {
1706 let rect = self.apply_offset(rect);
1707 self.push(Prim::Plate { rect, radii, material: *material, depth, shape });
1708 }
1709
1710 /// Emit the plate a [`PlateSpec`] describes: role-resolved per-corner
1711 /// radii and role-encoded frost (RFC Phase 7b).
1712 ///
1713 /// The spec's radii are FINAL on-screen values (a window corner already
1714 /// wears the full silhouette span), but `Prim::Plate` speaks the older
1715 /// convention — NOMINAL radii, span applied downstream by
1716 /// `plate_push_raised(scale_corners = true)`, which the unmigrated
1717 /// hand-rolled plates (cce-cloud, the test-interface gallery shim) still
1718 /// rely on. So divide the span back out here and let the push multiply
1719 /// reconstruct the spec's exact values.
1720 ///
1721 /// Feeding the final radii straight through double-spanned every window
1722 /// corner (12 → ~100 logical at corner_shape 4.5): the plate arc pulled
1723 /// away from the compositor's clip, the black window background showed
1724 /// through as a corner crescent, and the corners stopped matching the
1725 /// desktop grid — the original 7b-2 report of this looking like "the arc
1726 /// correction" was the regression itself.
1727 pub fn plate_spec(&mut self, spec: &PlateSpec) {
1728 let f = crate::layout::corner_span_factor();
1729 let (tl, tr, br, bl) = spec.radii();
1730 self.plate(spec.rect, (tl / f, tr / f, br / f, bl / f), &spec.material.for_role(spec.role()), spec.depth);
1731 }
1732
1733 /// The standard root plate of a `width` x `height` window —
1734 /// [`PlateSpec::window`] emitted. The first prim of a standard cce app's
1735 /// frame: everything else is laid on this surface (pane plates atop it,
1736 /// bands and wells carved into it), starting
1737 /// [`crate::layout::root_plate_inset`] in from each window edge.
1738 pub fn root_plate(&mut self, width: f32, height: f32) {
1739 self.plate_spec(&PlateSpec::window(width, height));
1740 }
1741
1742 pub fn arc(&mut self, cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4]) {
1743 let (ox, oy) = self.offset;
1744 self.push(Prim::Arc { cx: cx + ox, cy: cy + oy, radius, thickness, start, end, color });
1745 }
1746
1747 /// A radially-shaded ring band — see [`Prim::ArcShaded`].
1748 #[allow(clippy::too_many_arguments)]
1749 pub fn arc_shaded(
1750 &mut self,
1751 cx: f32,
1752 cy: f32,
1753 radius: f32,
1754 thickness: f32,
1755 start: f32,
1756 end: f32,
1757 inner: [f32; 4],
1758 crest: [f32; 4],
1759 outer: [f32; 4],
1760 ) {
1761 let (ox, oy) = self.offset;
1762 self.push(Prim::ArcShaded {
1763 cx: cx + ox,
1764 cy: cy + oy,
1765 radius,
1766 thickness,
1767 start,
1768 end,
1769 inner,
1770 crest,
1771 outer,
1772 });
1773 }
1774
1775 pub fn text(&mut self, text: impl Into<String>, x: f32, y: f32, font_size: f32, color: [u8; 3]) {
1776 self.text_with(text, x, y, font_size, color, None, None);
1777 }
1778
1779 /// Text with a per-label font and clip rect (`[l, t, r, b]`, local space) — what the
1780 /// legacy `text_labels_with_font_and_bounds` tuples carry, expressible in the display
1781 /// list since Phase 6.
1782 pub fn text_with(
1783 &mut self,
1784 text: impl Into<String>,
1785 x: f32,
1786 y: f32,
1787 font_size: f32,
1788 color: [u8; 3],
1789 font: Option<String>,
1790 bounds: Option<[f32; 4]>,
1791 ) {
1792 self.text_attrs(text, x, y, font_size, color, font, bounds, TextAttrs::default());
1793 }
1794
1795 /// [`text_with`](PaintCtx::text_with) plus shaping attributes (italic / weight) — what the
1796 /// font picker's style-variant previews need beyond family + size.
1797 #[allow(clippy::too_many_arguments)]
1798 pub fn text_attrs(
1799 &mut self,
1800 text: impl Into<String>,
1801 x: f32,
1802 y: f32,
1803 font_size: f32,
1804 color: [u8; 3],
1805 font: Option<String>,
1806 bounds: Option<[f32; 4]>,
1807 attrs: TextAttrs,
1808 ) {
1809 let (ox, oy) = self.offset;
1810 let bounds = bounds.map(|[l, t, r, b]| [l + ox, t + oy, r + ox, b + oy]);
1811 self.push(Prim::Text { text: text.into(), x: x + ox, y: y + oy, font_size, color, alpha: 1.0, font, bounds, attrs, layout: None });
1812 }
1813
1814 /// [`text_with`](PaintCtx::text_with) plus a glyph alpha (1.0 = opaque) — translucent
1815 /// labels (a pane fading out) without changing the sRGB u8 color convention.
1816 #[allow(clippy::too_many_arguments)]
1817 pub fn text_faded(
1818 &mut self,
1819 text: impl Into<String>,
1820 x: f32,
1821 y: f32,
1822 font_size: f32,
1823 color: [u8; 3],
1824 alpha: f32,
1825 font: Option<String>,
1826 bounds: Option<[f32; 4]>,
1827 ) {
1828 let (ox, oy) = self.offset;
1829 let bounds = bounds.map(|[l, t, r, b]| [l + ox, t + oy, r + ox, b + oy]);
1830 self.push(Prim::Text {
1831 text: text.into(),
1832 x: x + ox,
1833 y: y + oy,
1834 font_size,
1835 color,
1836 alpha,
1837 font,
1838 bounds,
1839 attrs: TextAttrs::default(),
1840 layout: None,
1841 });
1842 }
1843
1844 /// Boxed text: word-wrap + horizontal/vertical alignment within a box (a placed text box).
1845 /// Unlike [`text_with`](PaintCtx::text_with), the backend shapes this with the box layout
1846 /// applied (cached per box). `x, y` are the box's top-left; the backend applies the vertical offset.
1847 #[allow(clippy::too_many_arguments)]
1848 pub fn text_boxed(
1849 &mut self,
1850 text: impl Into<String>,
1851 x: f32,
1852 y: f32,
1853 font_size: f32,
1854 color: [u8; 3],
1855 font: Option<String>,
1856 bounds: Option<[f32; 4]>,
1857 attrs: TextAttrs,
1858 layout: TextLayout,
1859 ) {
1860 let (ox, oy) = self.offset;
1861 let bounds = bounds.map(|[l, t, r, b]| [l + ox, t + oy, r + ox, b + oy]);
1862 self.push(Prim::Text {
1863 text: text.into(),
1864 x: x + ox,
1865 y: y + oy,
1866 font_size,
1867 color,
1868 alpha: 1.0,
1869 font,
1870 bounds,
1871 attrs,
1872 layout: Some(layout),
1873 });
1874 }
1875
1876 /// Consume the context and return the accumulated display list.
1877 pub fn finish(self) -> DisplayList {
1878 debug_assert!(self.clip_stack.is_empty(), "unbalanced push_clip/pop_clip");
1879 debug_assert!(self.offset_stack.is_empty(), "unbalanced translate");
1880 self.list
1881 }
1882 }
1883
1884 /// `PaintCtx` as a popover render target: display-list hosts pass their frame
1885 /// ctx straight into `render_popover`, so popovers draw REAL prims — relief
1886 /// plates, rounded rects, bounded text — instead of the flattened
1887 /// `PopoverCollector` view (which stays for legacy tuple hosts).
1888 impl crate::layout::RenderTarget for PaintCtx {
1889 fn icon(&mut self, name: &str, rect: Rect, color: [f32; 4]) {
1890 PaintCtx::icon(self, name, rect, color);
1891 }
1892 fn line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, color: [f32; 4], cap: Cap) {
1893 PaintCtx::vector(self, x1, y1, x2, y2, thickness, color, cap);
1894 }
1895 fn arc(&mut self, cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4]) {
1896 PaintCtx::arc(self, cx, cy, radius, thickness, start, end, color);
1897 }
1898 fn circle(&mut self, cx: f32, cy: f32, radius: f32, color: [f32; 4]) {
1899 PaintCtx::circle(self, cx, cy, radius, color);
1900 }
1901
1902 fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
1903 self.quad(Rect { x, y, width: w, height: h }, color);
1904 }
1905 fn rect_with_radius(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32) {
1906 self.rounded_rect(Rect { x, y, width: w, height: h }, radius, (true, true, true, true), color);
1907 }
1908 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)) {
1909 self.rounded_rect(Rect { x, y, width: w, height: h }, radius, corners, color);
1910 }
1911 fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
1912 let c = [
1913 (color[0] * 255.0).clamp(0.0, 255.0) as u8,
1914 (color[1] * 255.0).clamp(0.0, 255.0) as u8,
1915 (color[2] * 255.0).clamp(0.0, 255.0) as u8,
1916 ];
1917 PaintCtx::text(self, content, x, y, size, c);
1918 }
1919 fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
1920 crate::layout::RenderTarget::text_with_font_and_bounds(self, content, x, y, size, color, font, None);
1921 }
1922 fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
1923 let c = [
1924 (color[0] * 255.0).clamp(0.0, 255.0) as u8,
1925 (color[1] * 255.0).clamp(0.0, 255.0) as u8,
1926 (color[2] * 255.0).clamp(0.0, 255.0) as u8,
1927 ];
1928 self.text_with(content, x, y, size, c, None, bounds);
1929 }
1930 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]>) {
1931 let c = [
1932 (color[0] * 255.0).clamp(0.0, 255.0) as u8,
1933 (color[1] * 255.0).clamp(0.0, 255.0) as u8,
1934 (color[2] * 255.0).clamp(0.0, 255.0) as u8,
1935 ];
1936 self.text_with(content, x, y, size, c, Some(font.to_string()), bounds);
1937 }
1938 fn push_clip_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
1939 self.push_clip(Rect { x, y, width: w, height: h });
1940 }
1941 fn pop_clip_rect(&mut self) {
1942 self.pop_clip();
1943 }
1944 fn inset_plate(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32) {
1945 PaintCtx::inset_plate(self, Rect { x, y, width: w, height: h }, (radius, radius, radius, radius), Material::face(color).as_ref(), depth);
1946 }
1947 fn inset_plate_tinted(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, tint: [f32; 3]) {
1948 PaintCtx::inset_plate_tinted(self, Rect { x, y, width: w, height: h }, (radius, radius, radius, radius), Material::face(color).as_ref(), depth, tint);
1949 }
1950 fn relief_carve(&mut self, carve: &crate::layout::ReliefCarve) {
1951 PaintCtx::carve(self, carve);
1952 }
1953 }
1954
1955 #[cfg(test)]
1956 mod tests {
1957 use super::*;
1958
1959 /// A nested paint spliced in keeps its own clips under the host's: the scissors
1960 /// intersect, and the nested item's rounded clip wins over the host's.
1961 #[test]
1962 fn appended_items_keep_their_clips_under_the_hosts() {
1963 let r = |x: f32, y: f32, w: f32, h: f32| Rect { x, y, width: w, height: h };
1964 let mut nested = PaintCtx::new();
1965 nested.quad(r(0.0, 0.0, 5.0, 5.0), [1.0; 4]);
1966 nested.clip_rounded(r(10.0, 10.0, 40.0, 40.0), 6.0, |pc| pc.quad(r(12.0, 12.0, 5.0, 5.0), [1.0; 4]));
1967 let mut host = PaintCtx::new();
1968 host.clip(r(0.0, 0.0, 30.0, 30.0), |pc| pc.append_items(nested.finish().items));
1969 let items = host.finish().items;
1970 assert_eq!(items.len(), 2);
1971 assert_eq!(items[0].clip, Some(r(0.0, 0.0, 30.0, 30.0)), "an unclipped item takes the host's clip");
1972 assert_eq!(items[1].clip, Some(r(10.0, 10.0, 20.0, 20.0)), "the two scissors intersect");
1973 assert!(items[1].clip_rrect.is_some_and(|c| c[4] == 6.0), "its rounded clip survives");
1974 }
1975
1976 /// The one flush control plate draws a face and a field that is all
1977 /// run — the edge every flush control wears — and no trough.
1978 #[test]
1979 fn an_inset_plate_is_a_field_that_is_all_run() {
1980 let rect = Rect { x: 10.0, y: 20.0, width: 120.0, height: 24.0 };
1981 let face = Material::face([0.2, 0.2, 0.25, 1.0]);
1982 for tint in [None, Some([1.0, 0.5, 0.0])] {
1983 let mut pc = PaintCtx::new();
1984 match tint {
1985 Some(t) => pc.inset_plate_tinted(rect, (4.0, 4.0, 4.0, 4.0), face.as_ref(), 4.0, t),
1986 None => pc.inset_plate(rect, (4.0, 4.0, 4.0, 4.0), face.as_ref(), 4.0),
1987 }
1988 let prims: Vec<Prim> = pc.finish().items.into_iter().map(|i| i.prim).collect();
1989 assert!(!prims.iter().any(|p| matches!(p, Prim::Trough { .. })), "{prims:?}");
1990 assert!(prims.iter().any(|p| matches!(p, Prim::Border { .. })), "the face");
1991 assert!(
1992 prims.iter().any(|p| matches!(p, Prim::Field { rect: r, split, tint: t, .. } if *r == rect && *split <= rect.x - 100.0 && *t == tint)),
1993 "{prims:?}"
1994 );
1995 }
1996 }
1997 use crate::scene::material::Frost;
1998
1999 /// RFC Phase 7b: PlateSpec role mechanics — flag derivation from window
2000 /// geometry, silhouette-vs-nominal radii selection, and the role-encoded
2001 /// frost (root positive-alpha, nested negative-alpha sentinel).
2002 #[test]
2003 fn plate_spec_roles() {
2004 // Flags: a full-window rect is root; an inset pane has none; a pane
2005 // flush to the window's right edge owns the two right corners.
2006 let root_flags = PlateSpec::window_corner_flags(
2007 Rect { x: 0.0, y: 0.0, width: 800.0, height: 600.0 }, 800.0, 600.0);
2008 assert_eq!(root_flags, (true, true, true, true));
2009 let inset = PlateSpec::window_corner_flags(
2010 Rect { x: 20.0, y: 20.0, width: 100.0, height: 100.0 }, 800.0, 600.0);
2011 assert_eq!(inset, (false, false, false, false));
2012 let right_pane = PlateSpec::window_corner_flags(
2013 Rect { x: 500.0, y: 0.0, width: 300.0, height: 600.0 }, 800.0, 600.0);
2014 assert_eq!(right_pane, (false, true, true, false));
2015
2016 // Radii: flagged corners wear the shared silhouette curve, interior
2017 // ones the nominal plate radius (compared against the same getters,
2018 // so the assertion holds for any configured values).
2019 let window_r =
2020 crate::layout::window_corner_radius() * crate::layout::corner_span_factor();
2021 let nominal = crate::layout::plate_corner_radius();
2022 let r = PlateSpec::radii_for((false, true, true, false));
2023 assert_eq!(r, (nominal, window_r, window_r, nominal));
2024
2025 // Frost encoding by role.
2026 let frosted = Frost::Frosted { compression: 0.0, refraction: 0.0, radius: Frost::DEFAULT_RADIUS };
2027 let mut spec = PlateSpec {
2028 rect: Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 },
2029 material: Material::opaque([0.1, 0.2, 0.3, 0.8]).with_frost(frosted),
2030 window_corners: (true, true, true, true),
2031 depth: 3.0,
2032 };
2033 assert!(spec.is_root());
2034 assert_eq!(spec.role(), PlateRole::Root);
2035 assert!(spec.fill()[3] > 0.0, "root frost is the compositor's; alpha stays positive");
2036 spec.window_corners = (false, true, true, false);
2037 assert!(!spec.is_root());
2038 assert_eq!(spec.role(), PlateRole::Nested);
2039 assert!(spec.fill()[3] < 0.0, "nested frost = negative-alpha sentinel");
2040 spec.material.frost = Frost::Unfrosted;
2041 assert_eq!(spec.fill()[3], 0.8, "no frost, no encoding");
2042
2043 // The detach role flip (RFC 7c): a frosted nested pane becomes a
2044 // root — silhouette corners, and the frost regime flips from the
2045 // in-app sentinel to the compositor's (alpha back to positive).
2046 spec.material.frost = frosted;
2047 assert!(spec.fill()[3] < 0.0);
2048 let det = spec.detached();
2049 assert!(det.is_root());
2050 assert!(det.fill()[3] > 0.0, "root frost is the compositor's again");
2051 let wr = crate::layout::window_silhouette_radius();
2052 assert_eq!(det.radii(), (wr, wr, wr, wr));
2053 }
2054
2055 fn r(x: f32, y: f32, w: f32, h: f32) -> Rect {
2056 Rect { x, y, width: w, height: h }
2057 }
2058
2059 /// The emission round-trip: `plate_spec` pre-divides by the span factor so
2060 /// `plate_push_raised(scale_corners = true)` lands each corner at exactly
2061 /// the spec's final radius. Guards the double-span regression (7b-2), and
2062 /// holds for any configured corner_shape because both sides use the same
2063 /// factor.
2064 #[test]
2065 fn plate_spec_emission_round_trips_the_span() {
2066 let spec = PlateSpec {
2067 rect: r(0.0, 0.0, 400.0, 300.0),
2068 material: Material::opaque([0.1, 0.2, 0.3, 0.8]),
2069 window_corners: (true, true, false, false),
2070 depth: 4.0,
2071 };
2072 let mut pc = PaintCtx::new();
2073 pc.plate_spec(&spec);
2074 let f = crate::layout::corner_span_factor();
2075 let emitted = pc
2076 .finish()
2077 .items
2078 .iter()
2079 .find_map(|it| match &it.prim {
2080 Prim::Plate { radii, .. } => Some(*radii),
2081 _ => None,
2082 })
2083 .expect("plate_spec emits a Prim::Plate");
2084 let want = spec.radii();
2085 let got = (emitted.0 * f, emitted.1 * f, emitted.2 * f, emitted.3 * f);
2086 for (g, w) in [(got.0, want.0), (got.1, want.1), (got.2, want.2), (got.3, want.3)] {
2087 assert!((g - w).abs() < 1e-3, "span round-trip drifted: {g} vs {w}");
2088 }
2089 }
2090
2091 #[test]
2092 fn emits_in_order_unclipped() {
2093 let mut ctx = PaintCtx::new();
2094 ctx.quad(r(0.0, 0.0, 10.0, 10.0), [1.0, 0.0, 0.0, 1.0]);
2095 ctx.quad(r(5.0, 5.0, 10.0, 10.0), [0.0, 1.0, 0.0, 1.0]);
2096 let list = ctx.finish();
2097 assert_eq!(list.len(), 2);
2098 assert_eq!(list.items[0].clip, None);
2099 assert!(matches!(list.items[0].prim, Prim::Quad { color, .. } if color[0] == 1.0));
2100 assert!(matches!(list.items[1].prim, Prim::Quad { color, .. } if color[1] == 1.0));
2101 }
2102
2103 #[test]
2104 fn clip_is_recorded_and_popped() {
2105 let mut ctx = PaintCtx::new();
2106 ctx.clip(r(0.0, 0.0, 50.0, 50.0), |ctx| {
2107 ctx.quad(r(10.0, 10.0, 5.0, 5.0), [0.0; 4]);
2108 });
2109 ctx.quad(r(60.0, 60.0, 5.0, 5.0), [0.0; 4]); // outside any clip now
2110 let list = ctx.finish();
2111 assert_eq!(list.items[0].clip, Some(r(0.0, 0.0, 50.0, 50.0)));
2112 assert_eq!(list.items[1].clip, None, "clip popped after the closure");
2113 }
2114
2115 #[test]
2116 fn nested_clips_intersect() {
2117 let mut ctx = PaintCtx::new();
2118 ctx.clip(r(0.0, 0.0, 100.0, 100.0), |ctx| {
2119 ctx.clip(r(50.0, 50.0, 100.0, 100.0), |ctx| {
2120 ctx.quad(r(0.0, 0.0, 1.0, 1.0), [0.0; 4]);
2121 });
2122 });
2123 // Intersection of (0,0,100,100) and (50,50,100,100) = (50,50,50,50).
2124 assert_eq!(ctx.finish().items[0].clip, Some(r(50.0, 50.0, 50.0, 50.0)));
2125 }
2126
2127 #[test]
2128 fn non_overlapping_clips_produce_empty_scissor() {
2129 let mut ctx = PaintCtx::new();
2130 ctx.clip(r(0.0, 0.0, 10.0, 10.0), |ctx| {
2131 ctx.clip(r(100.0, 100.0, 10.0, 10.0), |ctx| {
2132 ctx.quad(r(0.0, 0.0, 1.0, 1.0), [0.0; 4]);
2133 });
2134 });
2135 let clip = ctx.finish().items[0].clip.unwrap();
2136 assert_eq!((clip.width, clip.height), (0.0, 0.0), "empty intersection");
2137 }
2138
2139 #[test]
2140 fn translate_applies_to_coordinates_and_restores() {
2141 let mut ctx = PaintCtx::new();
2142 ctx.translate(100.0, 200.0, |ctx| {
2143 ctx.quad(r(0.0, 0.0, 5.0, 5.0), [0.0; 4]);
2144 });
2145 ctx.quad(r(0.0, 0.0, 5.0, 5.0), [0.0; 4]); // back at origin
2146 let list = ctx.finish();
2147 assert!(matches!(list.items[0].prim, Prim::Quad { rect, .. } if rect.x == 100.0 && rect.y == 200.0));
2148 assert!(matches!(list.items[1].prim, Prim::Quad { rect, .. } if rect.x == 0.0 && rect.y == 0.0));
2149 }
2150
2151 #[test]
2152 fn nested_translate_is_cumulative() {
2153 let mut ctx = PaintCtx::new();
2154 ctx.translate(10.0, 10.0, |ctx| {
2155 ctx.translate(5.0, 5.0, |ctx| {
2156 ctx.circle(0.0, 0.0, 3.0, [0.0; 4]);
2157 });
2158 });
2159 assert!(matches!(ctx.finish().items[0].prim, Prim::Circle { cx, cy, .. } if cx == 15.0 && cy == 15.0));
2160 }
2161
2162 #[test]
2163 fn clip_pushed_under_translation_is_absolute() {
2164 let mut ctx = PaintCtx::new();
2165 ctx.translate(20.0, 20.0, |ctx| {
2166 ctx.clip(r(0.0, 0.0, 30.0, 30.0), |ctx| {
2167 ctx.quad(r(0.0, 0.0, 5.0, 5.0), [0.0; 4]);
2168 });
2169 });
2170 let item = &ctx.finish().items[0];
2171 // Clip translated to absolute (20,20,30,30); prim likewise at (20,20).
2172 assert_eq!(item.clip, Some(r(20.0, 20.0, 30.0, 30.0)));
2173 assert!(matches!(item.prim, Prim::Quad { rect, .. } if rect.x == 20.0 && rect.y == 20.0));
2174 }
2175
2176 #[test]
2177 fn all_primitive_kinds_emit() {
2178 let mut ctx = PaintCtx::new();
2179 ctx.quad(r(0.0, 0.0, 1.0, 1.0), [0.0; 4]);
2180 ctx.rounded_rect(r(0.0, 0.0, 1.0, 1.0), 2.0, (true, false, true, false), [0.0; 4]);
2181 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);
2182 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);
2183 ctx.arc(5.0, 5.0, 4.0, 1.0, 0.0, std::f32::consts::PI, [0.0; 4]);
2184 ctx.vector(0.0, 0.0, 10.0, 0.0, 1.0, [0.0; 4], Cap::Arrow);
2185 ctx.circle(5.0, 5.0, 3.0, [0.0; 4]);
2186 ctx.text("hi", 1.0, 2.0, 12.0, [255, 255, 255]);
2187 assert_eq!(ctx.finish().len(), 8);
2188 }
2189
2190 #[test]
2191 fn border_and_bevel_are_offset() {
2192 let mut ctx = PaintCtx::new();
2193 ctx.translate(10.0, 20.0, |ctx| {
2194 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);
2195 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);
2196 });
2197 let list = ctx.finish();
2198 assert!(matches!(list.items[0].prim, Prim::Border { rect, .. } if rect.x == 10.0 && rect.y == 20.0));
2199 assert!(matches!(list.items[1].prim, Prim::Bevel { rect, .. } if rect.x == 10.0 && rect.y == 20.0));
2200 }
2201 #[test]
2202 fn text_with_translates_position_and_bounds() {
2203 let mut ctx = PaintCtx::new();
2204 ctx.translate(10.0, 20.0, |ctx| {
2205 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]));
2206 ctx.text("plain", 3.0, 4.0, 10.0, [9, 9, 9]);
2207 });
2208 let list = ctx.finish();
2209 match &list.items[0].prim {
2210 Prim::Text { x, y, font, bounds, .. } => {
2211 assert_eq!((*x, *y), (11.0, 22.0), "position translated");
2212 assert_eq!(font.as_deref(), Some("Mono"));
2213 assert_eq!(*bounds, Some([10.0, 20.0, 60.0, 50.0]), "bounds translated");
2214 }
2215 other => panic!("expected Text, got {other:?}"),
2216 }
2217 match &list.items[1].prim {
2218 Prim::Text { font, bounds, .. } => {
2219 assert_eq!(*font, None, "plain text carries no font");
2220 assert_eq!(*bounds, None);
2221 }
2222 other => panic!("expected Text, got {other:?}"),
2223 }
2224 }
2225
2226 }