1use std::panic::{catch_unwind, AssertUnwindSafe};
4
5use ailloli_ui_core::geometry::Constraints;
6use ailloli_ui_core::ids::ElementId;
7use ailloli_ui_core::{Rect, Size};
8
9use crate::component::reactive::{
10 with_untracked_reads, ReactiveDependencyBatchResult, ReactiveDependencyUpdate,
11 ReactiveReadScope, ReactiveReadSet, ReactiveStage,
12};
13use crate::element::element_node::LayoutCacheKey;
14use crate::element::{ElementKind, ElementTree};
15use crate::layout::layout_attempt::{FinishedLayoutAttempt, LayoutAttemptEvent, StagedLayout};
16use crate::layout::{ChildLayout, LayoutChild, LayoutCtx, LayoutResult};
17
18pub struct LayoutEngine<'t, A> {
37 pub tree: &'t mut ElementTree<A>,
39}
40
41impl<'t, A: 'static> LayoutEngine<'t, A> {
43 pub fn new(tree: &'t mut ElementTree<A>) -> Self {
58 Self { tree }
59 }
60
61 pub fn layout_element(
90 &mut self,
91 ctx: &mut LayoutCtx<'_>,
92 element_id: ElementId,
93 constraints: Constraints,
94 ) -> LayoutResult {
95 self.layout_element_with_publication(ctx, element_id, constraints)
96 .0
97 }
98
99 pub(crate) fn layout_element_with_publication(
106 &mut self,
107 ctx: &mut LayoutCtx<'_>,
108 element_id: ElementId,
109 constraints: Constraints,
110 ) -> (LayoutResult, bool) {
111 if ctx.has_layout_attempt() {
112 return (
113 self.layout_element_staged(ctx, element_id, constraints),
114 false,
115 );
116 }
117
118 let authoritative = ctx.layout_pass().is_committed();
119 ctx.begin_layout_attempt();
120 let result = catch_unwind(AssertUnwindSafe(|| {
121 self.layout_element_staged(ctx, element_id, constraints)
122 }));
123 match result {
124 Ok(result) => {
125 let attempt = ctx.take_layout_attempt();
126 let staged = attempt.finish(|branch| ctx.measure_branch_is_adopted(branch));
127 ctx.clear_measure_branch_adoptions();
128 let published = match staged {
129 Ok(attempt) => self.commit_attempt(ctx, attempt, authoritative),
130 Err(()) => {
131 ctx.record_reactive_layout_abandon();
132 self.tree.mark_layout_path_dirty(element_id);
133 ctx.request_reactive_layout_retry(element_id);
134 false
135 }
136 };
137 (result, published)
138 }
139 Err(payload) => {
140 drop(ctx.take_layout_attempt());
141 ctx.clear_measure_branch_adoptions();
142 ctx.record_reactive_layout_abandon();
143 std::panic::resume_unwind(payload)
144 }
145 }
146 }
147
148 fn layout_element_staged(
150 &mut self,
151 ctx: &mut LayoutCtx<'_>,
152 element_id: ElementId,
153 constraints: Constraints,
154 ) -> LayoutResult {
155 let text_metrics_revision = ctx
156 .text_system
157 .as_deref()
158 .map_or(0, ailloli_ui_text::TextSystem::metrics_revision);
159 let virtual_viewport = ctx.virtual_viewport().map(|viewport| {
160 [
161 viewport.rect.x.to_bits(),
162 viewport.rect.y.to_bits(),
163 viewport.rect.w.to_bits(),
164 viewport.rect.h.to_bits(),
165 viewport.overscan.to_bits(),
166 ]
167 });
168 let Some(element) = self.tree.get(element_id) else {
169 return LayoutResult::zero();
170 };
171 let mount_generation = element.mount_generation();
172 let layout_dependency_revision = with_untracked_reads(|| match &element.kind {
173 ElementKind::Widget(widget) => widget.layout_dependency_revision(),
174 ElementKind::Empty | ElementKind::Component(_) => 0,
175 });
176 let cache_key = LayoutCacheKey {
177 constraints: [
178 constraints.min_w.to_bits(),
179 constraints.max_w.to_bits(),
180 constraints.min_h.to_bits(),
181 constraints.max_h.to_bits(),
182 ],
183 scale: ctx.scale.dpr.to_bits(),
184 layout_pass: ctx.layout_pass(),
185 text_metrics_revision,
186 layout_revision: element.layout_revision,
187 layout_dependency_revision,
188 topology_revision: element.topology_revision,
189 virtual_viewport,
190 };
191
192 if let Some((layout, dependencies)) =
193 ctx.layout_attempt()
194 .cached(element_id, ctx.layout_pass(), mount_generation, cache_key)
195 {
196 return self.stage_cache_hit(
197 ctx,
198 element_id,
199 mount_generation,
200 layout,
201 cache_key,
202 dependencies,
203 );
204 }
205
206 let retained_cache = if ctx.layout_pass().is_measure() {
207 (
208 element.measurement_layout_cache_key,
209 element.measurement_layout.clone(),
210 element.measurement_reactive_dependencies.clone(),
211 true,
212 )
213 } else {
214 (
215 element.layout_cache_key,
216 element.layout.clone(),
217 element.layout_reactive_dependencies.clone(),
218 !element.dirty.layout,
219 )
220 };
221 if retained_cache.3 && retained_cache.0 == Some(cache_key) && retained_cache.2.is_current()
222 {
223 if let Some(layout) = retained_cache.1 {
224 return self.stage_cache_hit(
225 ctx,
226 element_id,
227 mount_generation,
228 layout,
229 cache_key,
230 retained_cache.2,
231 );
232 }
233 }
234
235 let observation = ReactiveReadScope::new();
236
237 let child_ids = self.tree.children_of(element_id).to_vec();
238 let mut children = child_ids
239 .into_iter()
240 .map(|id| LayoutChild { element_id: id })
241 .collect::<Vec<_>>();
242
243 let result = match self
244 .tree
245 .get(element_id)
246 .map(|e| e.kind.clone())
247 .unwrap_or(ElementKind::Empty)
248 {
249 ElementKind::Empty => LayoutResult::zero(),
250 ElementKind::Widget(widget) => widget.layout(self, ctx, &mut children, constraints),
251 ElementKind::Component(_) => {
252 if children.is_empty() {
256 LayoutResult::zero()
257 } else {
258 let mut max = Size::default();
259 let mut child_layouts = Vec::with_capacity(children.len());
260 for mut child in children {
261 let child_result = child.layout(self, ctx, constraints);
262 max.w = max.w.max(child_result.size.w);
263 max.h = max.h.max(child_result.size.h);
264 child_layouts.push(ChildLayout {
265 offset: ailloli_ui_core::Offset::default(),
266 size: child_result.size,
267 paint_bounds: Rect::new(
268 0.0,
269 0.0,
270 child_result.size.w,
271 child_result.size.h,
272 ),
273 visual_bounds: child_result.visual_bounds,
274 });
275 }
276 let size = constraints.constrain(max);
277 LayoutResult {
278 size,
279 children: child_layouts,
280 paint_bounds: Rect::new(0.0, 0.0, size.w, size.h),
281 visual_bounds: Rect::new(0.0, 0.0, size.w, size.h),
282 overlay_hit_bounds: Vec::new(),
283 clip: None,
284 is_window_root_clip: false,
285 artifact: None,
286 }
287 }
288 }
289 };
290 let dependencies = observation.finish();
291 let measure_branch = ctx
292 .layout_pass()
293 .is_measure()
294 .then(|| ctx.current_measure_branch())
295 .flatten();
296 let attempt = ctx.layout_attempt_mut();
297 attempt.record(LayoutAttemptEvent::CacheMiss(element_id));
298 attempt.record(LayoutAttemptEvent::Layout(element_id));
299 attempt.stage(StagedLayout {
300 element_id,
301 mount_generation,
302 result: result.clone(),
303 cache_key,
304 dependencies,
305 measure_branch,
306 callback_executed: true,
307 });
308 result
309 }
310
311 fn stage_cache_hit(
313 &mut self,
314 ctx: &mut LayoutCtx<'_>,
315 element_id: ElementId,
316 mount_generation: crate::component::reactive::MountGeneration,
317 layout: LayoutResult,
318 cache_key: LayoutCacheKey,
319 cached_dependencies: ReactiveReadSet,
320 ) -> LayoutResult {
321 let observation = ReactiveReadScope::new();
322 cached_dependencies.adopt_into_current();
323 let dependencies = observation.finish();
324 let measure_branch = cache_key
325 .layout_pass
326 .is_measure()
327 .then(|| ctx.current_measure_branch())
328 .flatten();
329 let attempt = ctx.layout_attempt_mut();
330 attempt.record(LayoutAttemptEvent::CacheHit(element_id));
331 attempt.stage(StagedLayout {
332 element_id,
333 mount_generation,
334 result: layout.clone(),
335 cache_key,
336 dependencies,
337 measure_branch,
338 callback_executed: false,
339 });
340 layout
341 }
342
343 fn commit_attempt(
345 &mut self,
346 ctx: &mut LayoutCtx<'_>,
347 mut attempt: FinishedLayoutAttempt,
348 authoritative: bool,
349 ) -> bool {
350 let attempt_token = attempt.token;
351 attempt.collect_stale_elements(|element_id, mount_generation| {
352 self.tree
353 .get(element_id)
354 .is_some_and(|element| element.mount_generation() == mount_generation)
355 });
356 if !attempt.stale_elements_mut().is_empty() {
357 self.reject_stale_attempt(ctx, attempt.stale_elements_mut());
358 return false;
359 }
360 if authoritative {
361 attempt.rebuild_dependency_updates(|element_id, mount_generation, mut dependencies| {
362 self.tree.get(element_id).map(|element| {
363 dependencies.merge(&element.layout_commit_reactive_dependencies);
364 ReactiveDependencyUpdate::new(
365 element_id,
366 mount_generation,
367 ReactiveStage::Layout,
368 dependencies,
369 )
370 })
371 });
372 if matches!(
373 ctx.publish_reactive_layout(attempt.dependency_updates()),
374 ReactiveDependencyBatchResult::Stale
375 ) {
376 attempt.collect_update_elements_as_stale();
377 self.reject_stale_attempt(ctx, attempt.stale_elements_mut());
378 return false;
379 }
380 }
381
382 for event in attempt.drain_events() {
383 match event {
384 LayoutAttemptEvent::CacheHit(id) => self.tree.record_layout_cache_hit(id),
385 LayoutAttemptEvent::CacheMiss(id) => self.tree.record_layout_cache_miss(id),
386 LayoutAttemptEvent::Layout(id) => self.tree.record_layout(id),
387 }
388 }
389 for entry in attempt.drain_entries() {
390 #[cfg(feature = "devtools")]
391 if entry.cache_key.layout_pass.is_committed() {
392 let constraints = Constraints {
393 min_w: f32::from_bits(entry.cache_key.constraints[0]),
394 max_w: f32::from_bits(entry.cache_key.constraints[1]),
395 min_h: f32::from_bits(entry.cache_key.constraints[2]),
396 max_h: f32::from_bits(entry.cache_key.constraints[3]),
397 };
398 let debug =
399 ctx.record_debug_layout(entry.element_id, constraints, entry.result.size);
400 self.tree.set_layout_debug(entry.element_id, debug);
401 }
402 self.tree.set_layout_with_cache_key(
403 entry.element_id,
404 entry.result,
405 entry.cache_key,
406 entry.dependencies,
407 attempt_token,
408 entry.callback_executed,
409 );
410 }
411 true
412 }
413
414 fn reject_stale_attempt(&mut self, ctx: &LayoutCtx<'_>, element_ids: &mut Vec<ElementId>) {
416 element_ids.sort_by_key(|element_id| element_id.0);
417 element_ids.dedup();
418 ctx.record_reactive_layout_abandon();
419 self.tree
420 .mark_layout_paths_dirty(element_ids.iter().copied());
421 for element_id in element_ids.iter().copied() {
422 ctx.request_reactive_layout_retry(element_id);
423 }
424 }
425}
426
427#[cfg(test)]
428mod tests {
429 use std::cell::Cell;
430 use std::rc::Rc;
431
432 use ailloli_ui_core::{Constraints, Rect, Scale, Size};
433
434 use super::LayoutEngine;
435 use crate::app::RuntimeHandle;
436 use crate::component::Widget;
437 use crate::element::{ElementKind, ElementTree};
438 use crate::layout::{LayoutChild, LayoutCtx, LayoutResult};
439 use crate::scene::PaintCtx;
440
441 struct ResizableLeaf {
443 extent: Rc<Cell<f32>>,
444 layouts: Rc<Cell<u32>>,
445 }
446
447 impl Widget<()> for ResizableLeaf {
448 fn debug_name(&self) -> &'static str {
449 "ResizableLeaf"
450 }
451
452 fn layout(
453 &self,
454 _engine: &mut LayoutEngine<'_, ()>,
455 _ctx: &mut LayoutCtx<'_>,
456 _children: &mut [LayoutChild],
457 _constraints: Constraints,
458 ) -> LayoutResult {
459 self.layouts.set(self.layouts.get() + 1);
460 let size = Size::new(self.extent.get(), self.extent.get());
461 LayoutResult {
462 size,
463 children: Vec::new(),
464 paint_bounds: Rect::new(0.0, 0.0, size.w, size.h),
465 visual_bounds: Rect::new(0.0, 0.0, size.w, size.h),
466 overlay_hit_bounds: Vec::new(),
467 clip: None,
468 is_window_root_clip: false,
469 artifact: None,
470 }
471 }
472
473 fn paint(&self, _ctx: &mut PaintCtx<'_>, _bounds: Rect, _layout: &LayoutResult) {}
474 }
475
476 #[test]
477 fn stale_batch_rejection_preserves_previous_geometry_and_cache() {
478 let extent = Rc::new(Cell::new(10.0));
479 let layouts = Rc::new(Cell::new(0));
480 let mut tree = ElementTree::new();
481 let root = tree.create_element(
482 ElementKind::Widget(Rc::new(ResizableLeaf {
483 extent: extent.clone(),
484 layouts: layouts.clone(),
485 })),
486 None,
487 None,
488 );
489
490 let mut first_ctx = LayoutCtx::new(Scale::new(1.0));
491 let (_, first_published) = LayoutEngine::new(&mut tree).layout_element_with_publication(
492 &mut first_ctx,
493 root,
494 Constraints::loose(100.0, 100.0),
495 );
496 assert!(first_published);
497 let first = tree.get(root).unwrap();
498 let old_layout = first.layout.clone();
499 let old_cache_key = first.layout_cache_key;
500 let old_attempt = first.committed_layout_attempt;
501 let staged_generation = first.mount_generation();
502
503 extent.set(20.0);
504 tree.mark_layout_path_dirty(root);
505
506 let runtime = RuntimeHandle::<()>::new();
507 runtime.register_element_mount(root, staged_generation.next());
508 let publisher = runtime.clone();
509 let retried = Rc::new(Cell::new(false));
510 let retry = retried.clone();
511 let abandoned = Rc::new(Cell::new(0_u32));
512 let abandon = abandoned.clone();
513 let mut stale_ctx = LayoutCtx::new(Scale::new(1.0));
514 stale_ctx.set_reactive_layout_callbacks(
515 Rc::new(move |updates| publisher.replace_reactive_dependencies_batch(updates)),
516 Rc::new(move |element_id| {
517 assert_eq!(element_id, root);
518 retry.set(true);
519 }),
520 );
521 stale_ctx.set_reactive_layout_abandon_callback(Rc::new(move || {
522 abandon.set(abandon.get() + 1);
523 }));
524
525 let (attempted, published) = LayoutEngine::new(&mut tree).layout_element_with_publication(
526 &mut stale_ctx,
527 root,
528 Constraints::loose(100.0, 100.0),
529 );
530
531 assert_eq!(attempted.size.w, 20.0);
532 assert!(!published);
533 assert_eq!(layouts.get(), 2);
534 assert!(retried.get());
535 assert_eq!(abandoned.get(), 1);
536 let retained = tree.get(root).unwrap();
537 assert!(retained
538 .layout
539 .as_ref()
540 .zip(old_layout.as_ref())
541 .is_some_and(|(retained, old)| retained.geometry_eq(old)));
542 assert!(retained.layout.as_ref().unwrap().artifact.is_none());
543 assert_eq!(retained.layout_cache_key, old_cache_key);
544 assert_eq!(retained.committed_layout_attempt, old_attempt);
545 assert!(retained.dirty.layout);
546 }
547}