1use fret_core::time::Instant;
2use std::collections::HashSet;
3use std::path::{Path, PathBuf};
4use std::sync::{Mutex, OnceLock};
5use std::time::Duration;
6
7use fret_core::{FrameId, NodeId, Point, Px, Rect, Size};
8use rustc_hash::FxHashMap;
9use serde_json::{Value, json};
10use slotmap::SecondaryMap;
11use taffy::{TaffyTree, prelude::NodeId as TaffyNodeId};
12
13use crate::layout_constraints::{AvailableSpace, LayoutConstraints, LayoutSize};
14use crate::runtime_config::{LayoutEngineSweepPolicy, ui_runtime_config};
15
16mod flow;
17pub(crate) use flow::{build_viewport_flow_subtree, layout_children_from_engine_if_solved};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20struct NodeContext {
21 node: NodeId,
22 measured: bool,
23 min_content_width_as_max: bool,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct LayoutId(TaffyNodeId);
28
29#[derive(Debug, Clone, Copy, Default)]
30pub struct LayoutEngineMeasureHotspot {
31 pub node: NodeId,
32 pub total_time: Duration,
33 pub calls: u64,
34 pub cache_hits: u64,
35}
36
37pub struct TaffyLayoutEngine {
38 tree: TaffyTree<NodeContext>,
39 node_to_layout: SecondaryMap<NodeId, LayoutId>,
40 layout_to_node: FxHashMap<LayoutId, NodeId>,
41 styles: SecondaryMap<NodeId, taffy::Style>,
42 children: SecondaryMap<NodeId, Vec<NodeId>>,
43 parent: SecondaryMap<NodeId, NodeId>,
44 seen_generation: u32,
45 seen_stamp: SecondaryMap<NodeId, u32>,
46 seen_count: usize,
47 child_nodes_scratch: Vec<TaffyNodeId>,
48 child_unique_scratch: Vec<NodeId>,
49 child_dedupe_set_scratch: HashSet<NodeId>,
50 mark_seen_stack_scratch: Vec<NodeId>,
51 mark_solved_stack_scratch: Vec<NodeId>,
52 clear_solved_stack_scratch: Vec<NodeId>,
53 solve_generation: u64,
54 node_solved_stamp: SecondaryMap<NodeId, SolvedStamp>,
55 root_solve_stamp: SecondaryMap<NodeId, RootSolveStamp>,
56 measure_cache_scratch: FxHashMap<LayoutMeasureKey, taffy::geometry::Size<f32>>,
57 batch_root_scratch: Option<TaffyNodeId>,
58 batch_root_children_scratch: Vec<TaffyNodeId>,
59 batch_root_style_size_bits: Option<(u32, u32)>,
60 solve_scale_factor: f32,
61 frame_id: Option<FrameId>,
62 last_solve_time: Duration,
63 last_solve_root: Option<NodeId>,
64 last_solve_elapsed: Duration,
65 last_solve_measure_calls: u64,
66 last_solve_measure_cache_hits: u64,
67 measure_profiling_enabled: bool,
68 last_solve_measure_time: Duration,
69 last_solve_measure_hotspots: Vec<LayoutEngineMeasureHotspot>,
70}
71
72#[derive(Debug, Default, Clone)]
73pub struct DebugDumpNodeInfo {
74 pub label: Option<String>,
75 pub debug: Option<Value>,
76}
77
78#[derive(Debug, Clone, Copy)]
79#[allow(dead_code)]
80pub(crate) struct ChildLayoutRectSolvedStampDebug {
81 pub(crate) frame_id: FrameId,
82 pub(crate) solve_generation: u64,
83}
84
85#[derive(Debug, Clone, Copy)]
86#[allow(dead_code)]
87pub(crate) struct ChildLayoutRectMissDebug {
88 pub(crate) solve_generation: u64,
89 pub(crate) engine_frame_id: Option<FrameId>,
90 pub(crate) parent_stamp: Option<ChildLayoutRectSolvedStampDebug>,
91 pub(crate) child_stamp: Option<ChildLayoutRectSolvedStampDebug>,
92 pub(crate) parent_seen: bool,
93 pub(crate) child_seen: bool,
94 pub(crate) child_engine_parent: Option<NodeId>,
95 pub(crate) child_layout_id_present: bool,
96}
97
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
99struct RootSolveKey {
100 width_bits: u64,
101 height_bits: u64,
102 scale_bits: u32,
103}
104
105#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
106struct LayoutMeasureKey {
107 node: NodeId,
108 known_w: Option<u32>,
109 known_h: Option<u32>,
110 avail_w: (u8, u32),
111 avail_h: (u8, u32),
112}
113
114#[derive(Debug, Clone, Copy, PartialEq, Eq)]
115struct SolvedStamp {
116 frame_id: FrameId,
117 solve_generation: u64,
118}
119
120#[derive(Debug, Clone, Copy, PartialEq, Eq)]
121struct RootSolveStamp {
122 frame_id: FrameId,
123 key: RootSolveKey,
124}
125
126impl Default for TaffyLayoutEngine {
127 fn default() -> Self {
128 let mut tree = TaffyTree::new();
129 tree.enable_rounding();
130 Self {
131 tree,
132 node_to_layout: SecondaryMap::new(),
133 layout_to_node: FxHashMap::default(),
134 styles: SecondaryMap::new(),
135 children: SecondaryMap::new(),
136 parent: SecondaryMap::new(),
137 seen_generation: 1,
138 seen_stamp: SecondaryMap::new(),
139 seen_count: 0,
140 child_nodes_scratch: Vec::new(),
141 child_unique_scratch: Vec::new(),
142 child_dedupe_set_scratch: HashSet::new(),
143 mark_seen_stack_scratch: Vec::new(),
144 mark_solved_stack_scratch: Vec::new(),
145 clear_solved_stack_scratch: Vec::new(),
146 solve_generation: 0,
147 node_solved_stamp: SecondaryMap::new(),
148 root_solve_stamp: SecondaryMap::new(),
149 measure_cache_scratch: FxHashMap::default(),
150 batch_root_scratch: None,
151 batch_root_children_scratch: Vec::new(),
152 batch_root_style_size_bits: None,
153 solve_scale_factor: 1.0,
154 frame_id: None,
155 last_solve_time: Duration::default(),
156 last_solve_root: None,
157 last_solve_elapsed: Duration::default(),
158 last_solve_measure_calls: 0,
159 last_solve_measure_cache_hits: 0,
160 measure_profiling_enabled: false,
161 last_solve_measure_time: Duration::default(),
162 last_solve_measure_hotspots: Vec::new(),
163 }
164 }
165}
166
167impl TaffyLayoutEngine {
168 fn warn_taffy_error_once(op: &'static str, err: taffy::TaffyError) {
169 static SEEN: OnceLock<Mutex<HashSet<String>>> = OnceLock::new();
170
171 if crate::strict_runtime::strict_runtime_enabled() {
172 panic!("taffy {op} failed: {err:?}");
173 }
174
175 let key = format!("{op}:{err:?}");
176 let seen = SEEN.get_or_init(|| Mutex::new(HashSet::new()));
177 let first = match seen.lock() {
178 Ok(mut guard) => guard.insert(key),
179 Err(_) => true,
180 };
181
182 if first {
183 tracing::warn!(
184 "taffy {op} failed; layout engine results may be missing for affected nodes (falling back to widget layout): {err:?}"
185 );
186 }
187 }
188
189 #[inline]
190 fn mark_seen(&mut self, node: NodeId) {
191 let prev = self.seen_stamp.insert(node, self.seen_generation);
192 if prev != Some(self.seen_generation) {
193 self.seen_count = self.seen_count.saturating_add(1);
194 }
195 }
196
197 #[inline]
198 fn is_seen(&self, node: NodeId) -> bool {
199 self.seen_stamp.get(node).copied() == Some(self.seen_generation)
200 }
201
202 fn invalidate_solved_ancestors(&mut self, mut node: NodeId) {
203 while let Some(parent) = self.parent.get(node).copied() {
204 self.node_solved_stamp.remove(parent);
205 self.root_solve_stamp.remove(parent);
206 node = parent;
207 }
208 }
209
210 fn ensure_batch_root_scratch(&mut self) -> Option<TaffyNodeId> {
211 if let Some(id) = self.batch_root_scratch {
212 return Some(id);
213 }
214 let id = match self.tree.new_leaf_with_context(
215 Default::default(),
216 NodeContext {
217 node: NodeId::default(),
218 measured: false,
219 min_content_width_as_max: false,
220 },
221 ) {
222 Ok(id) => id,
223 Err(err) => {
224 Self::warn_taffy_error_once("new_leaf_with_context(batch_root)", err);
225 return None;
226 }
227 };
228 self.batch_root_scratch = Some(id);
229 Some(id)
230 }
231
232 pub fn begin_frame(&mut self, frame_id: FrameId) {
233 if self.frame_id != Some(frame_id) {
234 self.frame_id = Some(frame_id);
235 self.seen_generation = self.seen_generation.wrapping_add(1);
236 if self.seen_generation == 0 {
237 self.seen_generation = 1;
238 self.seen_stamp.clear();
239 }
240 self.seen_count = 0;
241 self.solve_generation = 0;
242 self.solve_scale_factor = 1.0;
243 self.last_solve_time = Duration::default();
244 self.last_solve_root = None;
245 self.last_solve_elapsed = Duration::default();
246 self.last_solve_measure_calls = 0;
247 self.last_solve_measure_cache_hits = 0;
248 self.last_solve_measure_time = Duration::default();
249 self.last_solve_measure_hotspots.clear();
250 }
251 }
252
253 pub fn end_frame(&mut self) {
254 if ui_runtime_config().layout_engine_sweep_policy == LayoutEngineSweepPolicy::OnDemand
255 && self.seen_count == self.node_to_layout.len()
256 {
257 return;
258 }
259
260 let stale: Vec<NodeId> = self
261 .node_to_layout
262 .iter()
263 .filter_map(|(node, _)| (!self.is_seen(node)).then_some(node))
264 .collect();
265
266 for node in stale {
267 let Some(layout_id) = self.node_to_layout.remove(node) else {
268 continue;
269 };
270 self.layout_to_node.remove(&layout_id);
271 self.styles.remove(node);
272 self.seen_stamp.remove(node);
273 let stale_parent = self.parent.get(node).copied();
274 if let Some(parent) = stale_parent
275 && let Some(parent_children) = self.children.get_mut(parent)
276 {
277 let old_len = parent_children.len();
278 parent_children.retain(|&child| child != node);
279 if parent_children.len() != old_len {
280 self.node_solved_stamp.remove(parent);
281 self.root_solve_stamp.remove(parent);
282 self.invalidate_solved_ancestors(parent);
283 }
284 }
285 if let Some(children) = self.children.remove(node) {
286 for child in children {
287 if self.parent.get(child) == Some(&node) {
288 self.parent.remove(child);
289 }
290 }
291 }
292 self.parent.remove(node);
293 self.node_solved_stamp.remove(node);
294 self.root_solve_stamp.remove(node);
295 let _ = self.tree.remove(layout_id.0);
296 }
297 }
298
299 pub fn layout_id_for_node(&self, node: NodeId) -> Option<LayoutId> {
300 self.node_to_layout.get(node).copied()
301 }
302
303 pub(crate) fn mark_seen_if_present(&mut self, node: NodeId) {
304 if self.node_to_layout.contains_key(node) {
305 self.mark_seen(node);
306 }
307 }
308
309 pub fn node_for_layout_id(&self, id: LayoutId) -> Option<NodeId> {
310 self.layout_to_node.get(&id).copied()
311 }
312
313 pub fn solve_count(&self) -> u64 {
314 self.solve_generation
315 }
316
317 pub fn last_solve_time(&self) -> Duration {
318 self.last_solve_time
319 }
320
321 pub fn last_solve_root(&self) -> Option<NodeId> {
322 self.last_solve_root
323 }
324
325 pub fn last_solve_elapsed(&self) -> Duration {
326 self.last_solve_elapsed
327 }
328
329 pub fn last_solve_measure_calls(&self) -> u64 {
330 self.last_solve_measure_calls
331 }
332
333 pub fn last_solve_measure_cache_hits(&self) -> u64 {
334 self.last_solve_measure_cache_hits
335 }
336
337 pub fn last_solve_measure_time(&self) -> Duration {
338 self.last_solve_measure_time
339 }
340
341 pub fn last_solve_measure_hotspots(&self) -> &[LayoutEngineMeasureHotspot] {
342 self.last_solve_measure_hotspots.as_slice()
343 }
344
345 pub fn set_measure_profiling_enabled(&mut self, enabled: bool) {
346 self.measure_profiling_enabled = enabled;
347 }
348
349 pub fn child_layout_rect_if_solved(&self, parent: NodeId, child: NodeId) -> Option<Rect> {
350 if self.solve_generation == 0 {
351 return None;
352 }
353 let frame_id = self.frame_id?;
354 let parent_stamp = self.node_solved_stamp.get(parent).copied()?;
355 let child_stamp = self.node_solved_stamp.get(child).copied()?;
356 if parent_stamp.frame_id != frame_id || child_stamp.frame_id != frame_id {
357 return None;
358 }
359 if parent_stamp.solve_generation == 0
360 || child_stamp.solve_generation == 0
361 || parent_stamp.solve_generation != child_stamp.solve_generation
362 {
363 return None;
364 }
365 if !self.is_seen(parent) || !self.is_seen(child) {
366 return None;
367 }
368 if self.parent.get(child) != Some(&parent) {
369 return None;
370 }
371 self.layout_id_for_node(child)
372 .map(|id| self.layout_rect(id))
373 }
374
375 pub(crate) fn debug_child_layout_rect_miss(
376 &self,
377 parent: NodeId,
378 child: NodeId,
379 ) -> ChildLayoutRectMissDebug {
380 fn stamp_debug(stamp: Option<SolvedStamp>) -> Option<ChildLayoutRectSolvedStampDebug> {
381 stamp.map(|s| ChildLayoutRectSolvedStampDebug {
382 frame_id: s.frame_id,
383 solve_generation: s.solve_generation,
384 })
385 }
386
387 ChildLayoutRectMissDebug {
388 solve_generation: self.solve_generation,
389 engine_frame_id: self.frame_id,
390 parent_stamp: stamp_debug(self.node_solved_stamp.get(parent).copied()),
391 child_stamp: stamp_debug(self.node_solved_stamp.get(child).copied()),
392 parent_seen: self.is_seen(parent),
393 child_seen: self.is_seen(child),
394 child_engine_parent: self.parent.get(child).copied(),
395 child_layout_id_present: self.layout_id_for_node(child).is_some(),
396 }
397 }
398
399 pub fn root_is_solved_for(
400 &self,
401 root: NodeId,
402 available: LayoutSize<AvailableSpace>,
403 scale_factor: f32,
404 ) -> bool {
405 if !self.is_seen(root) {
406 return false;
407 }
408 let Some(frame_id) = self.frame_id else {
409 return false;
410 };
411 let solved = self
412 .node_solved_stamp
413 .get(root)
414 .copied()
415 .is_some_and(|s| s.frame_id == frame_id && s.solve_generation != 0);
416 if !solved {
417 return false;
418 }
419
420 fn key_bits(axis: AvailableSpace) -> u64 {
421 match axis {
422 AvailableSpace::Definite(px) => px.0.to_bits() as u64,
423 AvailableSpace::MinContent => 1u64 << 32,
424 AvailableSpace::MaxContent => 2u64 << 32,
425 }
426 }
427
428 let sf = if scale_factor.is_finite() && scale_factor > 0.0 {
429 scale_factor
430 } else {
431 1.0
432 };
433 let key = RootSolveKey {
434 width_bits: key_bits(available.width),
435 height_bits: key_bits(available.height),
436 scale_bits: sf.to_bits(),
437 };
438 self.root_solve_stamp
439 .get(root)
440 .copied()
441 .is_some_and(|s| s.frame_id == frame_id && s.key == key)
442 }
443
444 pub fn compute_root_for_node_with_measure_if_needed(
445 &mut self,
446 root: NodeId,
447 available: LayoutSize<AvailableSpace>,
448 scale_factor: f32,
449 measure: impl FnMut(NodeId, LayoutConstraints) -> Size,
450 ) -> Option<LayoutId> {
451 let root_id = self.layout_id_for_node(root)?;
452 if !self.root_is_solved_for(root, available, scale_factor) {
453 self.compute_root_with_measure(root_id, available, scale_factor, measure);
454 }
455 Some(root_id)
456 }
457
458 pub(crate) fn compute_independent_roots_with_measure_if_needed(
471 &mut self,
472 roots: &[(NodeId, LayoutSize<AvailableSpace>)],
473 scale_factor: f32,
474 mut measure: impl FnMut(NodeId, LayoutConstraints) -> Size,
475 ) {
476 let sf = if scale_factor.is_finite() && scale_factor > 0.0 {
477 scale_factor
478 } else {
479 1.0
480 };
481
482 fn compute_individual<F: FnMut(NodeId, LayoutConstraints) -> Size>(
483 engine: &mut TaffyLayoutEngine,
484 roots: &[(NodeId, LayoutSize<AvailableSpace>)],
485 scale_factor: f32,
486 measure: &mut F,
487 ) {
488 for &(root, available) in roots {
489 let _ = engine.compute_root_for_node_with_measure_if_needed(
490 root,
491 available,
492 scale_factor,
493 &mut *measure,
494 );
495 }
496 }
497
498 let mut batchable: Vec<(NodeId, LayoutId, LayoutSize<AvailableSpace>)> =
499 Vec::with_capacity(roots.len());
500 let mut fallback: Vec<(NodeId, LayoutSize<AvailableSpace>)> = Vec::new();
501 for &(root, available) in roots {
502 let Some(layout_id) = self.layout_id_for_node(root) else {
503 continue;
504 };
505
506 if self.root_is_solved_for(root, available, sf) {
507 continue;
508 }
509
510 if let Some(parent) = self.parent.get(root).copied() {
518 let linked = self
519 .children
520 .get(parent)
521 .is_some_and(|children| children.as_slice().contains(&root));
522 if linked {
523 fallback.push((root, available));
524 continue;
525 }
526 self.parent.remove(root);
527 }
528
529 if !matches!(available.width, AvailableSpace::Definite(_))
533 || !matches!(available.height, AvailableSpace::Definite(_))
534 {
535 fallback.push((root, available));
536 continue;
537 }
538
539 batchable.push((root, layout_id, available));
540 }
541
542 if batchable.is_empty() {
543 compute_individual(self, &fallback, sf, &mut measure);
544 return;
545 }
546
547 if batchable.len() == 1 {
548 let (root, _layout_id, available) = batchable[0];
549 let _ = self.compute_root_for_node_with_measure_if_needed(
550 root,
551 available,
552 sf,
553 &mut measure,
554 );
555 compute_individual(self, &fallback, sf, &mut measure);
556 return;
557 }
558
559 let mut scratch_w_dp: f32 = 0.0;
560 let mut scratch_h_dp: f32 = 0.0;
561 for &(_root, _id, available) in &batchable {
562 let (AvailableSpace::Definite(w), AvailableSpace::Definite(h)) =
563 (available.width, available.height)
564 else {
565 debug_assert!(
566 false,
567 "pending roots must be definite in both axes to batch"
568 );
569 return;
570 };
571 scratch_w_dp = scratch_w_dp.max(w.0.max(0.0) * sf);
572 scratch_h_dp += h.0.max(0.0) * sf;
573 }
574 scratch_w_dp = scratch_w_dp.max(1.0);
577 scratch_h_dp = scratch_h_dp.max(1.0);
578
579 let Some(scratch_id) = self.ensure_batch_root_scratch() else {
580 for (root, _layout_id, available) in batchable {
581 let _ = self.compute_root_for_node_with_measure_if_needed(
582 root,
583 available,
584 sf,
585 &mut measure,
586 );
587 }
588 compute_individual(self, &fallback, sf, &mut measure);
589 return;
590 };
591
592 let scratch_style = taffy::Style {
593 display: taffy::style::Display::Block,
598 size: taffy::geometry::Size {
599 width: taffy::style::Dimension::length(scratch_w_dp),
600 height: taffy::style::Dimension::length(scratch_h_dp),
601 },
602 max_size: taffy::geometry::Size {
603 width: taffy::style::Dimension::length(scratch_w_dp),
604 height: taffy::style::Dimension::length(scratch_h_dp),
605 },
606 ..Default::default()
607 };
608 let scratch_bits = (scratch_w_dp.to_bits(), scratch_h_dp.to_bits());
609 if self.batch_root_style_size_bits != Some(scratch_bits)
610 && let Err(err) = self.tree.set_style(scratch_id, scratch_style)
611 {
612 Self::warn_taffy_error_once("set_style(batch_root)", err);
613 let _ = self.tree.set_children(scratch_id, &[]);
614 for (root, _layout_id, available) in batchable {
615 let _ = self.compute_root_for_node_with_measure_if_needed(
616 root,
617 available,
618 sf,
619 &mut measure,
620 );
621 }
622 compute_individual(self, &fallback, sf, &mut measure);
623 return;
624 }
625 self.batch_root_style_size_bits = Some(scratch_bits);
626
627 self.batch_root_children_scratch.clear();
628 self.batch_root_children_scratch.reserve(batchable.len());
629 for (_root, id, _available) in &batchable {
630 self.batch_root_children_scratch.push(id.0);
631 }
632 if let Err(err) = self
633 .tree
634 .set_children(scratch_id, &self.batch_root_children_scratch)
635 {
636 Self::warn_taffy_error_once("set_children(batch_root)", err);
637 let _ = self.tree.set_children(scratch_id, &[]);
638 for (root, _layout_id, available) in batchable {
639 let _ = self.compute_root_for_node_with_measure_if_needed(
640 root,
641 available,
642 sf,
643 &mut measure,
644 );
645 }
646 compute_individual(self, &fallback, sf, &mut measure);
647 return;
648 }
649
650 let started = Instant::now();
653 self.solve_scale_factor = sf;
654
655 let span = if tracing::enabled!(tracing::Level::TRACE) {
656 tracing::trace_span!(
657 "fret.ui.layout_engine.solve",
658 root = tracing::field::Empty,
659 frame_id = self.frame_id.map(|f| f.0).unwrap_or(0),
660 scale_factor = sf,
661 elapsed_us = tracing::field::Empty,
662 measure_calls = tracing::field::Empty,
663 measure_cache_hits = tracing::field::Empty,
664 measure_us = tracing::field::Empty,
665 )
666 } else {
667 tracing::Span::none()
668 };
669 let _span_guard = span.enter();
670
671 let taffy_available = taffy::geometry::Size {
672 width: taffy::style::AvailableSpace::Definite(scratch_w_dp),
673 height: taffy::style::AvailableSpace::Definite(scratch_h_dp),
674 };
675
676 let mut measure_calls: u64 = 0;
677 let mut measure_cache_hits: u64 = 0;
678 self.measure_cache_scratch.clear();
679 let measure_cache = &mut self.measure_cache_scratch;
680 let enable_profile = self.measure_profiling_enabled;
681 let mut measure_time = Duration::default();
682
683 #[derive(Debug, Clone, Copy, Default)]
684 struct MeasureNodeProfile {
685 total_time: Duration,
686 calls: u64,
687 cache_hits: u64,
688 }
689
690 let mut by_node: Option<SecondaryMap<NodeId, MeasureNodeProfile>> =
691 enable_profile.then(SecondaryMap::new);
692 let result = self.tree.compute_layout_with_measure(
693 scratch_id,
694 taffy_available,
695 |known, avail, _id, ctx, _style| {
696 let Some(ctx) = ctx else {
697 return taffy::geometry::Size::default();
698 };
699 if !ctx.measured {
700 return taffy::geometry::Size::default();
701 }
702
703 measure_calls = measure_calls.saturating_add(1);
704 fn quantize_size_key_bits(value: f32) -> u32 {
705 if !value.is_finite() || value <= 0.0 {
706 return 0;
707 }
708 let quantum = 64.0f32;
709 let quantized = (value * quantum).round() / quantum;
710 quantized.to_bits()
711 }
712 fn avail_key(
713 avail: taffy::style::AvailableSpace,
714 min_content_as_max: bool,
715 ) -> (u8, u32) {
716 match avail {
717 taffy::style::AvailableSpace::Definite(v) => (0, quantize_size_key_bits(v)),
718 taffy::style::AvailableSpace::MinContent => {
719 if min_content_as_max {
720 (2, 0)
721 } else {
722 (1, 0)
723 }
724 }
725 taffy::style::AvailableSpace::MaxContent => (2, 0),
726 }
727 }
728
729 let key = LayoutMeasureKey {
730 node: ctx.node,
731 known_w: known.width.map(quantize_size_key_bits),
732 known_h: known.height.map(quantize_size_key_bits),
733 avail_w: avail_key(avail.width, ctx.min_content_width_as_max),
734 avail_h: avail_key(avail.height, false),
735 };
736 if let Some(size) = measure_cache.get(&key) {
737 measure_cache_hits = measure_cache_hits.saturating_add(1);
738 if enable_profile && let Some(by_node) = by_node.as_mut() {
739 if by_node.get(ctx.node).is_none() {
740 by_node.insert(ctx.node, MeasureNodeProfile::default());
741 }
742 if let Some(profile) = by_node.get_mut(ctx.node) {
743 profile.cache_hits = profile.cache_hits.saturating_add(1);
744 }
745 }
746 return *size;
747 }
748
749 let constraints = LayoutConstraints::new(
750 LayoutSize::new(
751 known.width.map(|w| Px(w / sf)),
752 known.height.map(|h| Px(h / sf)),
753 ),
754 LayoutSize::new(
755 match avail.width {
756 taffy::style::AvailableSpace::Definite(w) => {
757 AvailableSpace::Definite(Px(w / sf))
758 }
759 taffy::style::AvailableSpace::MinContent => {
760 if ctx.min_content_width_as_max {
761 AvailableSpace::MaxContent
762 } else {
763 AvailableSpace::MinContent
764 }
765 }
766 taffy::style::AvailableSpace::MaxContent => AvailableSpace::MaxContent,
767 },
768 match avail.height {
769 taffy::style::AvailableSpace::Definite(h) => {
770 AvailableSpace::Definite(Px(h / sf))
771 }
772 taffy::style::AvailableSpace::MinContent => AvailableSpace::MinContent,
773 taffy::style::AvailableSpace::MaxContent => AvailableSpace::MaxContent,
774 },
775 ),
776 );
777
778 let (s, elapsed) = if enable_profile {
779 let measure_started = Instant::now();
780 let size = measure(ctx.node, constraints);
781 (size, measure_started.elapsed())
782 } else {
783 (measure(ctx.node, constraints), Duration::default())
784 };
785
786 if enable_profile {
787 measure_time += elapsed;
788 if let Some(by_node) = by_node.as_mut() {
789 if by_node.get(ctx.node).is_none() {
790 by_node.insert(ctx.node, MeasureNodeProfile::default());
791 }
792 if let Some(profile) = by_node.get_mut(ctx.node) {
793 profile.total_time += elapsed;
794 profile.calls = profile.calls.saturating_add(1);
795 } else {
796 debug_assert!(
797 false,
798 "layout engine profiling: expected node profile to exist after insert"
799 );
800 }
801 }
802 }
803
804 let out = taffy::geometry::Size {
805 width: s.width.0 * sf,
806 height: s.height.0 * sf,
807 };
808 measure_cache.insert(key, out);
809 out
810 },
811 );
812
813 self.last_solve_measure_calls = measure_calls;
814 self.last_solve_measure_cache_hits = measure_cache_hits;
815 self.last_solve_measure_time = measure_time;
816 if enable_profile {
817 const MAX_HOTSPOTS: usize = 8;
818 let mut hotspots: Vec<LayoutEngineMeasureHotspot> = by_node
819 .unwrap_or_default()
820 .into_iter()
821 .map(|(node, p)| LayoutEngineMeasureHotspot {
822 node,
823 total_time: p.total_time,
824 calls: p.calls,
825 cache_hits: p.cache_hits,
826 })
827 .collect();
828 hotspots.sort_by_key(|h| std::cmp::Reverse(h.total_time));
829 hotspots.truncate(MAX_HOTSPOTS);
830 self.last_solve_measure_hotspots = hotspots;
831 } else {
832 self.last_solve_measure_hotspots.clear();
833 }
834
835 if let Err(err) = result {
836 Self::warn_taffy_error_once("compute_layout_with_measure(batch_root)", err);
837 let _ = self.tree.set_children(scratch_id, &[]);
838 self.last_solve_root = None;
839 self.last_solve_elapsed = started.elapsed();
840 span.record("elapsed_us", self.last_solve_elapsed.as_micros() as u64);
841 span.record("measure_calls", measure_calls);
842 span.record("measure_cache_hits", measure_cache_hits);
843 span.record("measure_us", measure_time.as_micros() as u64);
844 self.last_solve_time += self.last_solve_elapsed;
845 compute_individual(self, &fallback, sf, &mut measure);
846 return;
847 }
848
849 self.solve_generation = self.solve_generation.saturating_add(1);
850 let stamp_root = batchable[0].0;
851 span.record("root", tracing::field::debug(stamp_root));
852 self.last_solve_root = Some(stamp_root);
853
854 fn key_bits(axis: AvailableSpace) -> u64 {
855 match axis {
856 AvailableSpace::Definite(px) => px.0.to_bits() as u64,
857 AvailableSpace::MinContent => 1u64 << 32,
858 AvailableSpace::MaxContent => 2u64 << 32,
859 }
860 }
861 if let Some(frame_id) = self.frame_id {
862 for &(root, _id, available) in &batchable {
863 self.mark_solved_subtree(root);
864 self.root_solve_stamp.insert(
865 root,
866 RootSolveStamp {
867 frame_id,
868 key: RootSolveKey {
869 width_bits: key_bits(available.width),
870 height_bits: key_bits(available.height),
871 scale_bits: self.solve_scale_factor.to_bits(),
872 },
873 },
874 );
875 }
876 }
877
878 self.last_solve_elapsed = started.elapsed();
879 span.record("elapsed_us", self.last_solve_elapsed.as_micros() as u64);
880 span.record("measure_calls", measure_calls);
881 span.record("measure_cache_hits", measure_cache_hits);
882 span.record("measure_us", measure_time.as_micros() as u64);
883 self.last_solve_time += self.last_solve_elapsed;
884
885 let _ = self.tree.set_children(scratch_id, &[]);
887
888 compute_individual(self, &fallback, sf, &mut measure);
889 }
890
891 pub(crate) fn set_viewport_root_override_size(
892 &mut self,
893 root: NodeId,
894 viewport_size: Size,
895 scale_factor: f32,
896 ) {
897 let Some(mut style) = self.styles.get(root).cloned() else {
898 return;
899 };
900
901 let sf = if scale_factor.is_finite() && scale_factor > 0.0 {
902 scale_factor
903 } else {
904 1.0
905 };
906
907 let w = viewport_size.width.0.max(0.0) * sf;
908 let h = viewport_size.height.0.max(0.0) * sf;
909 style.size.width = taffy::style::Dimension::length(w);
910 style.size.height = taffy::style::Dimension::length(h);
911 style.max_size.width = taffy::style::Dimension::length(w);
912 style.max_size.height = taffy::style::Dimension::length(h);
913
914 self.set_style(root, style);
915 }
916
917 pub fn request_layout_node(&mut self, node: NodeId) -> Option<LayoutId> {
918 if let Some(id) = self.node_to_layout.get(node).copied() {
919 self.mark_seen(node);
920 return Some(id);
921 }
922
923 let taffy_id = match self.tree.new_leaf_with_context(
924 Default::default(),
925 NodeContext {
926 node,
927 measured: false,
928 min_content_width_as_max: false,
929 },
930 ) {
931 Ok(id) => id,
932 Err(err) => {
933 Self::warn_taffy_error_once("new_leaf_with_context", err);
934 return None;
935 }
936 };
937 let id = LayoutId(taffy_id);
938 self.node_to_layout.insert(node, id);
939 self.layout_to_node.insert(id, node);
940 self.mark_seen(node);
941 Some(id)
942 }
943
944 pub fn set_measured(&mut self, node: NodeId, measured: bool) {
945 let Some(id) = self.request_layout_node(node).map(|id| id.0) else {
946 return;
947 };
948 let ctx = self
949 .tree
950 .get_node_context(id)
951 .copied()
952 .unwrap_or(NodeContext {
953 node,
954 measured,
955 min_content_width_as_max: false,
956 });
957 if ctx.node == node && ctx.measured == measured {
958 return;
959 }
960 self.node_solved_stamp.remove(node);
961 self.root_solve_stamp.remove(node);
962 self.invalidate_solved_ancestors(node);
963 if let Err(err) = self.tree.set_node_context(
964 id,
965 Some(NodeContext {
966 node,
967 measured,
968 min_content_width_as_max: ctx.min_content_width_as_max,
969 }),
970 ) {
971 Self::warn_taffy_error_once("set_node_context", err);
972 }
973 if let Err(err) = self.tree.mark_dirty(id) {
974 Self::warn_taffy_error_once("mark_dirty", err);
975 }
976 }
977
978 pub fn set_measure_min_content_width_as_max(&mut self, node: NodeId, enabled: bool) {
979 let Some(id) = self.request_layout_node(node).map(|id| id.0) else {
980 return;
981 };
982 let ctx = self
983 .tree
984 .get_node_context(id)
985 .copied()
986 .unwrap_or(NodeContext {
987 node,
988 measured: false,
989 min_content_width_as_max: enabled,
990 });
991 if ctx.node == node && ctx.min_content_width_as_max == enabled {
992 return;
993 }
994 self.node_solved_stamp.remove(node);
995 self.root_solve_stamp.remove(node);
996 self.invalidate_solved_ancestors(node);
997 if let Err(err) = self.tree.set_node_context(
998 id,
999 Some(NodeContext {
1000 node,
1001 measured: ctx.measured,
1002 min_content_width_as_max: enabled,
1003 }),
1004 ) {
1005 Self::warn_taffy_error_once("set_node_context", err);
1006 }
1007 if let Err(err) = self.tree.mark_dirty(id) {
1008 Self::warn_taffy_error_once("mark_dirty", err);
1009 }
1010 }
1011
1012 pub fn set_style(&mut self, node: NodeId, style: taffy::Style) {
1013 let Some(id) = self.request_layout_node(node).map(|id| id.0) else {
1014 return;
1015 };
1016 if self.styles.get(node) == Some(&style) {
1017 return;
1018 }
1019 self.node_solved_stamp.remove(node);
1020 self.root_solve_stamp.remove(node);
1021 self.invalidate_solved_ancestors(node);
1022 match self.tree.set_style(id, style.clone()) {
1023 Ok(()) => {
1024 self.styles.insert(node, style);
1025 if let Err(err) = self.tree.mark_dirty(id) {
1026 Self::warn_taffy_error_once("mark_dirty", err);
1027 }
1028 }
1029 Err(err) => {
1030 Self::warn_taffy_error_once("set_style", err);
1031 }
1032 }
1033 }
1034
1035 pub fn set_children(&mut self, node: NodeId, children: &[NodeId]) {
1036 if self
1040 .children
1041 .get(node)
1042 .is_some_and(|prev| prev.as_slice() == children)
1043 {
1044 return;
1045 }
1046
1047 let original_len = children.len();
1048 let mut child_unique_scratch = std::mem::take(&mut self.child_unique_scratch);
1049 let mut child_dedupe_set_scratch = std::mem::take(&mut self.child_dedupe_set_scratch);
1050
1051 let mut had_dupes = false;
1052 let children = if children.len() <= 1 {
1053 children
1054 } else if children.len() <= 32 {
1055 child_unique_scratch.clear();
1056 child_unique_scratch.reserve(children.len());
1057 for &child in children {
1058 if child_unique_scratch.contains(&child) {
1059 had_dupes = true;
1060 continue;
1061 }
1062 child_unique_scratch.push(child);
1063 }
1064 if had_dupes {
1065 child_unique_scratch.as_slice()
1066 } else {
1067 children
1068 }
1069 } else {
1070 child_unique_scratch.clear();
1071 child_dedupe_set_scratch.clear();
1072 child_unique_scratch.reserve(children.len());
1073 for &child in children {
1074 if !child_dedupe_set_scratch.insert(child) {
1075 had_dupes = true;
1076 continue;
1077 }
1078 child_unique_scratch.push(child);
1079 }
1080 if had_dupes {
1081 child_unique_scratch.as_slice()
1082 } else {
1083 children
1084 }
1085 };
1086
1087 if had_dupes {
1088 tracing::warn!(
1089 parent = ?node,
1090 children_len = original_len,
1091 unique_len = children.len(),
1092 "layout engine set_children received duplicate children; deduping to avoid taffy panic"
1093 );
1094 }
1095
1096 let Some(parent) = self.request_layout_node(node).map(|id| id.0) else {
1097 self.child_unique_scratch = child_unique_scratch;
1098 self.child_dedupe_set_scratch = child_dedupe_set_scratch;
1099 return;
1100 };
1101 let prev_children = self.children.get(node).cloned();
1102
1103 self.node_solved_stamp.remove(node);
1104 self.root_solve_stamp.remove(node);
1105 self.invalidate_solved_ancestors(node);
1106
1107 self.child_nodes_scratch.clear();
1108 self.child_nodes_scratch.reserve(children.len());
1109 for &child in children {
1110 let Some(child_id) = self.request_layout_node(child).map(|id| id.0) else {
1111 self.child_unique_scratch = child_unique_scratch;
1112 self.child_dedupe_set_scratch = child_dedupe_set_scratch;
1113 return;
1114 };
1115 self.child_nodes_scratch.push(child_id);
1116 }
1117
1118 match self.tree.set_children(parent, &self.child_nodes_scratch) {
1119 Ok(()) => {
1120 if let Some(prev_children) = prev_children.as_ref() {
1121 for &child in prev_children.iter() {
1122 if self.parent.get(child) == Some(&node) {
1123 self.parent.remove(child);
1124 }
1125 }
1126 }
1127 for &child in children {
1128 self.parent.insert(child, node);
1129 }
1130 self.children.insert(node, children.to_vec());
1131 if let Err(err) = self.tree.mark_dirty(parent) {
1132 Self::warn_taffy_error_once("mark_dirty", err);
1133 }
1134 }
1135 Err(err) => {
1136 Self::warn_taffy_error_once("set_children", err);
1137 }
1138 }
1139
1140 self.child_unique_scratch = child_unique_scratch;
1141 self.child_dedupe_set_scratch = child_dedupe_set_scratch;
1142 }
1143
1144 #[cfg(test)]
1145 pub(crate) fn debug_style_for_node(&self, node: NodeId) -> Option<&taffy::Style> {
1146 self.styles.get(node)
1147 }
1148
1149 fn apply_flex_wrap_intrinsic_main_min_size_patches(
1150 &mut self,
1151 root_node: NodeId,
1152 sf: f32,
1153 measure: &mut impl FnMut(NodeId, LayoutConstraints) -> Size,
1154 ) {
1155 use taffy::style::{
1156 AvailableSpace as TaffyAvailableSpace, Dimension, Display, FlexDirection, FlexWrap,
1157 };
1158
1159 #[derive(Clone, Copy)]
1160 enum MainAxis {
1161 Row,
1162 Column,
1163 }
1164
1165 let mut stack: Vec<NodeId> = vec![root_node];
1166 while let Some(node) = stack.pop() {
1167 let children = self.children.get(node).cloned().unwrap_or_default();
1168
1169 if let Some(style) = self.styles.get(node).cloned()
1170 && style.display == Display::Flex
1171 && style.flex_wrap == FlexWrap::Wrap
1172 {
1173 let main_axis = match style.flex_direction {
1174 FlexDirection::Row | FlexDirection::RowReverse => MainAxis::Row,
1175 FlexDirection::Column | FlexDirection::ColumnReverse => MainAxis::Column,
1176 };
1177
1178 let compute_intrinsic_main_size =
1179 |engine: &mut Self,
1180 child_id: LayoutId,
1181 main_axis: MainAxis,
1182 use_min_content: bool,
1183 sf: f32,
1184 measure: &mut dyn FnMut(NodeId, LayoutConstraints) -> Size|
1185 -> Option<f32> {
1186 let avail = match (main_axis, use_min_content) {
1187 (MainAxis::Row, true) => taffy::geometry::Size {
1188 width: TaffyAvailableSpace::MinContent,
1189 height: TaffyAvailableSpace::MaxContent,
1190 },
1191 (MainAxis::Row, false) => taffy::geometry::Size {
1192 width: TaffyAvailableSpace::MaxContent,
1193 height: TaffyAvailableSpace::MaxContent,
1194 },
1195 (MainAxis::Column, true) => taffy::geometry::Size {
1196 width: TaffyAvailableSpace::MaxContent,
1197 height: TaffyAvailableSpace::MinContent,
1198 },
1199 (MainAxis::Column, false) => taffy::geometry::Size {
1200 width: TaffyAvailableSpace::MaxContent,
1201 height: TaffyAvailableSpace::MaxContent,
1202 },
1203 };
1204
1205 let result = engine.tree.compute_layout_with_measure(
1206 child_id.0,
1207 avail,
1208 |known, avail, _id, ctx, _style| {
1209 let Some(ctx) = ctx else {
1210 return taffy::geometry::Size::default();
1211 };
1212 if !ctx.measured {
1213 return taffy::geometry::Size::default();
1214 }
1215
1216 let constraints = LayoutConstraints::new(
1217 LayoutSize::new(
1218 known.width.map(|w| Px(w / sf)),
1219 known.height.map(|h| Px(h / sf)),
1220 ),
1221 LayoutSize::new(
1222 match avail.width {
1223 TaffyAvailableSpace::Definite(w) => {
1224 AvailableSpace::Definite(Px(w / sf))
1225 }
1226 TaffyAvailableSpace::MinContent => {
1227 if ctx.min_content_width_as_max {
1228 AvailableSpace::MaxContent
1229 } else {
1230 AvailableSpace::MinContent
1231 }
1232 }
1233 TaffyAvailableSpace::MaxContent => {
1234 AvailableSpace::MaxContent
1235 }
1236 },
1237 match avail.height {
1238 TaffyAvailableSpace::Definite(h) => {
1239 AvailableSpace::Definite(Px(h / sf))
1240 }
1241 TaffyAvailableSpace::MinContent => {
1242 AvailableSpace::MinContent
1243 }
1244 TaffyAvailableSpace::MaxContent => {
1245 AvailableSpace::MaxContent
1246 }
1247 },
1248 ),
1249 );
1250
1251 let s = measure(ctx.node, constraints);
1252 taffy::geometry::Size {
1253 width: s.width.0 * sf,
1254 height: s.height.0 * sf,
1255 }
1256 },
1257 );
1258 if result.is_err() {
1259 return None;
1260 }
1261
1262 let layout = engine.tree.layout(child_id.0).ok()?;
1263 let main_dp = match main_axis {
1264 MainAxis::Row => layout.size.width,
1265 MainAxis::Column => layout.size.height,
1266 };
1267 if main_dp.is_finite() && main_dp > 0.0 {
1268 Some(main_dp)
1269 } else {
1270 None
1271 }
1272 };
1273
1274 for child in children.iter().copied() {
1275 let Some(mut child_style) = self.styles.get(child).cloned() else {
1276 continue;
1277 };
1278
1279 let overflow_visible_in_main = match main_axis {
1280 MainAxis::Row => child_style.overflow.x == taffy::style::Overflow::Visible,
1281 MainAxis::Column => {
1282 child_style.overflow.y == taffy::style::Overflow::Visible
1283 }
1284 };
1285 if !overflow_visible_in_main {
1286 continue;
1287 }
1288
1289 let (main_size_is_auto, main_min_is_auto) = match main_axis {
1296 MainAxis::Row => (
1297 child_style.size.width.is_auto(),
1298 child_style.min_size.width.is_auto(),
1299 ),
1300 MainAxis::Column => (
1301 child_style.size.height.is_auto(),
1302 child_style.min_size.height.is_auto(),
1303 ),
1304 };
1305 if !main_size_is_auto || !main_min_is_auto {
1306 continue;
1307 }
1308
1309 let Some(child_id) = self.layout_id_for_node(child) else {
1310 continue;
1311 };
1312
1313 let main_dp =
1323 compute_intrinsic_main_size(self, child_id, main_axis, true, sf, measure)
1324 .or_else(|| {
1325 compute_intrinsic_main_size(
1326 self, child_id, main_axis, false, sf, measure,
1327 )
1328 });
1329 let Some(main_dp) = main_dp else {
1330 continue;
1331 };
1332
1333 match main_axis {
1334 MainAxis::Row => child_style.min_size.width = Dimension::length(main_dp),
1335 MainAxis::Column => {
1336 child_style.min_size.height = Dimension::length(main_dp)
1337 }
1338 }
1339 self.set_style(child, child_style);
1340 }
1341 }
1342
1343 stack.extend(children);
1345 }
1346 }
1347
1348 #[stacksafe::stacksafe]
1349 pub fn compute_root_with_measure(
1350 &mut self,
1351 root: LayoutId,
1352 available: LayoutSize<AvailableSpace>,
1353 scale_factor: f32,
1354 mut measure: impl FnMut(NodeId, LayoutConstraints) -> Size,
1355 ) {
1356 fn quantize_size_key_bits(value: f32) -> u32 {
1357 if !value.is_finite() || value <= 0.0 {
1358 return 0;
1359 }
1360 let quantum = 64.0f32;
1361 let quantized = (value * quantum).round() / quantum;
1362 quantized.to_bits()
1363 }
1364
1365 fn avail_key(avail: taffy::style::AvailableSpace, min_content_as_max: bool) -> (u8, u32) {
1366 match avail {
1367 taffy::style::AvailableSpace::Definite(v) => (0, quantize_size_key_bits(v)),
1368 taffy::style::AvailableSpace::MinContent => {
1369 if min_content_as_max {
1370 (2, 0)
1371 } else {
1372 (1, 0)
1373 }
1374 }
1375 taffy::style::AvailableSpace::MaxContent => (2, 0),
1376 }
1377 }
1378
1379 let started = Instant::now();
1380 let sf = if scale_factor.is_finite() && scale_factor > 0.0 {
1381 scale_factor
1382 } else {
1383 1.0
1384 };
1385 self.solve_scale_factor = sf;
1386
1387 let span = if tracing::enabled!(tracing::Level::TRACE) {
1388 tracing::trace_span!(
1389 "fret.ui.layout_engine.solve",
1390 root = tracing::field::Empty,
1391 frame_id = self.frame_id.map(|f| f.0).unwrap_or(0),
1392 scale_factor = sf,
1393 elapsed_us = tracing::field::Empty,
1394 measure_calls = tracing::field::Empty,
1395 measure_cache_hits = tracing::field::Empty,
1396 measure_us = tracing::field::Empty,
1397 )
1398 } else {
1399 tracing::Span::none()
1400 };
1401 let _span_guard = span.enter();
1402
1403 let root_node = self.node_for_layout_id(root);
1404 if let Some(root_node) = root_node {
1405 self.apply_flex_wrap_intrinsic_main_min_size_patches(root_node, sf, &mut measure);
1406 }
1407
1408 let taffy_available = taffy::geometry::Size {
1409 width: match available.width {
1410 AvailableSpace::Definite(px) => taffy::style::AvailableSpace::Definite(px.0 * sf),
1411 AvailableSpace::MinContent => taffy::style::AvailableSpace::MinContent,
1412 AvailableSpace::MaxContent => taffy::style::AvailableSpace::MaxContent,
1413 },
1414 height: match available.height {
1415 AvailableSpace::Definite(px) => taffy::style::AvailableSpace::Definite(px.0 * sf),
1416 AvailableSpace::MinContent => taffy::style::AvailableSpace::MinContent,
1417 AvailableSpace::MaxContent => taffy::style::AvailableSpace::MaxContent,
1418 },
1419 };
1420
1421 let mut measure_calls: u64 = 0;
1422 let mut measure_cache_hits: u64 = 0;
1423 self.measure_cache_scratch.clear();
1424 let measure_cache = &mut self.measure_cache_scratch;
1425 let enable_profile = self.measure_profiling_enabled;
1426 let mut measure_time = Duration::default();
1427
1428 #[derive(Debug, Clone, Copy, Default)]
1429 struct MeasureNodeProfile {
1430 total_time: Duration,
1431 calls: u64,
1432 cache_hits: u64,
1433 }
1434
1435 let mut by_node: Option<SecondaryMap<NodeId, MeasureNodeProfile>> =
1436 enable_profile.then(SecondaryMap::new);
1437 let result = self.tree.compute_layout_with_measure(
1438 root.0,
1439 taffy_available,
1440 |known, avail, _id, ctx, _style| {
1441 let Some(ctx) = ctx else {
1442 return taffy::geometry::Size::default();
1443 };
1444 if !ctx.measured {
1445 return taffy::geometry::Size::default();
1446 }
1447
1448 measure_calls = measure_calls.saturating_add(1);
1449 let key = LayoutMeasureKey {
1450 node: ctx.node,
1451 known_w: known.width.map(quantize_size_key_bits),
1452 known_h: known.height.map(quantize_size_key_bits),
1453 avail_w: avail_key(avail.width, ctx.min_content_width_as_max),
1454 avail_h: avail_key(avail.height, false),
1455 };
1456 if let Some(size) = measure_cache.get(&key) {
1457 measure_cache_hits = measure_cache_hits.saturating_add(1);
1458 if enable_profile && let Some(by_node) = by_node.as_mut() {
1459 if by_node.get(ctx.node).is_none() {
1460 by_node.insert(ctx.node, MeasureNodeProfile::default());
1461 }
1462 if let Some(profile) = by_node.get_mut(ctx.node) {
1463 profile.cache_hits = profile.cache_hits.saturating_add(1);
1464 } else {
1465 debug_assert!(
1466 false,
1467 "layout engine profiling: expected node profile to exist after insert"
1468 );
1469 }
1470 }
1471 return *size;
1472 }
1473
1474 let constraints = LayoutConstraints::new(
1475 LayoutSize::new(
1476 known.width.map(|w| Px(w / sf)),
1477 known.height.map(|h| Px(h / sf)),
1478 ),
1479 LayoutSize::new(
1480 match avail.width {
1481 taffy::style::AvailableSpace::Definite(w) => {
1482 AvailableSpace::Definite(Px(w / sf))
1483 }
1484 taffy::style::AvailableSpace::MinContent => {
1485 if ctx.min_content_width_as_max {
1486 AvailableSpace::MaxContent
1487 } else {
1488 AvailableSpace::MinContent
1489 }
1490 }
1491 taffy::style::AvailableSpace::MaxContent => AvailableSpace::MaxContent,
1492 },
1493 match avail.height {
1494 taffy::style::AvailableSpace::Definite(h) => {
1495 AvailableSpace::Definite(Px(h / sf))
1496 }
1497 taffy::style::AvailableSpace::MinContent => AvailableSpace::MinContent,
1498 taffy::style::AvailableSpace::MaxContent => AvailableSpace::MaxContent,
1499 },
1500 ),
1501 );
1502
1503 let (s, elapsed) = if enable_profile {
1504 let measure_started = Instant::now();
1505 let size = measure(ctx.node, constraints);
1506 (size, measure_started.elapsed())
1507 } else {
1508 (measure(ctx.node, constraints), Duration::default())
1509 };
1510
1511 if enable_profile {
1512 measure_time += elapsed;
1513 if let Some(by_node) = by_node.as_mut() {
1514 if by_node.get(ctx.node).is_none() {
1515 by_node.insert(ctx.node, MeasureNodeProfile::default());
1516 }
1517 if let Some(profile) = by_node.get_mut(ctx.node) {
1518 profile.total_time += elapsed;
1519 profile.calls = profile.calls.saturating_add(1);
1520 } else {
1521 debug_assert!(
1522 false,
1523 "layout engine profiling: expected node profile to exist after insert"
1524 );
1525 }
1526 }
1527 }
1528 let out = taffy::geometry::Size {
1529 width: s.width.0 * sf,
1530 height: s.height.0 * sf,
1531 };
1532 measure_cache.insert(key, out);
1533 out
1534 },
1535 );
1536
1537 self.last_solve_measure_calls = measure_calls;
1538 self.last_solve_measure_cache_hits = measure_cache_hits;
1539 self.last_solve_measure_time = measure_time;
1540 if enable_profile {
1541 const MAX_HOTSPOTS: usize = 8;
1542 let mut hotspots: Vec<LayoutEngineMeasureHotspot> = by_node
1543 .unwrap_or_default()
1544 .into_iter()
1545 .map(|(node, p)| LayoutEngineMeasureHotspot {
1546 node,
1547 total_time: p.total_time,
1548 calls: p.calls,
1549 cache_hits: p.cache_hits,
1550 })
1551 .collect();
1552 hotspots.sort_by_key(|h| std::cmp::Reverse(h.total_time));
1553 hotspots.truncate(MAX_HOTSPOTS);
1554 self.last_solve_measure_hotspots = hotspots;
1555 } else {
1556 self.last_solve_measure_hotspots.clear();
1557 }
1558
1559 if let Err(err) = result {
1560 Self::warn_taffy_error_once("compute_layout_with_measure", err);
1561 if let Some(root_node) = root_node {
1562 self.clear_solved_subtree(root_node);
1563 self.root_solve_stamp.remove(root_node);
1564 }
1565 self.last_solve_root = None;
1566 self.last_solve_elapsed = started.elapsed();
1567 span.record("elapsed_us", self.last_solve_elapsed.as_micros() as u64);
1568 span.record("measure_calls", measure_calls);
1569 span.record("measure_cache_hits", measure_cache_hits);
1570 span.record("measure_us", measure_time.as_micros() as u64);
1571 self.last_solve_time += self.last_solve_elapsed;
1572 return;
1573 }
1574
1575 self.solve_generation = self.solve_generation.saturating_add(1);
1576 if let Some(root_node) = root_node {
1577 span.record("root", tracing::field::debug(root_node));
1578 self.last_solve_root = Some(root_node);
1579 self.mark_solved_subtree(root_node);
1580 fn key_bits(axis: AvailableSpace) -> u64 {
1581 match axis {
1582 AvailableSpace::Definite(px) => px.0.to_bits() as u64,
1583 AvailableSpace::MinContent => 1u64 << 32,
1584 AvailableSpace::MaxContent => 2u64 << 32,
1585 }
1586 }
1587 if let Some(frame_id) = self.frame_id {
1588 self.root_solve_stamp.insert(
1589 root_node,
1590 RootSolveStamp {
1591 frame_id,
1592 key: RootSolveKey {
1593 width_bits: key_bits(available.width),
1594 height_bits: key_bits(available.height),
1595 scale_bits: self.solve_scale_factor.to_bits(),
1596 },
1597 },
1598 );
1599 }
1600 } else {
1601 self.last_solve_root = None;
1602 }
1603 self.last_solve_elapsed = started.elapsed();
1604 span.record("elapsed_us", self.last_solve_elapsed.as_micros() as u64);
1605 span.record("measure_calls", measure_calls);
1606 span.record("measure_cache_hits", measure_cache_hits);
1607 span.record("measure_us", measure_time.as_micros() as u64);
1608 self.last_solve_time += self.last_solve_elapsed;
1609 }
1610
1611 pub fn compute_root(
1612 &mut self,
1613 root: LayoutId,
1614 available: LayoutSize<AvailableSpace>,
1615 scale_factor: f32,
1616 ) {
1617 self.compute_root_with_measure(root, available, scale_factor, |_node, _constraints| {
1618 Size::default()
1619 });
1620 }
1621
1622 pub fn layout_rect(&self, id: LayoutId) -> Rect {
1623 let Ok(layout) = self.tree.layout(id.0) else {
1624 return Rect::new(Point::new(Px(0.0), Px(0.0)), Size::default());
1625 };
1626 let sf = if self.solve_scale_factor.is_finite() && self.solve_scale_factor > 0.0 {
1627 self.solve_scale_factor
1628 } else {
1629 1.0
1630 };
1631 let min_x_dp = layout.location.x;
1632 let min_y_dp = layout.location.y;
1633 let max_x_dp = layout.location.x + layout.size.width;
1634 let max_y_dp = layout.location.y + layout.size.height;
1635
1636 let snap_edge_dp = |v: f32| if v.is_finite() { v.round() } else { 0.0 };
1637
1638 let snapped_min_x_dp = snap_edge_dp(min_x_dp);
1639 let snapped_min_y_dp = snap_edge_dp(min_y_dp);
1640 let snapped_max_x_dp = snap_edge_dp(max_x_dp);
1641 let snapped_max_y_dp = snap_edge_dp(max_y_dp);
1642
1643 Rect::new(
1644 Point::new(Px(snapped_min_x_dp / sf), Px(snapped_min_y_dp / sf)),
1645 Size::new(
1646 Px(((snapped_max_x_dp - snapped_min_x_dp) / sf).max(0.0)),
1647 Px(((snapped_max_y_dp - snapped_min_y_dp) / sf).max(0.0)),
1648 ),
1649 )
1650 }
1651
1652 pub fn debug_dump_subtree_json(
1653 &self,
1654 root: NodeId,
1655 mut label_for_node: impl FnMut(NodeId) -> Option<String>,
1656 ) -> serde_json::Value {
1657 self.debug_dump_subtree_json_with_info(root, |node| DebugDumpNodeInfo {
1658 label: label_for_node(node),
1659 debug: None,
1660 })
1661 }
1662
1663 pub fn debug_dump_subtree_json_with_info(
1666 &self,
1667 root: NodeId,
1668 mut info_for_node: impl FnMut(NodeId) -> DebugDumpNodeInfo,
1669 ) -> serde_json::Value {
1670 fn sanitize_for_filename(s: &str) -> String {
1671 s.chars()
1672 .map(|ch| match ch {
1673 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' => ch,
1674 _ => '_',
1675 })
1676 .collect()
1677 }
1678
1679 fn abs_rect_for_node(
1680 engine: &TaffyLayoutEngine,
1681 node: NodeId,
1682 abs_cache: &mut FxHashMap<NodeId, Rect>,
1683 ) -> Rect {
1684 if let Some(rect) = abs_cache.get(&node).copied() {
1685 return rect;
1686 }
1687
1688 let local = engine
1689 .layout_id_for_node(node)
1690 .map(|id| engine.layout_rect(id))
1691 .unwrap_or_else(|| Rect::new(Point::new(Px(0.0), Px(0.0)), Size::default()));
1692 let abs = if let Some(parent) = engine.parent.get(node).copied() {
1693 let parent_abs = abs_rect_for_node(engine, parent, abs_cache);
1694 Rect::new(
1695 Point::new(
1696 Px(parent_abs.origin.x.0 + local.origin.x.0),
1697 Px(parent_abs.origin.y.0 + local.origin.y.0),
1698 ),
1699 local.size,
1700 )
1701 } else {
1702 local
1703 };
1704 abs_cache.insert(node, abs);
1705 abs
1706 }
1707
1708 let root_layout_id = self.layout_id_for_node(root);
1709 let mut stack: Vec<NodeId> = vec![root];
1710 let mut visited: std::collections::HashSet<NodeId> = std::collections::HashSet::new();
1711 let mut abs_cache: FxHashMap<NodeId, Rect> = FxHashMap::default();
1712 let mut nodes: Vec<serde_json::Value> = Vec::new();
1713
1714 while let Some(node) = stack.pop() {
1715 if !visited.insert(node) {
1716 continue;
1717 }
1718
1719 let children = self.children.get(node).cloned().unwrap_or_default();
1720 for &child in children.iter().rev() {
1721 stack.push(child);
1722 }
1723
1724 let layout_id = self.layout_id_for_node(node);
1725 let local = layout_id
1726 .map(|id| self.layout_rect(id))
1727 .unwrap_or_else(|| Rect::new(Point::new(Px(0.0), Px(0.0)), Size::default()));
1728 let abs = abs_rect_for_node(self, node, &mut abs_cache);
1729
1730 let measured = layout_id
1731 .and_then(|id| self.tree.get_node_context(id.0).copied())
1732 .map(|ctx| ctx.measured)
1733 .unwrap_or(false);
1734
1735 let style_dbg = self
1736 .styles
1737 .get(node)
1738 .map(|s| format!("{s:?}"))
1739 .unwrap_or_else(|| "<missing>".to_string());
1740
1741 let info = info_for_node(node);
1742 let label_dbg = info.label.unwrap_or_else(|| "<unknown>".to_string());
1743
1744 let mut node_json = json!({
1745 "node": format!("{node:?}"),
1746 "label": label_dbg,
1747 "measured": measured,
1748 "parent": self.parent.get(node).map(|p| format!("{p:?}")),
1749 "children": children.iter().map(|c| format!("{c:?}")).collect::<Vec<_>>(),
1750 "layout_id": layout_id.map(|id| format!("{:?}", id.0)).unwrap_or_else(|| "<missing>".to_string()),
1751 "local_rect": {
1752 "x": local.origin.x.0,
1753 "y": local.origin.y.0,
1754 "w": local.size.width.0,
1755 "h": local.size.height.0,
1756 },
1757 "abs_rect": {
1758 "x": abs.origin.x.0,
1759 "y": abs.origin.y.0,
1760 "w": abs.size.width.0,
1761 "h": abs.size.height.0,
1762 },
1763 "style": style_dbg,
1764 });
1765 if let Some(debug) = info.debug
1766 && let Some(obj) = node_json.as_object_mut()
1767 {
1768 obj.insert("debug".to_string(), debug);
1769 }
1770 nodes.push(node_json);
1771 }
1772
1773 let filename = sanitize_for_filename(&format!("{root:?}"));
1774 json!({
1775 "meta": {
1776 "root": format!("{root:?}"),
1777 "root_layout_id": root_layout_id.map(|id| format!("{:?}", id.0)),
1778 "frame_id": self.frame_id.map(|id| id.0),
1779 "solve_generation": self.solve_generation,
1780 "solve_scale_factor": self.solve_scale_factor,
1781 "last_solve_time_ms": self.last_solve_time.as_millis(),
1782 "suggested_filename": format!("taffy_{filename}.json"),
1783 },
1784 "nodes": nodes,
1785 })
1786 }
1787
1788 pub fn debug_independent_root_nodes(&self) -> Vec<NodeId> {
1789 self.node_to_layout
1790 .iter()
1791 .filter_map(|(node, _)| self.parent.get(node).is_none().then_some(node))
1792 .collect()
1793 }
1794
1795 pub fn debug_write_subtree_json(
1796 &self,
1797 root: NodeId,
1798 dir: impl AsRef<Path>,
1799 filename: impl AsRef<Path>,
1800 label_for_node: impl FnMut(NodeId) -> Option<String>,
1801 ) -> std::io::Result<PathBuf> {
1802 let dir = dir.as_ref();
1803 std::fs::create_dir_all(dir)?;
1804 let path = dir.join(filename);
1805 let dump = self.debug_dump_subtree_json(root, label_for_node);
1806 let bytes = match serde_json::to_vec_pretty(&dump) {
1807 Ok(bytes) => bytes,
1808 Err(err) => {
1809 if crate::strict_runtime::strict_runtime_enabled() {
1810 panic!("serialize taffy debug json failed: {err:?}");
1811 }
1812
1813 tracing::warn!(?err, ?root, "serialize taffy debug json failed");
1814
1815 let fallback = json!({
1816 "error": "serialize taffy debug json failed",
1817 "detail": err.to_string(),
1818 "root": format!("{root:?}"),
1819 });
1820
1821 serde_json::to_vec_pretty(&fallback).unwrap_or_else(|_| {
1822 b"{\"error\":\"serialize taffy debug json failed\"}\n".to_vec()
1823 })
1824 }
1825 };
1826 std::fs::write(&path, bytes)?;
1827 Ok(path)
1828 }
1829
1830 fn mark_solved_subtree(&mut self, root: NodeId) {
1831 let Some(frame_id) = self.frame_id else {
1832 return;
1833 };
1834 if !self.is_seen(root) {
1844 return;
1845 }
1846 self.mark_solved_stack_scratch.clear();
1847 self.mark_solved_stack_scratch.push(root);
1848 while let Some(node) = self.mark_solved_stack_scratch.pop() {
1849 if !self.is_seen(node) {
1850 continue;
1851 }
1852 self.node_solved_stamp.insert(
1853 node,
1854 SolvedStamp {
1855 frame_id,
1856 solve_generation: self.solve_generation,
1857 },
1858 );
1859 if let Some(children) = self.children.get(node) {
1860 for &child in children {
1861 if self.is_seen(child) {
1862 self.mark_solved_stack_scratch.push(child);
1863 }
1864 }
1865 }
1866 }
1867 }
1868
1869 fn clear_solved_subtree(&mut self, root: NodeId) {
1870 self.clear_solved_stack_scratch.clear();
1871 self.clear_solved_stack_scratch.push(root);
1872 while let Some(node) = self.clear_solved_stack_scratch.pop() {
1873 self.node_solved_stamp.remove(node);
1874 if let Some(children) = self.children.get(node) {
1875 self.clear_solved_stack_scratch
1876 .extend(children.iter().copied());
1877 }
1878 }
1879 }
1880}
1881
1882#[cfg(test)]
1883mod tests {
1884 use super::*;
1885 use slotmap::SlotMap;
1886
1887 fn fresh_node_ids(count: usize) -> Vec<NodeId> {
1888 let mut map: SlotMap<NodeId, ()> = SlotMap::with_key();
1889 (0..count).map(|_| map.insert(())).collect()
1890 }
1891
1892 #[test]
1893 fn multiple_roots_do_not_couple_layout_results() {
1894 let [root_a, root_b, child_a, child_b] = fresh_node_ids(4).try_into().unwrap();
1895
1896 let mut engine = TaffyLayoutEngine::default();
1897 engine.begin_frame(FrameId(1));
1898
1899 engine.set_children(root_a, &[child_a]);
1900 engine.set_children(root_b, &[child_b]);
1901
1902 engine.set_style(
1903 root_a,
1904 taffy::Style {
1905 display: taffy::style::Display::Block,
1906 size: taffy::geometry::Size {
1907 width: taffy::style::Dimension::length(100.0),
1908 height: taffy::style::Dimension::length(10.0),
1909 },
1910 ..Default::default()
1911 },
1912 );
1913 engine.set_style(
1914 root_b,
1915 taffy::Style {
1916 display: taffy::style::Display::Block,
1917 size: taffy::geometry::Size {
1918 width: taffy::style::Dimension::length(200.0),
1919 height: taffy::style::Dimension::length(10.0),
1920 },
1921 ..Default::default()
1922 },
1923 );
1924
1925 let fill = taffy::Style {
1926 display: taffy::style::Display::Block,
1927 size: taffy::geometry::Size {
1928 width: taffy::style::Dimension::percent(1.0),
1929 height: taffy::style::Dimension::percent(1.0),
1930 },
1931 ..Default::default()
1932 };
1933 engine.set_style(child_a, fill.clone());
1934 engine.set_style(child_b, fill);
1935
1936 let root_a_id = engine.layout_id_for_node(root_a).unwrap();
1937 let root_b_id = engine.layout_id_for_node(root_b).unwrap();
1938
1939 engine.compute_root(
1940 root_a_id,
1941 LayoutSize::new(
1942 AvailableSpace::Definite(Px(100.0)),
1943 AvailableSpace::Definite(Px(10.0)),
1944 ),
1945 1.0,
1946 );
1947
1948 let child_a_id = engine.layout_id_for_node(child_a).unwrap();
1949 let a_before = engine.layout_rect(child_a_id);
1950 assert!((a_before.size.width.0 - 100.0).abs() < 0.01);
1951 assert!((a_before.size.height.0 - 10.0).abs() < 0.01);
1952
1953 engine.compute_root(
1954 root_b_id,
1955 LayoutSize::new(
1956 AvailableSpace::Definite(Px(200.0)),
1957 AvailableSpace::Definite(Px(10.0)),
1958 ),
1959 1.0,
1960 );
1961
1962 let child_b_id = engine.layout_id_for_node(child_b).unwrap();
1963 let b = engine.layout_rect(child_b_id);
1964 assert!((b.size.width.0 - 200.0).abs() < 0.01);
1965 assert!((b.size.height.0 - 10.0).abs() < 0.01);
1966
1967 let a_after = engine.layout_rect(child_a_id);
1968 assert_eq!(a_before, a_after);
1969
1970 assert_eq!(
1971 engine.child_layout_rect_if_solved(root_a, child_a),
1972 Some(a_after),
1973 "solved subtree rects should remain readable after solving an unrelated root"
1974 );
1975 }
1976
1977 #[test]
1978 fn barrier_batch_solve_stamps_multiple_roots_in_one_generation() {
1979 let [root_a, root_b, child_a, child_b] = fresh_node_ids(4).try_into().unwrap();
1980
1981 let mut engine = TaffyLayoutEngine::default();
1982 engine.begin_frame(FrameId(1));
1983
1984 engine.set_children(root_a, &[child_a]);
1985 engine.set_children(root_b, &[child_b]);
1986
1987 engine.set_style(
1988 root_a,
1989 taffy::Style {
1990 display: taffy::style::Display::Block,
1991 size: taffy::geometry::Size {
1992 width: taffy::style::Dimension::length(100.0),
1993 height: taffy::style::Dimension::length(10.0),
1994 },
1995 ..Default::default()
1996 },
1997 );
1998 engine.set_style(
1999 root_b,
2000 taffy::Style {
2001 display: taffy::style::Display::Block,
2002 size: taffy::geometry::Size {
2003 width: taffy::style::Dimension::length(200.0),
2004 height: taffy::style::Dimension::length(20.0),
2005 },
2006 ..Default::default()
2007 },
2008 );
2009
2010 let fill = taffy::Style {
2011 display: taffy::style::Display::Block,
2012 size: taffy::geometry::Size {
2013 width: taffy::style::Dimension::percent(1.0),
2014 height: taffy::style::Dimension::percent(1.0),
2015 },
2016 ..Default::default()
2017 };
2018 engine.set_style(child_a, fill.clone());
2019 engine.set_style(child_b, fill);
2020
2021 let roots = [
2022 (
2023 root_a,
2024 LayoutSize::new(
2025 AvailableSpace::Definite(Px(100.0)),
2026 AvailableSpace::Definite(Px(10.0)),
2027 ),
2028 ),
2029 (
2030 root_b,
2031 LayoutSize::new(
2032 AvailableSpace::Definite(Px(200.0)),
2033 AvailableSpace::Definite(Px(20.0)),
2034 ),
2035 ),
2036 ];
2037
2038 engine.compute_independent_roots_with_measure_if_needed(&roots, 1.0, |_node, _c| {
2039 Size::default()
2040 });
2041 assert_eq!(engine.solve_count(), 1);
2042
2043 let a = engine.layout_rect(engine.layout_id_for_node(child_a).unwrap());
2044 assert!((a.size.width.0 - 100.0).abs() < 0.01);
2045 assert!((a.size.height.0 - 10.0).abs() < 0.01);
2046
2047 let b = engine.layout_rect(engine.layout_id_for_node(child_b).unwrap());
2048 assert!((b.size.width.0 - 200.0).abs() < 0.01);
2049 assert!((b.size.height.0 - 20.0).abs() < 0.01);
2050
2051 engine.compute_independent_roots_with_measure_if_needed(&roots, 1.0, |_node, _c| {
2053 Size::default()
2054 });
2055 assert_eq!(engine.solve_count(), 1);
2056 }
2057
2058 #[test]
2059 fn layout_rect_snaps_edges_not_location_and_size() {
2060 let [root, child] = fresh_node_ids(2).try_into().unwrap();
2061
2062 let mut engine = TaffyLayoutEngine::default();
2063 engine.begin_frame(FrameId(1));
2064
2065 engine.set_children(root, &[child]);
2066
2067 engine.set_style(
2068 root,
2069 taffy::Style {
2070 display: taffy::style::Display::Block,
2071 ..Default::default()
2072 },
2073 );
2074
2075 engine.set_style(
2076 child,
2077 taffy::Style {
2078 display: taffy::style::Display::Block,
2079 size: taffy::geometry::Size {
2080 width: taffy::style::Dimension::length(0.5),
2081 height: taffy::style::Dimension::length(0.5),
2082 },
2083 margin: taffy::geometry::Rect {
2084 left: taffy::style::LengthPercentageAuto::length(0.5),
2085 right: taffy::style::LengthPercentageAuto::auto(),
2086 top: taffy::style::LengthPercentageAuto::auto(),
2087 bottom: taffy::style::LengthPercentageAuto::auto(),
2088 },
2089 ..Default::default()
2090 },
2091 );
2092
2093 let root_id = engine.layout_id_for_node(root).unwrap();
2094 engine.compute_root(
2095 root_id,
2096 LayoutSize::new(
2097 AvailableSpace::Definite(Px(10.0)),
2098 AvailableSpace::Definite(Px(10.0)),
2099 ),
2100 2.0,
2101 );
2102
2103 let child_id = engine.layout_id_for_node(child).unwrap();
2104 let rect = engine.layout_rect(child_id);
2105 assert!((rect.origin.x.0 - 0.5).abs() < 0.0001);
2106 assert!((rect.size.width.0 - 0.0).abs() < 0.0001);
2107 }
2108
2109 #[test]
2110 fn end_frame_prunes_stale_children_from_live_parent_edges() {
2111 let [root, live_child, stale_child] = fresh_node_ids(3).try_into().unwrap();
2112
2113 let mut engine = TaffyLayoutEngine::default();
2114 engine.begin_frame(FrameId(1));
2115
2116 engine.set_children(root, &[live_child, stale_child]);
2117
2118 let block = taffy::Style {
2119 display: taffy::style::Display::Block,
2120 size: taffy::geometry::Size {
2121 width: taffy::style::Dimension::length(100.0),
2122 height: taffy::style::Dimension::length(20.0),
2123 },
2124 ..Default::default()
2125 };
2126 engine.set_style(root, block.clone());
2127 engine.set_style(live_child, block.clone());
2128 engine.set_style(stale_child, block);
2129
2130 engine.end_frame();
2131
2132 engine.begin_frame(FrameId(2));
2133 engine.mark_seen(root);
2134 engine.mark_seen(live_child);
2135
2136 engine.end_frame();
2137
2138 assert!(
2139 engine.layout_id_for_node(stale_child).is_none(),
2140 "stale child layout node should be swept at end of frame"
2141 );
2142 assert_eq!(
2143 engine.children.get(root).map(Vec::as_slice),
2144 Some([live_child].as_slice()),
2145 "sweeping a stale child must also sever the live parent's child edge"
2146 );
2147 assert_eq!(
2148 engine.parent.get(live_child).copied(),
2149 Some(root),
2150 "live sibling parent pointers must survive stale-child pruning"
2151 );
2152
2153 let dump = engine.debug_dump_subtree_json(root, |node| {
2154 Some(match node {
2155 n if n == root => "root".to_string(),
2156 n if n == live_child => "live".to_string(),
2157 n if n == stale_child => "stale".to_string(),
2158 _ => format!("{node:?}"),
2159 })
2160 });
2161 let dump_text = dump.to_string();
2162 assert!(
2163 !dump_text.contains("stale"),
2164 "debug dump should not retain swept stale children under a live parent: {dump_text}"
2165 );
2166 assert!(
2167 !dump_text.contains("<unknown>"),
2168 "debug dump should not expose placeholder unknown nodes after stale-child pruning: {dump_text}"
2169 );
2170 }
2171}