git.lucas.co / cce-ui
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 }