Skip to main content

fret_ui/layout/
engine.rs

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    /// Compute multiple independent roots in a single Taffy solve when possible.
459    ///
460    /// This is primarily used by layout barriers (scroll/virtualization/etc.) that need to solve
461    /// many child roots. Solving them one-by-one can amplify fixed per-solve overhead into
462    /// tail-latency spikes (e.g. virtual list "jump to bottom").
463    ///
464    /// Safety: this batches a *subset* of roots only when all of the following hold for that root:
465    /// - the root is a layout root in the engine (no recorded parent in `self.parent`)
466    /// - the root has a *definite* available size in both axes
467    /// - the root is not already solved for its `(available, scale_factor)` key
468    ///
469    /// Non-batchable roots fall back to per-root compute.
470    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            // Only batch truly-independent roots. If the engine already knows about a parent,
511            // computing under a synthetic wrapper could interfere with the parent solve stamp.
512            //
513            // Note: `self.parent` may contain stale entries when a node was previously attached
514            // under a parent but has since been detached without an explicit `set_children` update
515            // (e.g. sweep/retention edge cases). Treat "parent points to a node that doesn't list
516            // us as a child" as stale and clear it so independent-root batching can proceed.
517            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            // Batching relies on roots being sized via `build_viewport_flow_subtree`'s
530            // `root_override_size` (i.e. definite in both axes). If a caller needs unbounded
531            // (max-content) root sizing, fall back to per-root compute.
532            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        // Avoid 0-sized synthetic roots; a tiny definite box is enough to make percent-sizing and
575        // alignment stable if any child happens to depend on its containing block.
576        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            // A minimal, definite-sized containing block for batching independent root solves.
594            //
595            // `Display::Block` avoids flex layout machinery on the synthetic parent while still
596            // allowing children to resolve percent-based sizes against a stable containing block.
597            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        // Run a single solve on the synthetic root. We intentionally do NOT couple the solve stamp
651        // to the synthetic node; instead we stamp each real root independently below.
652        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        // Detach roots from the synthetic parent so they remain independent roots in the engine.
886        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        // Fast path: `build_flow_subtree` calls `set_children` for every visited node, and most
1037        // nodes keep their child lists stable across frames. Avoid per-frame dedupe work for large
1038        // child lists (e.g. long stacks/lists).
1039        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                    // Only patch `auto`-sized items that rely on flex-wrap to avoid squishing.
1290                    //
1291                    // This mirrors the web default `min-width: auto` / `min-height: auto` behavior
1292                    // that prevents wrapped flex items (e.g. `flex-1` buttons) from shrinking below
1293                    // their intrinsic min-content size unless an explicit min-size override (e.g.
1294                    // `min-w-0`) is applied.
1295                    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                    // Taffy supports `MinContent`/`MaxContent` probes for leaf nodes, but some
1314                    // internal layout containers (notably shrink-wrapped grid wrappers) may report
1315                    // an empty intrinsic size under `MinContent` even when they have non-empty
1316                    // children.
1317                    //
1318                    // Fall back to a `MaxContent` probe in those cases so overflow-visible
1319                    // interactive wrappers (e.g. shadcn-style `Pressable` controls inside a
1320                    // wrapping row) keep a non-zero main-axis allocation and don't collapse to
1321                    // `w=0` hit-test bounds.
1322                    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            // Continue traversing the subtree.
1344            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    /// Dump a layout subtree with both a human-readable label and optional structured debug
1664    /// metadata per node.
1665    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        // Only stamp nodes that are part of the engine's current "seen" set for this frame.
1835        //
1836        // The engine can retain stale `children` edges (e.g. when a node was previously attached
1837        // but later detached without an explicit `set_children` update). Stamping through those
1838        // stale edges needlessly inflates per-solve work and can amplify tail-latency spikes on
1839        // multi-root frames (window roots + overlays + detached flow roots).
1840        //
1841        // Using the "seen" set keeps stamping proportional to the subtree that was actually
1842        // requested/built for the current frame.
1843        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        // Solving again for the same keys should be a no-op.
2052        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}