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