GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/scene/painter.rs (14.3K)
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, WidgetHostExt};
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 widget's own plate — beveled, or rounded fill + optional solid border — for hosts that
57 /// hand-build their display list in their own draw order (the designer) and paint the widget
58 /// on it with `paint_self`. The plate only: until 2026-10-08 it carried the widget's arcs too,
59 /// which a host then painting the widget drew twice.
60 pub fn append_widget_plate(w: &dyn WidgetHost, pc: &mut PaintCtx) {
61 append_widget_plate_tinted(w, pc, None);
62 }
63
64 /// [`append_widget_plate`] with an optional specular tint for the plate's
65 /// bevel — the focused-pane treatment: the highlight colors the lit roll's
66 /// glint instead of drawing a separate border ring. Under `control_relief` a
67 /// bordered plate renders as a bevel rolled over `plate_bevel_width` — the
68 /// relief width, the same roll a `PlateSpec` pane plate and the root plate
69 /// wear — the beveled counterpart of the flat border line, exactly the
70 /// controls' own outline→relief degradation.
71 pub fn append_widget_plate_tinted(w: &dyn WidgetHost, pc: &mut PaintCtx, tint: Option<[f32; 3]>) {
72 let radii = w.corner_radii();
73 append_widget_plate_radii(w, pc, tint, (radii.top_left, radii.top_right, radii.bottom_right, radii.bottom_left));
74 }
75
76 /// [`append_widget_plate_tinted`] with the corner radii supplied by the caller
77 /// instead of read from the widget — for hosts whose panes tile the window:
78 /// a pane corner that sits ON a window corner is that pane's share of the
79 /// root-plate silhouette and wears the window's span-widened arc, while
80 /// interior corners keep the widget-scale nominal radius.
81 pub fn append_widget_plate_radii(w: &dyn WidgetHost, pc: &mut PaintCtx, tint: Option<[f32; 3]>, radii_tuple: (f32, f32, f32, f32)) {
82 let (x, y, ww, h) = w.rect();
83 let rect = Rect { x, y, width: ww, height: h };
84 let tint = tint.unwrap_or([1.0, 1.0, 1.0]);
85 if let Some((border_color, thickness)) = w.solid_border() {
86 if crate::layout::control_relief() {
87 pc.bevel_tinted(rect, radii_tuple, &crate::scene::material::Material::from_fill(w.color()), crate::colors::plate_bevel_width(), tint);
88 } else {
89 pc.border(rect, radii_tuple, w.color(), border_color, thickness);
90 }
91 } else {
92 pc.border(rect, radii_tuple, w.color(), [0.0; 4], 0.0);
93 }
94 }
95
96 /// The scroll-ancestor text clamp the deleted default fonted getter applied. Always `None`
97 /// since Phase 6av: ScrollBox (the last scroll ancestor type) was demoted to a plain
98 /// embedded struct — it never appeared as a tree parent, so the walk never matched.
99 pub fn scroll_ancestor_text_bounds(_w: &dyn WidgetHost, _ui: &UiContext) -> Option<[f32; 4]> {
100 None
101 }
102
103 /// The deleted `WidgetHost::text_labels` default's base-label synthesis: the control label
104 /// stored on the widget base, positioned by the configured control-label layout. For
105 /// legacy widgets whose only text was that label (List's columns=None frame).
106 pub fn base_control_label(w: &dyn WidgetHost) -> Vec<TextLabel> {
107 {
108 let b = w.base();
109 if let Some(ref label) = b.label {
110 let (_, font_size) = crate::layout::control_label_font_detached_parsed();
111 let color = crate::colors::control_label_color_detached_for_state(b.hovered, b.focused);
112 return vec![TextLabel { text: label.clone(), x: b.x, y: b.y, font_size, color }];
113 }
114 }
115 Vec::new()
116 }
117
118 /// Map a legacy leaf's own plain labels to the (label, font, bounds) triples the deleted
119 /// default fonted getter produced: the widget's control font on every label plus the
120 /// scroll-ancestor clamp.
121 pub fn fonted_leaf_labels(
122 w: &dyn WidgetHost,
123 ui: &UiContext,
124 labels: Vec<TextLabel>,
125 ) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
126 let font = w.widget_font();
127 let bounds = scroll_ancestor_text_bounds(w, ui);
128 labels.into_iter().map(|l| (l, font.clone(), bounds)).collect()
129 }
130
131 fn paint_node(ui: &UiContext, w: &dyn WidgetHost, pc: &mut PaintCtx) {
132 if !w.visible() {
133 return;
134 }
135
136 // Legacy subtree painters (e.g. TreeList) render their own geometry AND their children
137 // through their own recursive aggregates, exposed via a paint_self override (see
138 // TreeList::paint_self) — emit that and stop; the walk must not also descend.
139 if w.renders_own_subtree() {
140 w.paint_self(ui, pc);
141 return;
142 }
143
144 w.paint_self(ui, pc);
145
146 let children = ui.tree.children_ptrs(w.base().id());
147 if children.is_empty() {
148 return;
149 }
150 // SAFETY: registry-resolved transients — the entries are live by the toolkit-wide
151 // registration contract, and the walk only reads through them.
152 if w.clips_children() {
153 let (x, y, cw, ch) = w.rect();
154 pc.clip(Rect { x, y, width: cw, height: ch }, |pc| {
155 for &child in &children {
156 paint_node(ui, unsafe { &*child }, pc);
157 }
158 });
159 } else {
160 for &child in &children {
161 paint_node(ui, unsafe { &*child }, pc);
162 }
163 }
164 }
165
166 #[cfg(test)]
167 mod tests {
168 use super::*;
169 use crate::scene::paint::Prim;
170 use crate::widget::Widget;
171
172 /// A synthetic widget that paints a single quad tagged by `tag` (encoded in the red channel),
173 /// so tests can assert emission order and clipping precisely. Children come from the ctx tree.
174 struct P {
175 base: Widget,
176 tag: f32,
177 clips: bool,
178 vis: bool,
179 }
180 impl P {
181 fn new(tag: f32) -> P {
182 P { base: Widget::new(), tag, clips: false, vis: true }
183 }
184 }
185 impl crate::widget::Paint for P {
186 fn color(&self) -> [f32; 4] {
187 [self.tag, 0.0, 0.0, 1.0]
188 }
189 fn clips_children(&self) -> bool {
190 self.clips
191 }
192 }
193 impl WidgetHost for P {
194 crate::impl_widget_base!(P);
195 fn paint_model(&self) -> &dyn crate::widget::Paint {
196 self
197 }
198 fn visible(&self) -> bool {
199 self.vis
200 }
201 fn paint_self(&self, _ui: &UiContext, ctx: &mut PaintCtx) {
202 let (x, y, w, h) = self.rect();
203 ctx.quad(Rect { x, y, width: w, height: h }, [self.tag, 0.0, 0.0, 1.0]);
204 }
205 }
206
207 fn reg(ctx: &mut UiContext, w: P) -> (crate::widget::WidgetId, crate::widget::Handle<P>) {
208 let h = ctx.insert(w);
209 (h.id(), h)
210 }
211
212 /// Tags of the emitted quads, in order.
213 fn tags(list: &DisplayList) -> Vec<f32> {
214 list.items
215 .iter()
216 .map(|it| match it.prim {
217 Prim::Quad { color, .. } => color[0],
218 _ => -1.0,
219 })
220 .collect()
221 }
222
223 #[test]
224 fn walks_parent_then_children_in_order() {
225 let mut ctx = UiContext::new();
226 let mut root = P::new(1.0);
227 let a = P::new(2.0);
228 let b = P::new(3.0);
229 root.base.w = 100.0;
230 root.base.h = 100.0;
231
232 let (root_id, root) = reg(&mut ctx, root);
233 let (a_id, _) = reg(&mut ctx, a);
234 let (b_id, _) = reg(&mut ctx, b);
235 ctx.link_ids(root_id, a_id);
236 ctx.link_ids(root_id, b_id);
237
238 let list = paint_tree(&ctx, &ctx[root]);
239 assert_eq!(tags(&list), vec![1.0, 2.0, 3.0], "parent, then children left-to-right");
240 assert!(list.items.iter().all(|it| it.clip.is_none()), "no clipping widget => no clips");
241 }
242
243 #[test]
244 fn clipping_container_clips_its_children() {
245 let mut ctx = UiContext::new();
246 let mut root = P::new(1.0);
247 root.clips = true;
248 root.base.x = 0.0;
249 root.base.y = 0.0;
250 root.base.w = 50.0;
251 root.base.h = 50.0;
252 let mut child = P::new(2.0);
253 child.base.x = 10.0;
254 child.base.y = 10.0;
255 child.base.w = 100.0;
256 child.base.h = 100.0;
257
258 let (root_id, root) = reg(&mut ctx, root);
259 let (child_id, _) = reg(&mut ctx, child);
260 ctx.link_ids(root_id, child_id);
261
262 let list = paint_tree(&ctx, &ctx[root]);
263 // Root paints itself unclipped; the child is clipped to the root's rect.
264 assert_eq!(list.items[0].clip, None, "root's own quad is not self-clipped");
265 assert_eq!(
266 list.items[1].clip,
267 Some(Rect { x: 0.0, y: 0.0, width: 50.0, height: 50.0 }),
268 "child clipped to the clipping container",
269 );
270 }
271
272 #[test]
273 fn invisible_subtree_is_skipped() {
274 let mut ctx = UiContext::new();
275 let root = P::new(1.0);
276 let mut mid = P::new(2.0);
277 mid.vis = false; // invisible: itself and its child must be skipped
278 let leaf = P::new(3.0);
279 let sibling = P::new(4.0);
280
281 let (root_id, root) = reg(&mut ctx, root);
282 let (mid_id, _) = reg(&mut ctx, mid);
283 let (leaf_id, _) = reg(&mut ctx, leaf);
284 let (sib_id, _) = reg(&mut ctx, sibling);
285 ctx.link_ids(root_id, mid_id);
286 ctx.link_ids(root_id, sib_id);
287 ctx.link_ids(mid_id, leaf_id);
288
289 let list = paint_tree(&ctx, &ctx[root]);
290 assert_eq!(tags(&list), vec![1.0, 4.0], "mid (invisible) and its leaf are skipped");
291 }
292
293 #[test]
294 fn nested_clipping_containers_intersect() {
295 let mut ctx = UiContext::new();
296 let mut root = P::new(1.0);
297 root.clips = true;
298 root.base.w = 100.0;
299 root.base.h = 100.0;
300 let mut inner = P::new(2.0);
301 inner.clips = true;
302 inner.base.x = 50.0;
303 inner.base.y = 50.0;
304 inner.base.w = 100.0;
305 inner.base.h = 100.0;
306 let mut leaf = P::new(3.0);
307 leaf.base.w = 200.0;
308 leaf.base.h = 200.0;
309
310 let (root_id, root) = reg(&mut ctx, root);
311 let (inner_id, _) = reg(&mut ctx, inner);
312 let (leaf_id, _) = reg(&mut ctx, leaf);
313 ctx.link_ids(root_id, inner_id);
314 ctx.link_ids(inner_id, leaf_id);
315
316 let list = paint_tree(&ctx, &ctx[root]);
317 // inner's OWN quad is clipped by its parent (root) only — its own rect clips its children,
318 // not itself. The leaf, a child of inner, is clipped to inner∩root = (50,50,50,50).
319 assert_eq!(list.items[1].clip, Some(Rect { x: 0.0, y: 0.0, width: 100.0, height: 100.0 }));
320 assert_eq!(list.items[2].clip, Some(Rect { x: 50.0, y: 50.0, width: 50.0, height: 50.0 }));
321 }
322
323 #[test]
324 fn default_paint_self_emits_rounded_background() {
325 // A widget using the DEFAULT paint_self (rounded corners + opaque color) emits a rounded
326 // rect for its background.
327 struct Rounded {
328 base: Widget,
329 }
330 impl crate::widget::Paint for Rounded {
331 fn color(&self) -> [f32; 4] {
332 [0.2, 0.4, 0.6, 1.0]
333 }
334 fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
335 Some((4.0, (true, true, true, true)))
336 }
337 }
338 impl WidgetHost for Rounded {
339 crate::impl_widget_base!(Rounded);
340 fn paint_model(&self) -> &dyn crate::widget::Paint {
341 self
342 }
343 }
344 let mut ctx = UiContext::new();
345 let mut w = Rounded { base: Widget::new() };
346 w.base.w = 20.0;
347 w.base.h = 10.0;
348 let w = ctx.insert(w);
349
350 let list = paint_tree(&ctx, &ctx[w]);
351 assert!(
352 list.items.iter().any(|it| matches!(it.prim, Prim::RoundedRect { radius, .. } if radius == 4.0)),
353 "default paint_self emits the rounded background",
354 );
355 }
356 }