ailloli_ui_runtime/scene/
paint_engine.rs1use std::collections::BTreeMap;
4
5use ailloli_ui_core::geometry::{ClipShape, Offset, Rect};
6use ailloli_ui_core::ids::ElementId;
7
8use crate::app::{Invalidation, RuntimeHandle};
9use crate::component::reactive::{MountGeneration, ReactiveReadScope, ReactiveStage};
10use crate::element::{ElementKind, ElementTree};
11use crate::layout::{LayoutArtifact, LayoutResult};
12use crate::scene::{DrawCmd, DrawText, PaintCtx};
13
14pub fn paint_element<A: 'static>(
40 tree: &ElementTree<A>,
41 ctx: &mut PaintCtx<'_>,
42 element_id: ElementId,
43 origin: Offset,
44) {
45 let Some(el) = tree.get(element_id) else {
46 return;
47 };
48 let Some(layout) = el.layout.as_ref() else {
49 return;
50 };
51
52 let bounds = layout.paint_bounds.translate(origin);
53 paint_element_with_bounds(tree, None, &mut BTreeMap::new(), ctx, element_id, bounds);
54}
55
56pub(crate) fn paint_element_observed<A: 'static>(
58 tree: &ElementTree<A>,
59 runtime: &RuntimeHandle<A>,
60 ctx: &mut PaintCtx<'_>,
61 element_id: ElementId,
62 origin: Offset,
63) {
64 let Some(element) = tree.get(element_id) else {
65 return;
66 };
67 let Some(layout) = element.layout.as_ref() else {
68 return;
69 };
70 let bounds = layout.paint_bounds.translate(origin);
71 let mut stale_feedback = BTreeMap::new();
72 paint_element_with_bounds(
73 tree,
74 Some(runtime),
75 &mut stale_feedback,
76 ctx,
77 element_id,
78 bounds,
79 );
80 runtime.record_stale_paint_feedback(stale_feedback.len());
81 for (_, (element_id, _, invalidation)) in stale_feedback {
82 runtime.invalidate(element_id, invalidation);
83 }
84}
85
86fn paint_element_with_bounds<A: 'static>(
92 tree: &ElementTree<A>,
93 runtime: Option<&RuntimeHandle<A>>,
94 stale_feedback: &mut BTreeMap<u64, (ElementId, MountGeneration, Invalidation)>,
95 ctx: &mut PaintCtx<'_>,
96 element_id: ElementId,
97 bounds: Rect,
98) {
99 tree.record_paint(element_id);
100 let Some(el) = tree.get(element_id) else {
101 return;
102 };
103 let Some(layout) = el.layout.as_ref() else {
104 return;
105 };
106 let pending_invalidation =
107 runtime.and_then(|runtime| runtime.pending_invalidation_for_element(element_id));
108 let generation_is_current = el.committed_layout_generation == Some(el.mount_generation());
109 let reactive_layout_is_current = layout_stamp_is_current(el);
110 if pending_invalidation == Some(Invalidation::Build) {
111 return;
116 }
117 if pending_invalidation == Some(Invalidation::Layout) {
118 record_feedback(
123 stale_feedback,
124 element_id,
125 el.mount_generation(),
126 Invalidation::Layout,
127 );
128 if generation_is_current && !el.dirty.layout && can_replay_committed_text_artifact(el) {
129 replay_committed_text_artifact(ctx, bounds, layout);
130 }
131 return;
132 }
133 if !generation_is_current || !reactive_layout_is_current {
134 record_feedback(
135 stale_feedback,
136 element_id,
137 el.mount_generation(),
138 Invalidation::Layout,
139 );
140 if generation_is_current && !el.dirty.layout && can_replay_committed_text_artifact(el) {
141 replay_committed_text_artifact(ctx, bounds, layout);
142 }
143 return;
144 }
145
146 let checkpoint = ctx.checkpoint();
147
148 let mut paint_children = |tree: &ElementTree<A>, ctx: &mut PaintCtx<'_>| {
149 for (idx, child_id) in el.children.iter().copied().enumerate() {
150 let Some(child_layout) = layout.children.get(idx) else {
151 continue;
152 };
153 let child_bounds = Rect::new(
154 bounds.x + child_layout.offset.x,
155 bounds.y + child_layout.offset.y,
156 child_layout.size.w,
157 child_layout.size.h,
158 );
159 paint_element_with_bounds(tree, runtime, stale_feedback, ctx, child_id, child_bounds);
160 }
161 };
162
163 let observation = match (&el.kind, runtime) {
164 (ElementKind::Widget(_), Some(_)) => Some(ReactiveReadScope::new()),
165 _ => None,
166 };
167
168 match &el.kind {
169 ElementKind::Empty => {}
170 ElementKind::Widget(widget) => {
171 let previous = ctx.set_current_interaction(
172 ctx.input_snapshot()
173 .interaction_for_widget_paint(tree, element_id),
174 );
175 widget.paint(ctx, bounds, layout);
176 ctx.set_current_interaction(previous);
177 }
178 ElementKind::Component(_) => {}
179 }
180
181 if let Some(invalidation) = paint_blocking_invalidation(runtime, element_id, el) {
182 ctx.rollback_to(checkpoint);
183 record_pending_feedback(
184 stale_feedback,
185 element_id,
186 el.mount_generation(),
187 invalidation,
188 );
189 return;
190 }
191
192 if let Some(clip) = layout.clip {
193 let clip = translate_clip(clip, Offset::new(bounds.x, bounds.y));
194 let is_root = layout.is_window_root_clip;
195 ctx.with_clip_shape(clip, is_root, |ctx| paint_children(tree, ctx));
196 } else {
197 paint_children(tree, ctx);
198 }
199
200 if let Some(invalidation) = paint_blocking_invalidation(runtime, element_id, el) {
203 ctx.rollback_to(checkpoint);
204 record_pending_feedback(
205 stale_feedback,
206 element_id,
207 el.mount_generation(),
208 invalidation,
209 );
210 return;
211 }
212
213 match &el.kind {
214 ElementKind::Empty => {}
215 ElementKind::Widget(widget) => {
216 let previous = ctx.set_current_interaction(
217 ctx.input_snapshot()
218 .interaction_for_widget_paint(tree, element_id),
219 );
220 widget.paint_overlay(ctx, bounds, layout);
221 ctx.set_current_interaction(previous);
222 }
223 ElementKind::Component(_) => {}
224 }
225
226 if let Some(invalidation) = paint_blocking_invalidation(runtime, element_id, el) {
227 ctx.rollback_to(checkpoint);
228 record_pending_feedback(
229 stale_feedback,
230 element_id,
231 el.mount_generation(),
232 invalidation,
233 );
234 return;
235 }
236
237 if let (Some(runtime), Some(observation)) = (runtime, observation) {
238 let dependencies = observation.finish();
239 if !dependencies.is_current() {
240 ctx.rollback_to(checkpoint);
241 record_feedback(
242 stale_feedback,
243 element_id,
244 el.mount_generation(),
245 Invalidation::Paint,
246 );
247 return;
248 }
249 let _ = runtime.replace_reactive_dependencies(
250 element_id,
251 el.mount_generation(),
252 ReactiveStage::Paint,
253 &dependencies,
254 );
255 }
256}
257
258fn can_replay_committed_text_artifact<A: 'static>(element: &crate::element::Element<A>) -> bool {
260 matches!(
261 &element.kind,
262 ElementKind::Widget(widget) if widget.can_replay_committed_text_artifact()
263 )
264}
265
266fn replay_committed_text_artifact(ctx: &mut PaintCtx<'_>, bounds: Rect, layout: &LayoutResult) {
273 let Some(LayoutArtifact::Text(prepared)) = layout.artifact.as_ref() else {
274 return;
275 };
276 let style = prepared.style();
277 let baseline = prepared.lines.first().map_or(0.0, |line| line.baseline_y);
278 ctx.push(DrawCmd::Text(DrawText {
279 pos: [bounds.x, bounds.y + baseline],
280 color: style.color,
281 decoration: style.decoration,
282 layout: prepared.clone(),
283 }));
284}
285
286fn layout_stamp_is_current<A>(element: &crate::element::Element<A>) -> bool {
288 !element.dirty.layout
289 && element.committed_layout_generation == Some(element.mount_generation())
290 && element.layout_reactive_dependencies.is_current()
291 && element.layout_commit_reactive_dependencies.is_current()
292}
293
294fn paint_blocking_invalidation<A>(
301 runtime: Option<&RuntimeHandle<A>>,
302 element_id: ElementId,
303 element: &crate::element::Element<A>,
304) -> Option<Invalidation> {
305 match runtime.and_then(|runtime| runtime.pending_invalidation_for_element(element_id)) {
306 Some(Invalidation::Build) => Some(Invalidation::Build),
307 Some(Invalidation::Layout) => Some(Invalidation::Layout),
308 Some(Invalidation::Paint) | None if !layout_stamp_is_current(element) => {
309 Some(Invalidation::Layout)
310 }
311 Some(Invalidation::Paint) | None => None,
312 }
313}
314
315fn record_pending_feedback(
321 feedback: &mut BTreeMap<u64, (ElementId, MountGeneration, Invalidation)>,
322 element_id: ElementId,
323 mount_generation: MountGeneration,
324 invalidation: Invalidation,
325) {
326 if invalidation == Invalidation::Layout {
327 record_feedback(feedback, element_id, mount_generation, Invalidation::Layout);
328 }
329}
330
331fn record_feedback(
333 feedback: &mut BTreeMap<u64, (ElementId, MountGeneration, Invalidation)>,
334 element_id: ElementId,
335 mount_generation: MountGeneration,
336 invalidation: Invalidation,
337) {
338 feedback
339 .entry(element_id.0)
340 .and_modify(|entry| entry.2 = entry.2.merge(invalidation))
341 .or_insert((element_id, mount_generation, invalidation));
342}
343
344fn translate_clip(clip: ClipShape, origin: Offset) -> ClipShape {
348 match clip {
349 ClipShape::Rect(r) => ClipShape::Rect(r.translate(origin)),
350 ClipShape::RoundRect { rect, radius } => ClipShape::RoundRect {
351 rect: rect.translate(origin),
352 radius,
353 },
354 }
355}