GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/scene/painter.rs (15.8K)
1 //! The paint walk — Phase 3 of the core rebuild.
2 //!
3 //! One traversal of the widget tree that emits every widget's own primitives into a single
4 //! [`DisplayList`], in draw order, through a [`PaintCtx`]. Recursion and clipping live *here*
5 //! (not smeared across each container's `all_*` methods): a widget contributes its own geometry
6 //! via [`WidgetHost::paint_self`], then the walk descends into its children — pushing the widget's
7 //! rect as a clip first when [`WidgetHost::clips_children`] is set, so the clip stack composes
8 //! automatically instead of every container re-deriving intersections by hand.
9 //!
10 //! This replaces, once wired into the backend, the three uncoordinated render paths (top-level
11 //! `view*`, recursive `all_rounded_quads`, immediate-mode `render_widget`). This module is the
12 //! walk itself, unit-tested here; routing the backend's `render()` through its `DisplayList`
13 //! (with GPU `set_scissor_rect` per clip) is the runtime-gated follow-up.
14
15 use crate::scene::layout::Rect;
16 use crate::scene::paint::{DisplayList, PaintCtx, Prim};
17 use crate::widget::{WidgetHost, TextLabel, UiContext};
18
19 /// Walk the widget subtree rooted at `root` and produce its ordered, clipped [`DisplayList`].
20 /// The walk only reads through the widgets; descent resolves children through the registry
21 /// (`ui.tree`), whose entries must be live — the toolkit-wide registration contract.
22 pub fn paint_tree(ui: &UiContext, root: &dyn WidgetHost) -> DisplayList {
23 let mut pc = PaintCtx::new();
24 paint_root_into(ui, root, &mut pc);
25 pc.finish()
26 }
27
28 /// Walk one root subtree into an existing [`PaintCtx`], for apps that compose several top-level
29 /// widgets (and their own chrome) into a single display list rather than one `root_window` tree.
30 pub fn paint_root_into(ui: &UiContext, root: &dyn WidgetHost, pc: &mut PaintCtx) {
31 paint_node(ui, root, pc);
32 }
33
34 /// Append ONLY the text of the widget subtree at `root` to `pc` — the paint walk's `Prim::Text`
35 /// items (per-widget content font, and the walk's container clip composed into each prim's
36 /// bounds). For hosts that build their frame as a [`PaintCtx`] and already emit a widget's
37 /// geometry another way, but want its text without re-deriving it through the legacy
38 /// `text_labels*` getters (the four hand-aggregate clients). The walk only reads through the
39 /// widget, so a shared `&dyn WidgetHost` is enough.
40 pub fn append_widget_text(ui: &UiContext, root: &dyn WidgetHost, pc: &mut PaintCtx) {
41 let mut scratch = PaintCtx::new();
42 paint_node(ui, root, &mut scratch);
43 for item in scratch.finish().items {
44 if let Prim::Text { text, x, y, font_size, color, font, bounds, .. } = item.prim {
45 let clip = item.clip.map(|c| [c.x, c.y, c.x + c.width, c.y + c.height]);
46 let merged = match (clip, bounds) {
47 (Some(a), Some(b)) => Some([a[0].max(b[0]), a[1].max(b[1]), a[2].min(b[2]), a[3].min(b[3])]),
48 (Some(a), None) => Some(a),
49 (None, b) => b,
50 };
51 pc.text_with(text, x, y, font_size, color, font, merged);
52 }
53 }
54 }
55
56 /// The `WidgetHost` default `paint_self`'s LEAF branch as a reusable body: leaf geometry
57 /// (rounded quads, plain quads, arcs, circles) followed by the widget's fonted labels.
58 /// Legacy leaf widgets' `paint_self` overrides call this with their own labels — the
59 /// labels are PASSED IN rather than fetched through the per-widget text getters, so this
60 /// helper (and every override built on it) survives the getters' deletion. `pub` so
61 /// app-local legacy widgets (display-manager's status/session widgets, cloud's fuzzel)
62 /// can use it too.
63 pub fn paint_legacy_leaf(
64 w: &dyn WidgetHost,
65 ui: &UiContext,
66 pc: &mut PaintCtx,
67 labels: Vec<(TextLabel, Option<String>, Option<[f32; 4]>)>,
68 ) {
69 for (x, y, qw, qh, r, c, corners) in w.all_rounded_quads(ui) {
70 pc.rounded_rect(Rect { x, y, width: qw, height: qh }, r, corners, c);
71 }
72 for (x, y, qw, qh, c) in w.all_quads(ui) {
73 pc.quad(Rect { x, y, width: qw, height: qh }, c);
74 }
75 for (cx, cy, r, t, s, e, c) in w.extra_arcs() {
76 pc.arc(cx, cy, r, t, s, e, c);
77 }
78 for (cx, cy, r, c) in w.extra_circles() {
79 pc.circle(cx, cy, r, c);
80 }
81 for (tl, font, bounds) in labels {
82 pc.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, font, bounds);
83 }
84 }
85
86 /// The prim-level mirror of the backend's `push_widget_vertices`: the widget's own plate —
87 /// beveled, or rounded fill + optional solid border — plus its extra arcs. For hosts that
88 /// hand-build their display list in their own draw order (the designer) instead of walking
89 /// `paint_self`, but want a widget's background exactly as the vertex path drew it.
90 pub fn append_widget_plate(w: &dyn WidgetHost, pc: &mut PaintCtx) {
91 append_widget_plate_tinted(w, pc, None);
92 }
93
94 /// [`append_widget_plate`] with an optional specular tint for the plate's
95 /// bevel — the focused-pane treatment: the highlight colors the lit roll's
96 /// glint instead of drawing a separate border ring. Under `control_relief` a
97 /// bordered plate renders as a bevel (`plate_bevel_width` roll) — the beveled
98 /// counterpart of the flat border line, exactly the controls' own
99 /// outline→relief degradation.
100 pub fn append_widget_plate_tinted(w: &dyn WidgetHost, pc: &mut PaintCtx, tint: Option<[f32; 3]>) {
101 let radii = w.corner_radii();
102 append_widget_plate_radii(w, pc, tint, (radii.top_left, radii.top_right, radii.bottom_right, radii.bottom_left));
103 }
104
105 /// [`append_widget_plate_tinted`] with the corner radii supplied by the caller
106 /// instead of read from the widget — for hosts whose panes tile the window:
107 /// a pane corner that sits ON a window corner is that pane's share of the
108 /// root-plate silhouette and wears the window's span-widened arc, while
109 /// interior corners keep the widget-scale nominal radius.
110 pub fn append_widget_plate_radii(w: &dyn WidgetHost, pc: &mut PaintCtx, tint: Option<[f32; 3]>, radii_tuple: (f32, f32, f32, f32)) {
111 let (x, y, ww, h) = w.rect();
112 let rect = Rect { x, y, width: ww, height: h };
113 let tint = tint.unwrap_or([1.0, 1.0, 1.0]);
114 if let Some(thickness) = w.plate_bevel() {
115 pc.bevel_tinted(rect, radii_tuple, &crate::scene::material::Material::from_fill(w.color()), thickness, tint);
116 } else if let Some((border_color, thickness)) = w.solid_border() {
117 if crate::layout::control_relief() {
118 pc.bevel_tinted(rect, radii_tuple, &crate::scene::material::Material::from_fill(w.color()), crate::colors::plate_bevel_width(), tint);
119 } else {
120 pc.border(rect, radii_tuple, w.color(), border_color, thickness);
121 }
122 } else {
123 pc.border(rect, radii_tuple, w.color(), [0.0; 4], 0.0);
124 }
125 for (cx, cy, r, t, s, e, c) in w.extra_arcs() {
126 pc.arc(cx, cy, r, t, s, e, c);
127 }
128 }
129
130 /// The scroll-ancestor text clamp the deleted default fonted getter applied. Always `None`
131 /// since Phase 6av: ScrollBox (the last scroll ancestor type) was demoted to a plain
132 /// embedded struct — it never appeared as a tree parent, so the walk never matched.
133 pub fn scroll_ancestor_text_bounds(_w: &dyn WidgetHost, _ui: &UiContext) -> Option<[f32; 4]> {
134 None
135 }
136
137 /// The deleted `WidgetHost::text_labels` default's base-label synthesis: the control label
138 /// stored on the widget base, positioned by the configured control-label layout. For
139 /// legacy widgets whose only text was that label (List's columns=None frame).
140 pub fn base_control_label(w: &dyn WidgetHost) -> Vec<TextLabel> {
141 {
142 let b = w.base();
143 if let Some(ref label) = b.label {
144 let (_, font_size) = crate::layout::control_label_font_detached_parsed();
145 let color = crate::colors::control_label_color_detached_for_state(b.hovered, b.focused);
146 return vec![TextLabel { text: label.clone(), x: b.x, y: b.y, font_size, color }];
147 }
148 }
149 Vec::new()
150 }
151
152 /// Map a legacy leaf's own plain labels to the (label, font, bounds) triples the deleted
153 /// default fonted getter produced: the widget's control font on every label plus the
154 /// scroll-ancestor clamp.
155 pub fn fonted_leaf_labels(
156 w: &dyn WidgetHost,
157 ui: &UiContext,
158 labels: Vec<TextLabel>,
159 ) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
160 let font = w.widget_font();
161 let bounds = scroll_ancestor_text_bounds(w, ui);
162 labels.into_iter().map(|l| (l, font.clone(), bounds)).collect()
163 }
164
165 fn paint_node(ui: &UiContext, w: &dyn WidgetHost, pc: &mut PaintCtx) {
166 if !w.visible() {
167 return;
168 }
169
170 // Legacy subtree painters (e.g. TreeList) render their own geometry AND their children
171 // through their own recursive aggregates, exposed via a paint_self override (see
172 // TreeList::paint_self) — emit that and stop; the walk must not also descend.
173 if w.renders_own_subtree() {
174 w.paint_self(ui, pc);
175 return;
176 }
177
178 w.paint_self(ui, pc);
179
180 let children = ui.tree.children_ptrs(w.base().id());
181 if children.is_empty() {
182 return;
183 }
184 // SAFETY: registry-resolved transients — the entries are live by the toolkit-wide
185 // registration contract, and the walk only reads through them.
186 if w.clips_children() {
187 let (x, y, cw, ch) = w.rect();
188 pc.clip(Rect { x, y, width: cw, height: ch }, |pc| {
189 for &child in &children {
190 paint_node(ui, unsafe { &*child }, pc);
191 }
192 });
193 } else {
194 for &child in &children {
195 paint_node(ui, unsafe { &*child }, pc);
196 }
197 }
198 }
199
200 #[cfg(test)]
201 mod tests {
202 use super::*;
203 use crate::scene::paint::Prim;
204 use crate::widget::Widget;
205
206 /// A synthetic widget that paints a single quad tagged by `tag` (encoded in the red channel),
207 /// so tests can assert emission order and clipping precisely. Children come from the ctx tree.
208 struct P {
209 base: Widget,
210 tag: f32,
211 clips: bool,
212 vis: bool,
213 }
214 impl P {
215 fn new(tag: f32) -> Box<P> {
216 Box::new(P { base: Widget::new(), tag, clips: false, vis: true })
217 }
218 }
219 impl WidgetHost for P {
220 crate::impl_widget_base!(P);
221 fn color(&self) -> [f32; 4] {
222 [self.tag, 0.0, 0.0, 1.0]
223 }
224 fn visible(&self) -> bool {
225 self.vis
226 }
227 fn clips_children(&self) -> bool {
228 self.clips
229 }
230 fn paint_self(&self, _ui: &UiContext, ctx: &mut PaintCtx) {
231 let (x, y, w, h) = self.rect();
232 ctx.quad(Rect { x, y, width: w, height: h }, [self.tag, 0.0, 0.0, 1.0]);
233 }
234 }
235
236 type ElemPtr = *mut (dyn WidgetHost + 'static);
237
238 fn reg(ctx: &mut UiContext, w: &mut P) -> (crate::widget::WidgetId, ElemPtr) {
239 let ptr = &mut *w as *mut _ as *mut (dyn crate::widget::WidgetHost + 'static);
240 let id = w.base.id();
241 ctx.register_widget(id, ptr);
242 (id, ptr)
243 }
244
245 /// Tags of the emitted quads, in order.
246 fn tags(list: &DisplayList) -> Vec<f32> {
247 list.items
248 .iter()
249 .map(|it| match it.prim {
250 Prim::Quad { color, .. } => color[0],
251 _ => -1.0,
252 })
253 .collect()
254 }
255
256 #[test]
257 fn walks_parent_then_children_in_order() {
258 let mut ctx = UiContext::new();
259 let mut root = P::new(1.0);
260 let mut a = P::new(2.0);
261 let mut b = P::new(3.0);
262 root.base.w = 100.0;
263 root.base.h = 100.0;
264
265 let (root_id, root_ptr) = reg(&mut ctx, &mut root);
266 let (a_id, _) = reg(&mut ctx, &mut a);
267 let (b_id, _) = reg(&mut ctx, &mut b);
268 ctx.link_ids(root_id, a_id);
269 ctx.link_ids(root_id, b_id);
270
271 let list = paint_tree(&ctx, unsafe { &*root_ptr });
272 assert_eq!(tags(&list), vec![1.0, 2.0, 3.0], "parent, then children left-to-right");
273 assert!(list.items.iter().all(|it| it.clip.is_none()), "no clipping widget => no clips");
274 }
275
276 #[test]
277 fn clipping_container_clips_its_children() {
278 let mut ctx = UiContext::new();
279 let mut root = P::new(1.0);
280 root.clips = true;
281 root.base.x = 0.0;
282 root.base.y = 0.0;
283 root.base.w = 50.0;
284 root.base.h = 50.0;
285 let mut child = P::new(2.0);
286 child.base.x = 10.0;
287 child.base.y = 10.0;
288 child.base.w = 100.0;
289 child.base.h = 100.0;
290
291 let (root_id, root_ptr) = reg(&mut ctx, &mut root);
292 let (child_id, _) = reg(&mut ctx, &mut child);
293 ctx.link_ids(root_id, child_id);
294
295 let list = paint_tree(&ctx, unsafe { &*root_ptr });
296 // Root paints itself unclipped; the child is clipped to the root's rect.
297 assert_eq!(list.items[0].clip, None, "root's own quad is not self-clipped");
298 assert_eq!(
299 list.items[1].clip,
300 Some(Rect { x: 0.0, y: 0.0, width: 50.0, height: 50.0 }),
301 "child clipped to the clipping container",
302 );
303 }
304
305 #[test]
306 fn invisible_subtree_is_skipped() {
307 let mut ctx = UiContext::new();
308 let mut root = P::new(1.0);
309 let mut mid = P::new(2.0);
310 mid.vis = false; // invisible: itself and its child must be skipped
311 let mut leaf = P::new(3.0);
312 let mut sibling = P::new(4.0);
313
314 let (root_id, root_ptr) = reg(&mut ctx, &mut root);
315 let (mid_id, _) = reg(&mut ctx, &mut mid);
316 let (leaf_id, _) = reg(&mut ctx, &mut leaf);
317 let (sib_id, _) = reg(&mut ctx, &mut sibling);
318 ctx.link_ids(root_id, mid_id);
319 ctx.link_ids(root_id, sib_id);
320 ctx.link_ids(mid_id, leaf_id);
321
322 let list = paint_tree(&ctx, unsafe { &*root_ptr });
323 assert_eq!(tags(&list), vec![1.0, 4.0], "mid (invisible) and its leaf are skipped");
324 }
325
326 #[test]
327 fn nested_clipping_containers_intersect() {
328 let mut ctx = UiContext::new();
329 let mut root = P::new(1.0);
330 root.clips = true;
331 root.base.w = 100.0;
332 root.base.h = 100.0;
333 let mut inner = P::new(2.0);
334 inner.clips = true;
335 inner.base.x = 50.0;
336 inner.base.y = 50.0;
337 inner.base.w = 100.0;
338 inner.base.h = 100.0;
339 let mut leaf = P::new(3.0);
340 leaf.base.w = 200.0;
341 leaf.base.h = 200.0;
342
343 let (root_id, root_ptr) = reg(&mut ctx, &mut root);
344 let (inner_id, _) = reg(&mut ctx, &mut inner);
345 let (leaf_id, _) = reg(&mut ctx, &mut leaf);
346 ctx.link_ids(root_id, inner_id);
347 ctx.link_ids(inner_id, leaf_id);
348
349 let list = paint_tree(&ctx, unsafe { &*root_ptr });
350 // inner's OWN quad is clipped by its parent (root) only — its own rect clips its children,
351 // not itself. The leaf, a child of inner, is clipped to inner∩root = (50,50,50,50).
352 assert_eq!(list.items[1].clip, Some(Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 }));
353 assert_eq!(list.items[2].clip, Some(Rect { x: 50.0, y: 50.0, width: 50.0, height: 50.0 }));
354 }
355
356 #[test]
357 fn default_paint_self_emits_rounded_background() {
358 // A widget using the DEFAULT paint_self (rounded corners + opaque color) emits a rounded
359 // rect for its background.
360 struct Rounded {
361 base: Widget,
362 }
363 impl WidgetHost for Rounded {
364 crate::impl_widget_base!(Rounded);
365 fn color(&self) -> [f32; 4] {
366 [0.2, 0.4, 0.6, 1.0]
367 }
368 fn corner_style(&self) -> (f32, (bool, bool, bool, bool)) {
369 (4.0, (true, true, true, true))
370 }
371 }
372 let mut ctx = UiContext::new();
373 let mut w = Rounded { base: Widget::new() };
374 w.base.w = 20.0;
375 w.base.h = 10.0;
376 let ptr = &mut w as *mut _ as *mut (dyn crate::widget::WidgetHost + 'static);
377 ctx.register_widget(w.base.id(), ptr);
378
379 let list = paint_tree(&ctx, unsafe { &*ptr });
380 assert!(
381 list.items.iter().any(|it| matches!(it.prim, Prim::RoundedRect { radius, .. } if radius == 4.0)),
382 "default paint_self emits the rounded background",
383 );
384 }
385 }