Skip to main content

fission_layout/
lib.rs

1//! Constraint-based layout engine for the Fission UI framework.
2//!
3//! This crate takes a flat list of [`LayoutInputNode`]s (produced from the
4//! [`fission-ir`](fission_ir) intermediate representation) and computes the
5//! absolute position and size of every node on screen. It implements:
6//!
7//! * **Box layout** -- constrained containers with padding, min/max, and aspect ratio.
8//! * **Flexbox** -- single-axis distribution with grow, shrink, wrap, alignment, and justification.
9//! * **CSS Grid** -- two-dimensional track-based layout with `fr`, `%`, and fixed sizing.
10//! * **Scroll containers** -- clipped viewports with infinite content axes.
11//! * **Absolute positioning** -- `top`/`left`/`right`/`bottom` offsets.
12//! * **ZStack** -- overlapping children.
13//! * **Flyout anchoring** -- popups positioned relative to an anchor node.
14//!
15//! The engine is pure computation with no platform dependencies. Give it nodes and
16//! a viewport size, and it returns a [`LayoutSnapshot`] mapping every
17//! [`NodeId`](fission_ir::NodeId) to a [`LayoutRect`].
18//!
19//! # Example
20//!
21//! ```rust,no_run
22//! use fission_layout::*;
23//! use fission_ir::{NodeId, LayoutOp};
24//!
25//! let mut engine = LayoutEngine::new();
26//! let root_id = NodeId::explicit("root");
27//! // ... build LayoutInputNode list ...
28//! // let snapshot = engine.compute_layout(&nodes, root_id, viewport, &|_| 0.0).unwrap();
29//! ```
30
31use anyhow::Result;
32use fission_diagnostics::prelude as diag;
33use fission_ir::op::{RichTextAnnotation, TextParagraphStyle, TextRun};
34use fission_ir::{FlexDirection as IrFlexDirection, FlexWrap as IrFlexWrap, NodeId};
35use serde::{Deserialize, Serialize};
36use std::collections::hash_map::DefaultHasher;
37use std::collections::{HashMap, HashSet};
38use std::hash::{Hash, Hasher};
39use std::sync::Arc;
40
41pub use fission_ir::{FlexDirection, GridPlacement, GridTrack, LayoutOp};
42
43/// A source of scroll offsets for scroll containers.
44///
45/// The layout engine calls [`get_offset`](ScrollDataSource::get_offset) for each
46/// [`LayoutOp::Scroll`] node to learn how far the user has scrolled. Platform
47/// backends implement this trait (or pass a closure, which also implements it).
48///
49/// # Example
50///
51/// ```rust
52/// use fission_layout::ScrollDataSource;
53/// use fission_ir::NodeId;
54///
55/// // A closure works as a ScrollDataSource:
56/// let source = |_node: NodeId| -> f32 { 0.0 };
57/// assert_eq!(source.get_offset(NodeId::explicit("scroll")), 0.0);
58/// ```
59pub trait ScrollDataSource {
60    /// Returns the current scroll offset for the given scroll container node.
61    fn get_offset(&self, node_id: NodeId) -> f32;
62}
63
64impl<F> ScrollDataSource for F
65where
66    F: Fn(NodeId) -> f32,
67{
68    fn get_offset(&self, node_id: NodeId) -> f32 {
69        self(node_id)
70    }
71}
72
73/// The scalar type used for all layout measurements.
74///
75/// Currently `f32`. Matches [`fission_ir::op::LayoutUnit`].
76pub type LayoutUnit = f32;
77
78/// Returns `value` if it is finite, otherwise `fallback`.
79fn finite_or(value: LayoutUnit, fallback: LayoutUnit) -> LayoutUnit {
80    if value.is_finite() {
81        value
82    } else {
83        fallback
84    }
85}
86
87/// A 2D point in layout coordinate space.
88///
89/// Represents an (x, y) position in logical pixels. Used for node origins and
90/// coordinate calculations throughout the layout engine.
91#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
92pub struct LayoutPoint {
93    /// Horizontal position in logical pixels.
94    pub x: LayoutUnit,
95    /// Vertical position in logical pixels.
96    pub y: LayoutUnit,
97}
98
99impl LayoutPoint {
100    /// The origin point: `(0.0, 0.0)`.
101    pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
102
103    /// Creates a new point from x and y coordinates.
104    pub fn new(x: LayoutUnit, y: LayoutUnit) -> Self {
105        Self { x, y }
106    }
107}
108
109/// A 2D size in layout coordinate space.
110///
111/// Represents a width and height in logical pixels. Used as the output of layout
112/// measurement and as input to constraints.
113#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
114pub struct LayoutSize {
115    /// Width in logical pixels.
116    pub width: LayoutUnit,
117    /// Height in logical pixels.
118    pub height: LayoutUnit,
119}
120
121impl LayoutSize {
122    /// A zero-sized size: `(0.0, 0.0)`.
123    pub const ZERO: Self = Self {
124        width: 0.0,
125        height: 0.0,
126    };
127
128    /// Creates a new size from width and height values.
129    pub fn new(width: LayoutUnit, height: LayoutUnit) -> Self {
130        Self { width, height }
131    }
132}
133
134/// Minimum and maximum width/height bounds passed from parent to child during layout.
135///
136/// `BoxConstraints` is the fundamental mechanism for top-down size negotiation. A
137/// parent creates constraints describing the space available to a child, and the
138/// child returns a [`LayoutSize`] that satisfies those constraints.
139///
140/// There are two common patterns:
141///
142/// * **Tight constraints** -- `min == max`, forcing the child to a specific size.
143///   Created with [`BoxConstraints::tight`].
144/// * **Loose constraints** -- `min == 0`, giving the child freedom to be smaller
145///   than the max. Created with [`BoxConstraints::loose`].
146///
147/// # Example
148///
149/// ```rust
150/// use fission_layout::{BoxConstraints, LayoutSize};
151///
152/// let constraints = BoxConstraints::loose(800.0, 600.0);
153/// assert_eq!(constraints.min_w, 0.0);
154///
155/// let child_wants = LayoutSize::new(300.0, 200.0);
156/// let actual = constraints.constrain(child_wants);
157/// assert_eq!(actual, child_wants); // fits within the constraints
158/// ```
159#[derive(Debug, Clone, Copy, PartialEq)]
160pub struct BoxConstraints {
161    /// Minimum width the child must occupy.
162    pub min_w: LayoutUnit,
163    /// Maximum width the child may occupy. Can be `f32::INFINITY` for unbounded.
164    pub max_w: LayoutUnit,
165    /// Minimum height the child must occupy.
166    pub min_h: LayoutUnit,
167    /// Maximum height the child may occupy. Can be `f32::INFINITY` for unbounded.
168    pub max_h: LayoutUnit,
169}
170
171impl BoxConstraints {
172    /// Creates tight constraints that force a child to exactly `size`.
173    ///
174    /// Both min and max are set to the given width/height.
175    pub fn tight(size: LayoutSize) -> Self {
176        Self {
177            min_w: size.width,
178            max_w: size.width,
179            min_h: size.height,
180            max_h: size.height,
181        }
182    }
183
184    /// Creates loose constraints: min is zero, max is the given values.
185    ///
186    /// The child can be anywhere from zero to `max_w` x `max_h`.
187    pub fn loose(max_w: LayoutUnit, max_h: LayoutUnit) -> Self {
188        Self {
189            min_w: 0.0,
190            max_w,
191            min_h: 0.0,
192            max_h,
193        }
194    }
195
196    /// Returns `true` if the maximum width is finite (not `f32::INFINITY`).
197    pub fn is_width_bounded(&self) -> bool {
198        self.max_w.is_finite()
199    }
200
201    /// Returns `true` if the maximum height is finite (not `f32::INFINITY`).
202    pub fn is_height_bounded(&self) -> bool {
203        self.max_h.is_finite()
204    }
205
206    /// Clamps `size` so it falls within these constraints.
207    ///
208    /// The returned width is `max(min_w, min(size.width, max_w))`, and likewise
209    /// for height.
210    pub fn constrain(&self, size: LayoutSize) -> LayoutSize {
211        LayoutSize {
212            width: size.width.max(self.min_w).min(self.max_w),
213            height: size.height.max(self.min_h).min(self.max_h),
214        }
215    }
216
217    /// Returns the smallest size that satisfies these constraints: `(min_w, min_h)`.
218    pub fn smallest(&self) -> LayoutSize {
219        LayoutSize::new(self.min_w, self.min_h)
220    }
221
222    /// Returns new constraints shrunk inward by `padding`.
223    ///
224    /// Padding is `[left, right, top, bottom]`. Horizontal padding reduces the
225    /// width bounds; vertical padding reduces the height bounds. Bounds are
226    /// clamped to zero.
227    pub fn deflate(&self, padding: [LayoutUnit; 4]) -> Self {
228        let horiz = padding[0] + padding[1];
229        let vert = padding[2] + padding[3];
230        let max_w = (self.max_w - horiz).max(0.0);
231        let max_h = (self.max_h - vert).max(0.0);
232        let min_w = (self.min_w - horiz).max(0.0).min(max_w);
233        let min_h = (self.min_h - vert).max(0.0).min(max_h);
234        Self {
235            min_w,
236            max_w,
237            min_h,
238            max_h,
239        }
240    }
241
242    /// Makes the constraints tighter by fixing the width and/or height.
243    ///
244    /// If `width` is `Some`, both `min_w` and `max_w` are set to that value
245    /// (clamped to the current bounds). Same for `height`.
246    pub fn tighten(&self, width: Option<LayoutUnit>, height: Option<LayoutUnit>) -> Self {
247        let mut out = *self;
248        if let Some(w) = width {
249            let clamped = w.min(out.max_w).max(out.min_w);
250            out.min_w = clamped;
251            out.max_w = clamped;
252        }
253        if let Some(h) = height {
254            let clamped = h.min(out.max_h).max(out.min_h);
255            out.min_h = clamped;
256            out.max_h = clamped;
257        }
258        if out.max_w < out.min_w {
259            out.max_w = out.min_w;
260        }
261        if out.max_h < out.min_h {
262            out.max_h = out.min_h;
263        }
264        out
265    }
266
267    /// Applies additional min/max constraints on top of the current ones.
268    ///
269    /// Each `Some` value further restricts the corresponding bound. `None` values
270    /// leave the bound unchanged. After adjustment, max is clamped to be at least
271    /// min.
272    pub fn apply_min_max(
273        &self,
274        min_w: Option<LayoutUnit>,
275        max_w: Option<LayoutUnit>,
276        min_h: Option<LayoutUnit>,
277        max_h: Option<LayoutUnit>,
278    ) -> Self {
279        let mut out = *self;
280        if let Some(w) = min_w {
281            out.min_w = out.min_w.max(w);
282        }
283        if let Some(h) = min_h {
284            out.min_h = out.min_h.max(h);
285        }
286        if let Some(w) = max_w {
287            out.max_w = out.max_w.min(w);
288        }
289        if let Some(h) = max_h {
290            out.max_h = out.max_h.min(h);
291        }
292        if out.max_w < out.min_w {
293            out.max_w = out.min_w;
294        }
295        if out.max_h < out.min_h {
296            out.max_h = out.min_h;
297        }
298        out
299    }
300
301    /// Returns loose constraints with the same maximums but zeroed minimums.
302    ///
303    /// Useful when a parent wants to let a child be as small as it likes while
304    /// still capping its maximum size.
305    pub fn loosen(&self) -> Self {
306        Self {
307            min_w: 0.0,
308            max_w: self.max_w,
309            min_h: 0.0,
310            max_h: self.max_h,
311        }
312    }
313}
314
315#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
316struct MeasureCacheKey {
317    node_id: u128,
318    min_w: u32,
319    max_w: u32,
320    min_h: u32,
321    max_h: u32,
322}
323
324impl MeasureCacheKey {
325    fn new(node_id: NodeId, constraints: BoxConstraints) -> Self {
326        Self {
327            node_id: node_id.as_u128(),
328            min_w: constraints.min_w.to_bits(),
329            max_w: constraints.max_w.to_bits(),
330            min_h: constraints.min_h.to_bits(),
331            max_h: constraints.max_h.to_bits(),
332        }
333    }
334}
335
336#[derive(Debug, Clone, Default)]
337struct LayoutGraphValidationState {
338    duplicate_nodes: Vec<NodeId>,
339    missing_parent_refs: Vec<(NodeId, NodeId)>,
340    missing_child_refs: Vec<(NodeId, NodeId)>,
341    parent_child_mismatches: Vec<(NodeId, NodeId, Option<NodeId>)>,
342    cycle_nodes: Vec<NodeId>,
343    root_nodes: Vec<NodeId>,
344}
345
346impl LayoutGraphValidationState {
347    fn first_error(&self) -> Option<anyhow::Error> {
348        if let Some(node_id) = self.duplicate_nodes.first() {
349            return Some(anyhow::anyhow!(
350                "[layout] duplicate node id encountered during graph build: {:?}",
351                node_id
352            ));
353        }
354        if let Some((node_id, parent_id)) = self.missing_parent_refs.first() {
355            return Some(anyhow::anyhow!(
356                "[layout] node {:?} references missing parent {:?}",
357                node_id,
358                parent_id
359            ));
360        }
361        if let Some((node_id, child_id)) = self.missing_child_refs.first() {
362            return Some(anyhow::anyhow!(
363                "[layout] node {:?} references missing child {:?}",
364                node_id,
365                child_id
366            ));
367        }
368        if let Some((parent_id, child_id, actual_parent)) = self.parent_child_mismatches.first() {
369            return Some(anyhow::anyhow!(
370                "[layout] parent/child mismatch parent={:?} child={:?} child.parent_id={:?}",
371                parent_id,
372                child_id,
373                actual_parent
374            ));
375        }
376        if let Some(node_id) = self.cycle_nodes.first() {
377            return Some(anyhow::anyhow!(
378                "[layout] cycle detected while rebuilding graph at {:?}",
379                node_id
380            ));
381        }
382        None
383    }
384}
385
386#[derive(Debug, Clone, Default)]
387struct LayoutGraphState {
388    graph_version: u64,
389    last_layout_version: Option<u64>,
390    node_order: Vec<NodeId>,
391    node_fingerprints: HashMap<NodeId, u64>,
392    nodes: HashMap<NodeId, LayoutInputNode>,
393    parents: HashMap<NodeId, Option<NodeId>>,
394    children: HashMap<NodeId, Vec<NodeId>>,
395    roots: Vec<NodeId>,
396    validation: LayoutGraphValidationState,
397}
398
399#[derive(Debug, Clone, Default)]
400struct IncrementalLayoutReuseState {
401    previous_snapshot: LayoutSnapshot,
402    dirty_ancestors: HashSet<NodeId>,
403}
404
405impl LayoutGraphState {
406    fn is_empty(&self) -> bool {
407        self.nodes.is_empty()
408    }
409
410    fn mark_layout_complete(&mut self) {
411        self.last_layout_version = Some(self.graph_version);
412    }
413
414    fn matches_input_nodes(&self, input_nodes: &[LayoutInputNode]) -> bool {
415        if self.nodes.len() != input_nodes.len() || self.node_order.len() != input_nodes.len() {
416            return false;
417        }
418
419        for (expected_id, node) in self.node_order.iter().zip(input_nodes.iter()) {
420            if *expected_id != node.id {
421                return false;
422            }
423            let Some(existing) = self.node_fingerprints.get(&node.id) else {
424                return false;
425            };
426            if *existing != layout_input_fingerprint(node) {
427                return false;
428            }
429        }
430
431        true
432    }
433
434    fn from_input_nodes(input_nodes: &[LayoutInputNode], version: u64) -> Self {
435        let mut state = Self {
436            graph_version: version,
437            ..Self::default()
438        };
439        state.replace_all_nodes(input_nodes);
440        state
441    }
442
443    fn replace_all_nodes(&mut self, input_nodes: &[LayoutInputNode]) {
444        self.node_order.clear();
445        self.node_fingerprints.clear();
446        self.nodes.clear();
447        self.last_layout_version = None;
448
449        let mut validation = LayoutGraphValidationState::default();
450        let mut seen = HashSet::new();
451        for node in input_nodes {
452            if !seen.insert(node.id) {
453                validation.duplicate_nodes.push(node.id);
454            } else {
455                self.node_order.push(node.id);
456            }
457            self.node_fingerprints
458                .insert(node.id, layout_input_fingerprint(node));
459            self.nodes.insert(node.id, node.clone());
460        }
461
462        self.rebuild_topology(validation);
463    }
464
465    fn update_nodes(&mut self, input_nodes: &[LayoutInputNode]) {
466        let mut validation = LayoutGraphValidationState::default();
467        let mut seen = HashSet::new();
468        let mut next_order = Vec::with_capacity(input_nodes.len());
469        let mut next_fingerprints = HashMap::with_capacity(input_nodes.len());
470        let mut next_nodes = HashMap::with_capacity(input_nodes.len());
471
472        for node in input_nodes {
473            if !seen.insert(node.id) {
474                validation.duplicate_nodes.push(node.id);
475                continue;
476            }
477            next_order.push(node.id);
478            next_fingerprints.insert(node.id, layout_input_fingerprint(node));
479            next_nodes.insert(node.id, node.clone());
480        }
481
482        self.node_order = next_order;
483        self.node_fingerprints = next_fingerprints;
484        self.nodes = next_nodes;
485        self.last_layout_version = None;
486        self.rebuild_topology(validation);
487    }
488
489    fn rebuild_topology(&mut self, mut validation: LayoutGraphValidationState) {
490        self.parents.clear();
491        self.children.clear();
492        self.roots.clear();
493
494        for node_id in &self.node_order {
495            let Some(node) = self.nodes.get(node_id) else {
496                continue;
497            };
498            self.parents.insert(*node_id, node.parent_id);
499            self.children.insert(*node_id, node.children_ids.clone());
500            if node.parent_id.is_none() {
501                self.roots.push(*node_id);
502            } else if let Some(parent_id) = node.parent_id {
503                if !self.nodes.contains_key(&parent_id) {
504                    validation.missing_parent_refs.push((*node_id, parent_id));
505                }
506            }
507        }
508
509        for node_id in &self.node_order {
510            let Some(node) = self.nodes.get(node_id) else {
511                continue;
512            };
513            for child_id in &node.children_ids {
514                let Some(child) = self.nodes.get(child_id) else {
515                    validation.missing_child_refs.push((*node_id, *child_id));
516                    continue;
517                };
518                if child.parent_id != Some(*node_id) {
519                    validation
520                        .parent_child_mismatches
521                        .push((*node_id, *child_id, child.parent_id));
522                }
523            }
524        }
525
526        validation.root_nodes = self.roots.clone();
527        validation.cycle_nodes = self.detect_cycle_nodes();
528        self.validation = validation;
529    }
530
531    fn node(&self, node_id: NodeId) -> Option<&LayoutInputNode> {
532        self.nodes.get(&node_id)
533    }
534
535    fn children_of(&self, node_id: NodeId) -> &[NodeId] {
536        self.children
537            .get(&node_id)
538            .map(Vec::as_slice)
539            .unwrap_or(&[])
540    }
541
542    fn parent_of(&self, node_id: NodeId) -> Option<NodeId> {
543        self.parents.get(&node_id).copied().flatten()
544    }
545
546    fn ordered_nodes(&self) -> impl Iterator<Item = &LayoutInputNode> {
547        self.node_order
548            .iter()
549            .filter_map(|node_id| self.nodes.get(node_id))
550    }
551
552    fn detect_cycle_nodes(&self) -> Vec<NodeId> {
553        fn dfs(
554            node_id: NodeId,
555            children: &HashMap<NodeId, Vec<NodeId>>,
556            visited: &mut HashSet<NodeId>,
557            stack: &mut HashSet<NodeId>,
558            cycle_nodes: &mut Vec<NodeId>,
559        ) {
560            if stack.contains(&node_id) {
561                cycle_nodes.push(node_id);
562                return;
563            }
564            if !visited.insert(node_id) {
565                return;
566            }
567
568            stack.insert(node_id);
569            if let Some(child_nodes) = children.get(&node_id) {
570                for child_id in child_nodes {
571                    dfs(*child_id, children, visited, stack, cycle_nodes);
572                }
573            }
574            stack.remove(&node_id);
575        }
576
577        let mut visited = HashSet::new();
578        let mut stack = HashSet::new();
579        let mut cycle_nodes = Vec::new();
580        for node_id in &self.node_order {
581            dfs(
582                *node_id,
583                &self.children,
584                &mut visited,
585                &mut stack,
586                &mut cycle_nodes,
587            );
588        }
589        cycle_nodes.sort_by_key(|node_id| node_id.as_u128());
590        cycle_nodes.dedup();
591        cycle_nodes
592    }
593}
594
595#[cfg(test)]
596mod tests {
597    use super::{LayoutEngine, LayoutGraphState, LayoutInputNode};
598    use fission_ir::{LayoutOp, NodeId};
599
600    fn box_node(
601        id: NodeId,
602        parent_id: Option<NodeId>,
603        children_ids: Vec<NodeId>,
604    ) -> LayoutInputNode {
605        LayoutInputNode {
606            id,
607            parent_id,
608            op: LayoutOp::Box {
609                width: Some(40.0),
610                height: Some(20.0),
611                min_width: None,
612                max_width: None,
613                min_height: None,
614                max_height: None,
615                padding: [0.0; 4],
616                flex_grow: 0.0,
617                flex_shrink: 0.0,
618                aspect_ratio: None,
619            },
620            children_ids,
621            debug_name: format!("node-{}", id.as_u128()),
622            width: Some(40.0),
623            height: Some(20.0),
624            flex_grow: 0.0,
625            flex_shrink: 0.0,
626            rich_text: None,
627        }
628    }
629
630    #[test]
631    fn matches_input_nodes_rejects_reordered_flattened_inputs() {
632        let root = NodeId::from_u128(1);
633        let first = NodeId::from_u128(2);
634        let second = NodeId::from_u128(3);
635        let canonical = vec![
636            box_node(root, None, vec![first, second]),
637            box_node(first, Some(root), vec![]),
638            box_node(second, Some(root), vec![]),
639        ];
640        let reordered = vec![
641            box_node(root, None, vec![first, second]),
642            box_node(second, Some(root), vec![]),
643            box_node(first, Some(root), vec![]),
644        ];
645
646        let state = LayoutGraphState::from_input_nodes(&canonical, 1);
647        assert!(!state.matches_input_nodes(&reordered));
648    }
649
650    #[test]
651    fn update_refreshes_node_order_for_reordered_flattened_inputs() {
652        let root = NodeId::from_u128(10);
653        let first = NodeId::from_u128(11);
654        let second = NodeId::from_u128(12);
655        let canonical = vec![
656            box_node(root, None, vec![first, second]),
657            box_node(first, Some(root), vec![]),
658            box_node(second, Some(root), vec![]),
659        ];
660        let reordered = vec![
661            box_node(root, None, vec![first, second]),
662            box_node(second, Some(root), vec![]),
663            box_node(first, Some(root), vec![]),
664        ];
665
666        let mut engine = LayoutEngine::new();
667        engine.update(&canonical);
668        engine.update(&reordered);
669
670        let ordered = engine
671            .graph_state
672            .ordered_nodes()
673            .map(|node| node.id)
674            .collect::<Vec<_>>();
675        assert_eq!(ordered, vec![root, second, first]);
676    }
677}
678
679fn layout_input_fingerprint(node: &LayoutInputNode) -> u64 {
680    let mut hasher = DefaultHasher::new();
681    format!("{node:?}").hash(&mut hasher);
682    hasher.finish()
683}
684
685/// An axis-aligned rectangle: an origin point plus a size.
686///
687/// `LayoutRect` is the final output for every node after layout: it says exactly
688/// where the node sits on screen and how large it is.
689///
690/// # Example
691///
692/// ```rust
693/// use fission_layout::{LayoutRect, LayoutPoint};
694///
695/// let rect = LayoutRect::new(10.0, 20.0, 300.0, 200.0);
696/// assert_eq!(rect.right(), 310.0);
697/// assert!(rect.contains(LayoutPoint::new(15.0, 25.0)));
698/// ```
699#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
700pub struct LayoutRect {
701    /// The top-left corner of the rectangle.
702    pub origin: LayoutPoint,
703    /// The width and height of the rectangle.
704    pub size: LayoutSize,
705}
706
707impl LayoutRect {
708    /// Creates a rectangle from x, y, width, and height.
709    pub fn new(x: LayoutUnit, y: LayoutUnit, width: LayoutUnit, height: LayoutUnit) -> Self {
710        Self {
711            origin: LayoutPoint { x, y },
712            size: LayoutSize { width, height },
713        }
714    }
715
716    /// The x coordinate of the left edge.
717    pub fn x(&self) -> LayoutUnit {
718        self.origin.x
719    }
720    /// The y coordinate of the top edge.
721    pub fn y(&self) -> LayoutUnit {
722        self.origin.y
723    }
724    /// The width of the rectangle.
725    pub fn width(&self) -> LayoutUnit {
726        self.size.width
727    }
728    /// The height of the rectangle.
729    pub fn height(&self) -> LayoutUnit {
730        self.size.height
731    }
732
733    /// The x coordinate of the right edge (`x + width`).
734    pub fn right(&self) -> LayoutUnit {
735        self.origin.x + self.size.width
736    }
737    /// The y coordinate of the bottom edge (`y + height`).
738    pub fn bottom(&self) -> LayoutUnit {
739        self.origin.y + self.size.height
740    }
741
742    /// Returns `true` if the point `p` lies within this rectangle (inclusive on
743    /// the left/top edges, exclusive on the right/bottom edges).
744    pub fn contains(&self, p: LayoutPoint) -> bool {
745        p.x >= self.x() && p.x < self.right() && p.y >= self.y() && p.y < self.bottom()
746    }
747}
748
749/// The computed geometry of a single layout node.
750///
751/// After layout, every node has a bounding rectangle (its position and size on
752/// screen) and a content size (how large its content actually is, which may exceed
753/// the rect for scroll containers).
754#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
755pub struct LayoutNodeGeometry {
756    /// The bounding rectangle of this node in absolute (screen) coordinates.
757    pub rect: LayoutRect,
758    /// The natural size of the node's content before clipping. For scroll containers,
759    /// this may be larger than `rect.size`, indicating scrollable overflow.
760    pub content_size: LayoutSize,
761}
762
763/// The complete output of a layout pass.
764///
765/// `LayoutSnapshot` maps every node to its computed geometry and records the
766/// viewport size that was used. It is the primary interface between the layout
767/// engine and downstream consumers (the renderer, hit testing, accessibility).
768///
769/// # Example
770///
771/// ```rust,no_run
772/// use fission_layout::{LayoutSnapshot, LayoutSize};
773/// use fission_ir::NodeId;
774///
775/// let snapshot = LayoutSnapshot::new(LayoutSize::new(800.0, 600.0));
776/// assert_eq!(snapshot.viewport_size.width, 800.0);
777/// ```
778#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
779pub struct LayoutSnapshot {
780    /// Computed geometry for every node, keyed by [`NodeId`].
781    pub nodes: HashMap<NodeId, LayoutNodeGeometry>,
782    /// The constraints that were passed to each node during layout. Useful for
783    /// debugging. Skipped during serialization.
784    #[serde(skip)]
785    pub constraints: HashMap<NodeId, BoxConstraints>,
786    /// The viewport size used for this layout pass.
787    pub viewport_size: LayoutSize,
788}
789
790impl LayoutSnapshot {
791    /// Creates an empty snapshot for the given viewport size.
792    pub fn new(viewport_size: LayoutSize) -> Self {
793        Self {
794            nodes: HashMap::new(),
795            constraints: HashMap::new(),
796            viewport_size,
797        }
798    }
799
800    /// Returns the full geometry (rect + content size) for a node, or `None` if
801    /// the node was not part of this layout pass.
802    pub fn get_node_geometry(&self, node_id: NodeId) -> Option<&LayoutNodeGeometry> {
803        self.nodes.get(&node_id)
804    }
805
806    /// Returns just the bounding rectangle for a node, or `None` if not found.
807    pub fn get_node_rect(&self, node_id: NodeId) -> Option<LayoutRect> {
808        self.nodes.get(&node_id).map(|g| g.rect)
809    }
810
811    /// Returns the constraints that were passed to a node during layout, or `None`
812    /// if not found. Useful for debugging layout issues.
813    pub fn get_node_constraints(&self, node_id: NodeId) -> Option<BoxConstraints> {
814        self.constraints.get(&node_id).copied()
815    }
816}
817
818/// A flattened representation of a layout node, ready for the layout engine.
819///
820/// The widget compiler produces a list of `LayoutInputNode`s from the IR. Each node
821/// carries its layout operation, parent/child relationships, flex participation
822/// parameters, and optional rich text content for text measurement.
823///
824/// The layout engine operates on `&[LayoutInputNode]` rather than traversing the
825/// IR directly, which keeps the engine decoupled from the IR's internal structure.
826#[derive(Debug, Clone)]
827pub struct LayoutInputNode {
828    /// The unique identity of this node.
829    pub id: NodeId,
830    /// The parent node's ID, or `None` for the root.
831    pub parent_id: Option<NodeId>,
832    /// The layout operation this node performs.
833    pub op: LayoutOp,
834    /// Ordered list of child node IDs.
835    pub children_ids: Vec<NodeId>,
836    /// A human-readable name for debugging and diagnostics.
837    pub debug_name: String,
838    /// Explicit width override, or `None` to derive from constraints.
839    pub width: Option<LayoutUnit>,
840    /// Explicit height override, or `None` to derive from constraints.
841    pub height: Option<LayoutUnit>,
842    /// How much extra main-axis space this node claims from its flex parent.
843    pub flex_grow: LayoutUnit,
844    /// How much this node shrinks when its flex parent overflows.
845    pub flex_shrink: LayoutUnit,
846    /// Optional rich text content. When present, the layout engine uses the
847    /// [`TextMeasurer`] to determine the node's intrinsic size from the text.
848    pub rich_text: Option<Vec<TextRun>>,
849}
850
851/// Per-line metrics returned by text measurement.
852///
853/// When the layout engine or hit-testing code needs to know about individual lines
854/// of text (e.g., for cursor positioning in a multi-line text field), it calls
855/// [`TextMeasurer::get_line_metrics`] and receives a `Vec<LineMetric>`.
856pub struct LineMetric {
857    /// Byte index where this line starts in the source string.
858    pub start_index: usize,
859    /// Byte index where this line ends in the source string (exclusive).
860    pub end_index: usize,
861    /// Distance from the top of the line to its alphabetic baseline, in logical pixels.
862    pub baseline: f32,
863    /// Total height of the line (ascent + descent + leading), in logical pixels.
864    pub height: f32,
865    /// Measured width of the line's content, in logical pixels.
866    pub width: f32,
867}
868
869#[derive(Debug, Clone, Copy, PartialEq)]
870pub struct RichTextInlineBox {
871    pub id: u64,
872    pub x: f32,
873    pub y: f32,
874    pub width: f32,
875    pub height: f32,
876}
877
878#[derive(Debug, Clone, PartialEq)]
879pub struct RichTextLayoutInfo {
880    pub width: f32,
881    pub height: f32,
882    pub inline_boxes: Vec<RichTextInlineBox>,
883}
884
885/// A platform-provided text measurement backend.
886///
887/// The layout engine does not shape or measure text itself. Instead, platform
888/// backends implement `TextMeasurer` to wrap their native text engine (CoreText
889/// on macOS, DirectWrite on Windows, HarfBuzz + FreeType on Linux, etc.).
890///
891/// All methods have default implementations that return zero-sized results, so
892/// you only need to override the methods your backend supports.
893///
894/// # Required
895///
896/// * [`measure`](TextMeasurer::measure) -- must be implemented to get correct text layout.
897///
898/// # Optional
899///
900/// * [`hit_test`](TextMeasurer::hit_test) -- needed for click-to-cursor in text fields.
901/// * [`get_line_metrics`](TextMeasurer::get_line_metrics) -- needed for multi-line cursor navigation.
902/// * [`get_caret_position`](TextMeasurer::get_caret_position) -- needed for drawing the text cursor.
903/// * [`measure_rich_text`](TextMeasurer::measure_rich_text) -- needed for mixed-style text.
904pub trait TextMeasurer: Send + Sync {
905    /// Measures single-style text and returns `(width, height)` in logical pixels.
906    ///
907    /// If `available_width` is `Some`, the text should be wrapped at that width.
908    /// If `None`, the text is measured as a single unwrapped line.
909    fn measure(&self, text: &str, font_size: f32, available_width: Option<f32>) -> (f32, f32);
910
911    /// Returns the byte index of the character closest to the point `(x, y)`,
912    /// relative to the text's origin. Used for click-to-cursor in text fields.
913    ///
914    /// The default implementation returns `0`.
915    fn hit_test(
916        &self,
917        _text: &str,
918        _font_size: f32,
919        _available_width: Option<f32>,
920        _x: f32,
921        _y: f32,
922    ) -> usize {
923        0
924    }
925
926    /// Returns per-line metrics for the given text. Used for multi-line text fields
927    /// and line-based cursor navigation.
928    ///
929    /// The default implementation returns an empty vec.
930    fn get_line_metrics(
931        &self,
932        _text: &str,
933        _font_size: f32,
934        _available_width: Option<f32>,
935    ) -> Vec<LineMetric> {
936        vec![]
937    }
938
939    /// Returns the `(x, y)` position of the text cursor at `caret_index` (byte offset),
940    /// relative to the text's origin.
941    ///
942    /// The default implementation returns `(0.0, 0.0)`.
943    fn get_caret_position(
944        &self,
945        _text: &str,
946        _font_size: f32,
947        _available_width: Option<f32>,
948        _caret_index: usize,
949    ) -> (f32, f32) {
950        (0.0, 0.0)
951    }
952
953    /// Measures multi-style (rich) text and returns `(width, height)` in logical pixels.
954    ///
955    /// The default implementation returns `(0.0, 0.0)`.
956    fn measure_rich_text(&self, _runs: &[TextRun], _available_width: Option<f32>) -> (f32, f32) {
957        (0.0, 0.0)
958    }
959
960    /// Measures rich text and returns positioned inline-widget boxes, if any.
961    ///
962    /// Backends that understand inline rich-text widget markers should override
963    /// this so layout can place the child widgets at the same coordinates used
964    /// by text shaping.
965    fn layout_rich_text(
966        &self,
967        runs: &[TextRun],
968        available_width: Option<f32>,
969    ) -> RichTextLayoutInfo {
970        let (width, height) = if runs.len() == 1 {
971            let run = &runs[0];
972            self.measure(&run.text, run.style.font_size, available_width)
973        } else {
974            self.measure_rich_text(runs, available_width)
975        };
976        RichTextLayoutInfo {
977            width,
978            height,
979            inline_boxes: Vec::new(),
980        }
981    }
982
983    /// Hit-test rich text (styled runs) at the given (x, y) position.
984    /// Returns the byte offset into the concatenated text of all runs.
985    /// Default falls back to plain hit_test using the first run's font size.
986    fn hit_test_rich(
987        &self,
988        runs: &[TextRun],
989        _available_width: Option<f32>,
990        x: f32,
991        y: f32,
992    ) -> usize {
993        // Default: concatenate text and use plain hit_test
994        let text: String = runs.iter().map(|r| r.text.as_str()).collect();
995        let font_size = runs.first().map(|r| r.style.font_size).unwrap_or(13.0);
996        self.hit_test(&text, font_size, None, x, y)
997    }
998
999    /// Resolves the rich-text annotation at the given point, if any.
1000    ///
1001    /// This is used for interactive rich-text spans that need hit testing
1002    /// against shaped rich text rather than box nodes.
1003    fn resolve_rich_text_annotation_at_point(
1004        &self,
1005        _runs: &[TextRun],
1006        _available_width: Option<f32>,
1007        _x: f32,
1008        _y: f32,
1009        _paragraph_style: TextParagraphStyle,
1010        _annotations: &[RichTextAnnotation],
1011    ) -> Option<RichTextAnnotation> {
1012        None
1013    }
1014}
1015
1016/// The constraint-based layout solver.
1017///
1018/// `LayoutEngine` walks the node tree top-down, passing [`BoxConstraints`] from
1019/// parent to child, and bottom-up, returning [`LayoutSize`] from child to parent.
1020/// The final result is a [`LayoutSnapshot`] that maps every node to its absolute
1021/// screen-space rectangle.
1022///
1023/// The engine optionally holds a [`TextMeasurer`] for sizing text nodes. Without
1024/// one, text nodes are treated as zero-sized.
1025///
1026/// # Example
1027///
1028/// ```rust,no_run
1029/// use fission_layout::*;
1030/// use fission_ir::NodeId;
1031/// use std::sync::Arc;
1032///
1033/// let mut engine = LayoutEngine::new();
1034/// // engine = engine.with_measurer(my_text_measurer);
1035///
1036/// // let snapshot = engine.compute_layout(&nodes, root_id, viewport, &|_| 0.0).unwrap();
1037/// ```
1038pub struct LayoutEngine {
1039    measurer: Option<Arc<dyn TextMeasurer>>,
1040    graph_state: LayoutGraphState,
1041    next_graph_version: u64,
1042    incremental_reuse: Option<IncrementalLayoutReuseState>,
1043}
1044
1045impl LayoutEngine {
1046    const MAX_LAYOUT_RECURSION_DEPTH: usize = 100;
1047
1048    /// Creates a new layout engine with no text measurer.
1049    ///
1050    /// Text nodes will be treated as zero-sized until a measurer is provided
1051    /// via [`with_measurer`](LayoutEngine::with_measurer).
1052    pub fn new() -> Self {
1053        Self {
1054            measurer: None,
1055            graph_state: LayoutGraphState::default(),
1056            next_graph_version: 1,
1057            incremental_reuse: None,
1058        }
1059    }
1060
1061    /// Returns a new engine with the given text measurer attached.
1062    ///
1063    /// This is a builder-style method that consumes and returns `self`.
1064    pub fn with_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
1065        self.measurer = Some(measurer);
1066        self
1067    }
1068
1069    fn allocate_graph_version(&mut self) -> u64 {
1070        let version = self.next_graph_version;
1071        self.next_graph_version = self.next_graph_version.saturating_add(1);
1072        version
1073    }
1074
1075    fn refresh_graph_state(&mut self, input_nodes: &[LayoutInputNode]) {
1076        let version = self.allocate_graph_version();
1077        self.graph_state = LayoutGraphState::from_input_nodes(input_nodes, version);
1078    }
1079
1080    fn ensure_graph_state(&mut self, input_nodes: &[LayoutInputNode]) {
1081        if self.graph_state.is_empty() || !self.graph_state.matches_input_nodes(input_nodes) {
1082            self.refresh_graph_state(input_nodes);
1083        }
1084    }
1085
1086    fn validate_graph_state(&self, root: NodeId) -> Result<()> {
1087        if let Some(err) = self.graph_state.validation.first_error() {
1088            return Err(err);
1089        }
1090        if !self.graph_state.nodes.contains_key(&root) {
1091            anyhow::bail!("[verify] missing node {:?}", root);
1092        }
1093        if !self.graph_state.roots.contains(&root)
1094            && self
1095                .graph_state
1096                .parents
1097                .get(&root)
1098                .copied()
1099                .flatten()
1100                .is_some()
1101        {
1102            anyhow::bail!("[verify] root {:?} is not a graph root", root);
1103        }
1104        if let Some(last_layout_version) = self.graph_state.last_layout_version {
1105            if last_layout_version > self.graph_state.graph_version {
1106                anyhow::bail!(
1107                    "[verify] cached layout version {} exceeds graph version {}",
1108                    last_layout_version,
1109                    self.graph_state.graph_version
1110                );
1111            }
1112        }
1113        Ok(())
1114    }
1115
1116    /// Refreshes the cached graph state after upstream layout edits.
1117    ///
1118    /// Unchanged nodes keep their cached graph entries while edited topology and
1119    /// fingerprints are synchronized to the latest flattened node list.
1120    pub fn update(&mut self, input_nodes: &[LayoutInputNode]) {
1121        if self.graph_state.is_empty() {
1122            self.refresh_graph_state(input_nodes);
1123            return;
1124        }
1125
1126        if self.graph_state.matches_input_nodes(input_nodes) {
1127            return;
1128        }
1129
1130        let version = self.allocate_graph_version();
1131        self.graph_state.graph_version = version;
1132        self.graph_state.update_nodes(input_nodes);
1133    }
1134
1135    /// Rebuilds internal data structures from the full node list.
1136    pub fn rebuild(&mut self, input_nodes: &[LayoutInputNode]) -> Result<()> {
1137        self.refresh_graph_state(input_nodes);
1138        if let Some(err) = self.graph_state.validation.first_error() {
1139            return Err(err);
1140        }
1141        Ok(())
1142    }
1143
1144    /// Verifies parent-child consistency and checks for cycles in the node graph.
1145    ///
1146    /// Call this during development/testing to catch malformed IR before it causes
1147    /// layout panics. Returns `Err` with a description of the first problem found.
1148    pub fn verify_post_update(&self, input_nodes: &[LayoutInputNode], root: NodeId) -> Result<()> {
1149        if self.graph_state.matches_input_nodes(input_nodes) {
1150            return self.validate_graph_state(root);
1151        }
1152
1153        let node_map: HashMap<NodeId, &LayoutInputNode> =
1154            input_nodes.iter().map(|n| (n.id, n)).collect();
1155        // Parent/child consistency
1156        for n in input_nodes {
1157            for child in &n.children_ids {
1158                let child_node = node_map
1159                    .get(child)
1160                    .ok_or_else(|| anyhow::anyhow!("[verify] child {:?} not found", child))?;
1161                if child_node.parent_id != Some(n.id) {
1162                    anyhow::bail!("[verify] parent/child mismatch parent={:?} child={:?} child.parent_id={:?}", n.id, child, child_node.parent_id);
1163                }
1164            }
1165        }
1166        // Cycle via DFS
1167        fn dfs(
1168            id: NodeId,
1169            map: &HashMap<NodeId, &LayoutInputNode>,
1170            visited: &mut HashSet<NodeId>,
1171            stack: &mut HashSet<NodeId>,
1172        ) -> Result<()> {
1173            if !visited.insert(id) {
1174                return Ok(());
1175            }
1176            stack.insert(id);
1177            let node = map
1178                .get(&id)
1179                .ok_or_else(|| anyhow::anyhow!("[verify] missing node {:?}", id))?;
1180            for child in &node.children_ids {
1181                if stack.contains(child) {
1182                    anyhow::bail!("[verify] cycle detected at {:?} -> {:?}", id, child);
1183                }
1184                dfs(*child, map, visited, stack)?;
1185            }
1186            stack.remove(&id);
1187            Ok(())
1188        }
1189        let mut visited = HashSet::new();
1190        let mut stack = HashSet::new();
1191        dfs(root, &node_map, &mut visited, &mut stack)?;
1192        Ok(())
1193    }
1194
1195    /// Computes layout for the entire node tree and returns a snapshot.
1196    ///
1197    /// This is the main entry point. It runs the constraint-based layout algorithm
1198    /// starting from `root_node_id`, using `viewport_size` as the root constraints,
1199    /// and querying `scroll_source` for scroll offsets. After layout, it emits scroll
1200    /// diagnostics for debugging.
1201    ///
1202    /// # Arguments
1203    ///
1204    /// * `input_nodes` -- The flat list of all layout nodes.
1205    /// * `root_node_id` -- Which node is the root of the tree.
1206    /// * `viewport_size` -- The size of the window/screen.
1207    /// * `scroll_source` -- Provides scroll offsets for scroll containers.
1208    ///
1209    /// # Errors
1210    ///
1211    /// Returns `Err` if a cycle is detected or a required node is missing.
1212    pub fn compute_layout(
1213        &mut self,
1214        input_nodes: &[LayoutInputNode],
1215        root_node_id: NodeId,
1216        viewport_size: LayoutSize,
1217        scroll_source: &impl ScrollDataSource,
1218    ) -> Result<LayoutSnapshot> {
1219        self.ensure_graph_state(input_nodes);
1220        self.validate_graph_state(root_node_id)?;
1221        let snapshot = self.compute_layout_constraints(
1222            input_nodes,
1223            root_node_id,
1224            viewport_size,
1225            scroll_source,
1226        )?;
1227        self.emit_scroll_diagnostics(&snapshot);
1228        Ok(snapshot)
1229    }
1230
1231    /// Lower-level layout that skips scroll diagnostics.
1232    ///
1233    /// Same as [`compute_layout`](LayoutEngine::compute_layout) but does not emit
1234    /// diagnostic events. Useful when you need the snapshot but not the debug output.
1235    pub fn compute_layout_constraints(
1236        &mut self,
1237        input_nodes: &[LayoutInputNode],
1238        root_node_id: NodeId,
1239        viewport_size: LayoutSize,
1240        scroll_source: &impl ScrollDataSource,
1241    ) -> Result<LayoutSnapshot> {
1242        self.ensure_graph_state(input_nodes);
1243        self.validate_graph_state(root_node_id)?;
1244
1245        // Root constraints should be tight to the viewport size if no explicit size is given
1246        let mut constraints = BoxConstraints::tight(viewport_size);
1247        if let Some(root) = self.graph_state.node(root_node_id) {
1248            // Only loosen if explicit dimensions are provided for the root node
1249            if root.width.is_some() || root.height.is_some() {
1250                constraints = BoxConstraints::loose(viewport_size.width, viewport_size.height)
1251                    .tighten(root.width, root.height);
1252            }
1253        }
1254
1255        let mut snapshot = LayoutSnapshot::new(viewport_size);
1256        let mut measure_cache = HashMap::new();
1257        self.layout_node_constraints(
1258            root_node_id,
1259            constraints,
1260            LayoutPoint::ZERO,
1261            &mut snapshot.nodes,
1262            &mut snapshot.constraints,
1263            &mut measure_cache,
1264            scroll_source,
1265            true,
1266            0,
1267        )?;
1268
1269        let visual_location = |node_id: NodeId| -> Option<LayoutPoint> {
1270            let mut pos = snapshot.nodes.get(&node_id)?.rect.origin;
1271            let mut current = self.graph_state.parent_of(node_id);
1272            while let Some(parent_id) = current {
1273                if let Some(parent) = self.graph_state.node(parent_id) {
1274                    if let LayoutOp::Scroll { direction, .. } = &parent.op {
1275                        let offset = scroll_source.get_offset(parent_id);
1276                        match direction {
1277                            FlexDirection::Row => pos.x -= offset,
1278                            FlexDirection::Column => pos.y -= offset,
1279                        }
1280                    }
1281                    current = self.graph_state.parent_of(parent_id);
1282                } else {
1283                    break;
1284                }
1285            }
1286            Some(pos)
1287        };
1288
1289        let mut flyout_abs_overrides: HashMap<NodeId, (f32, f32)> = HashMap::new();
1290        for node in self.graph_state.ordered_nodes() {
1291            if let LayoutOp::Flyout { anchor, content } = node.op {
1292                if let (Some(anchor_geom), Some(content_geom)) =
1293                    (snapshot.nodes.get(&anchor), snapshot.nodes.get(&content))
1294                {
1295                    if let Some(anchor_abs) = visual_location(anchor) {
1296                        let content_w = content_geom.rect.width();
1297                        let content_h = content_geom.rect.height();
1298                        let anchor_h = anchor_geom.rect.height();
1299                        let max_left = (snapshot.viewport_size.width - content_w).max(0.0);
1300                        let left_rel = anchor_abs.x.clamp(0.0, max_left);
1301
1302                        let below_top = anchor_abs.y + anchor_h;
1303                        let max_top = (snapshot.viewport_size.height - content_h).max(0.0);
1304                        let top_rel = if below_top + content_h <= snapshot.viewport_size.height {
1305                            below_top
1306                        } else {
1307                            let above_top = anchor_abs.y - content_h;
1308                            if above_top >= 0.0 {
1309                                above_top
1310                            } else {
1311                                below_top.clamp(0.0, max_top)
1312                            }
1313                        };
1314                        flyout_abs_overrides.insert(content, (left_rel, top_rel));
1315                    }
1316                }
1317            }
1318        }
1319
1320        if !flyout_abs_overrides.is_empty() {
1321            for (nid, (abs_x, abs_y)) in flyout_abs_overrides {
1322                if let Some(current) = snapshot.nodes.get(&nid) {
1323                    let dx = abs_x - current.rect.origin.x;
1324                    let dy = abs_y - current.rect.origin.y;
1325                    let mut stack = vec![(nid, 0usize)];
1326                    while let Some((current_id, depth)) = stack.pop() {
1327                        if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1328                            return Err(self.layout_depth_overflow(current_id, depth));
1329                        }
1330                        if let Some(geometry) = snapshot.nodes.get_mut(&current_id) {
1331                            geometry.rect.origin.x += dx;
1332                            geometry.rect.origin.y += dy;
1333                        }
1334                        for child_id in self.graph_state.children_of(current_id).iter().rev() {
1335                            stack.push((*child_id, depth + 1));
1336                        }
1337                    }
1338                }
1339            }
1340        }
1341
1342        self.graph_state.mark_layout_complete();
1343        self.incremental_reuse = None;
1344
1345        Ok(snapshot)
1346    }
1347
1348    pub fn compute_layout_incremental(
1349        &mut self,
1350        input_nodes: &[LayoutInputNode],
1351        root_node_id: NodeId,
1352        viewport_size: LayoutSize,
1353        scroll_source: &impl ScrollDataSource,
1354        previous_snapshot: &LayoutSnapshot,
1355        dirty_nodes: &HashSet<NodeId>,
1356    ) -> Result<LayoutSnapshot> {
1357        self.ensure_graph_state(input_nodes);
1358        self.validate_graph_state(root_node_id)?;
1359
1360        let mut dirty_ancestors = HashSet::new();
1361        for node_id in dirty_nodes {
1362            let mut current = Some(*node_id);
1363            while let Some(id) = current {
1364                if !dirty_ancestors.insert(id) {
1365                    break;
1366                }
1367                current = self.graph_state.parent_of(id);
1368            }
1369        }
1370        dirty_ancestors.insert(root_node_id);
1371
1372        self.incremental_reuse = Some(IncrementalLayoutReuseState {
1373            previous_snapshot: previous_snapshot.clone(),
1374            dirty_ancestors,
1375        });
1376        let result = self.compute_layout_constraints(
1377            input_nodes,
1378            root_node_id,
1379            viewport_size,
1380            scroll_source,
1381        );
1382        self.incremental_reuse = None;
1383        result
1384    }
1385
1386    fn emit_scroll_diagnostics(&self, snapshot: &LayoutSnapshot) {
1387        use fission_diagnostics::prelude as diag;
1388        let trace_scroll = std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
1389        for n in self.graph_state.ordered_nodes() {
1390            if let LayoutOp::Scroll { .. } = n.op {
1391                if let Some(g) = snapshot.nodes.get(&n.id) {
1392                    let note = if g.rect.height() <= 0.0 {
1393                        let parent_op = n
1394                            .parent_id
1395                            .and_then(|pid| self.graph_state.node(pid))
1396                            .map(|p| format!("{:?}", p.op));
1397                        let parent_constraints = n
1398                            .parent_id
1399                            .and_then(|pid| snapshot.constraints.get(&pid))
1400                            .copied();
1401                        snapshot
1402                            .constraints
1403                            .get(&n.id)
1404                            .map(|c| {
1405                                format!(
1406                                    "op={:?} parent={:?} parent_op={:?} parent_constraints={:?} constraints={:?}",
1407                                    n.op,
1408                                    n.parent_id,
1409                                    parent_op,
1410                                    parent_constraints,
1411                                    c
1412                                )
1413                            })
1414                    } else {
1415                        None
1416                    };
1417                    diag::emit(
1418                        diag::DiagCategory::Layout,
1419                        diag::DiagLevel::Debug,
1420                        diag::DiagEventKind::ScrollExtent {
1421                            node: n.id.as_u128(),
1422                            viewport_w: g.rect.width(),
1423                            viewport_h: g.rect.height(),
1424                            content_w: g.content_size.width,
1425                            content_h: g.content_size.height,
1426                            note,
1427                        },
1428                    );
1429                    if trace_scroll {
1430                        eprintln!(
1431                            "[scroll-trace] node={} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
1432                            n.id.as_u128(),
1433                            g.rect.width(),
1434                            g.rect.height(),
1435                            g.content_size.width,
1436                            g.content_size.height
1437                        );
1438                    }
1439                }
1440            }
1441        }
1442    }
1443
1444    fn layout_depth_overflow(&self, node_id: NodeId, depth: usize) -> anyhow::Error {
1445        let details = format!(
1446            "layout recursion depth {} exceeded max {} at node {}",
1447            depth,
1448            Self::MAX_LAYOUT_RECURSION_DEPTH,
1449            node_id.as_u128()
1450        );
1451        diag::emit(
1452            diag::DiagCategory::Invariants,
1453            diag::DiagLevel::Error,
1454            diag::DiagEventKind::InvariantViolation {
1455                kind: "layout_recursion_depth".into(),
1456                node: Some(node_id.as_u128()),
1457                details: details.clone(),
1458                dump_ref: None,
1459            },
1460        );
1461        anyhow::anyhow!(details)
1462    }
1463
1464    fn copy_cached_subtree(
1465        &self,
1466        node_id: NodeId,
1467        origin: LayoutPoint,
1468        current_constraints: BoxConstraints,
1469        out: &mut HashMap<NodeId, LayoutNodeGeometry>,
1470        constraints_out: &mut HashMap<NodeId, BoxConstraints>,
1471    ) -> Result<Option<LayoutSize>> {
1472        let Some(reuse) = self.incremental_reuse.as_ref() else {
1473            return Ok(None);
1474        };
1475        if reuse.dirty_ancestors.contains(&node_id) {
1476            return Ok(None);
1477        }
1478
1479        let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(&node_id) else {
1480            return Ok(None);
1481        };
1482        let Some(previous_constraints) = reuse.previous_snapshot.constraints.get(&node_id).copied()
1483        else {
1484            return Ok(None);
1485        };
1486        if previous_constraints != current_constraints {
1487            return Ok(None);
1488        }
1489
1490        let dx = origin.x - previous_geometry.rect.origin.x;
1491        let dy = origin.y - previous_geometry.rect.origin.y;
1492        let mut stack = vec![(node_id, 0usize)];
1493        while let Some((current_id, depth)) = stack.pop() {
1494            if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1495                return Err(self.layout_depth_overflow(current_id, depth));
1496            }
1497            let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(&current_id) else {
1498                return Ok(None);
1499            };
1500            let Some(previous_constraints) = reuse
1501                .previous_snapshot
1502                .constraints
1503                .get(&current_id)
1504                .copied()
1505            else {
1506                return Ok(None);
1507            };
1508
1509            let mut geometry = previous_geometry.clone();
1510            geometry.rect.origin.x += dx;
1511            geometry.rect.origin.y += dy;
1512            out.insert(current_id, geometry);
1513            constraints_out.insert(current_id, previous_constraints);
1514
1515            let children = self.graph_state.children_of(current_id);
1516            for child_id in children.iter().rev() {
1517                stack.push((*child_id, depth + 1));
1518            }
1519        }
1520
1521        Ok(Some(previous_geometry.content_size))
1522    }
1523
1524    fn layout_node_constraints(
1525        &self,
1526        node_id: NodeId,
1527        constraints: BoxConstraints,
1528        origin: LayoutPoint,
1529        out: &mut HashMap<NodeId, LayoutNodeGeometry>,
1530        constraints_out: &mut HashMap<NodeId, BoxConstraints>,
1531        measure_cache: &mut HashMap<MeasureCacheKey, LayoutSize>,
1532        scroll_source: &impl ScrollDataSource,
1533        record: bool,
1534        depth: usize,
1535    ) -> Result<LayoutSize> {
1536        if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1537            return Err(self.layout_depth_overflow(node_id, depth));
1538        }
1539        if !record {
1540            let cache_key = MeasureCacheKey::new(node_id, constraints);
1541            if let Some(cached) = measure_cache.get(&cache_key).copied() {
1542                return Ok(cached);
1543            }
1544        }
1545        let node = match self.graph_state.node(node_id) {
1546            Some(node) => node,
1547            None => return Ok(LayoutSize::ZERO),
1548        };
1549
1550        if record {
1551            constraints_out.insert(node_id, constraints);
1552        }
1553
1554        if record {
1555            if let Some(reused) =
1556                self.copy_cached_subtree(node_id, origin, constraints, out, constraints_out)?
1557            {
1558                return Ok(reused);
1559            }
1560        }
1561
1562        let mut flow_children: Vec<NodeId> = Vec::new();
1563        let mut abs_children: Vec<NodeId> = Vec::new();
1564        for child_id in self.graph_state.children_of(node_id) {
1565            let is_absolute = matches!(
1566                self.graph_state.node(*child_id).map(|n| &n.op),
1567                Some(LayoutOp::AbsoluteFill) | Some(LayoutOp::Positioned { .. })
1568            );
1569            if is_absolute {
1570                abs_children.push(*child_id);
1571            } else {
1572                flow_children.push(*child_id);
1573            }
1574        }
1575        let rich_text_inline_children = node.rich_text.is_some() && !flow_children.is_empty();
1576
1577        let mut content_size;
1578        let size = match &node.op {
1579            LayoutOp::Box {
1580                width,
1581                height,
1582                min_width,
1583                max_width,
1584                min_height,
1585                max_height,
1586                padding,
1587                aspect_ratio,
1588                ..
1589            } => {
1590                let mut local =
1591                    constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
1592                local = local.tighten(*width, *height);
1593                if let Some(ratio) = aspect_ratio.filter(|r| *r > 0.0) {
1594                    let mut target_w = *width;
1595                    let mut target_h = *height;
1596
1597                    if target_w.is_some() && target_h.is_none() {
1598                        target_h = Some(target_w.unwrap() / ratio);
1599                    } else if target_h.is_some() && target_w.is_none() {
1600                        target_w = Some(target_h.unwrap() * ratio);
1601                    } else if target_w.is_none() && target_h.is_none() {
1602                        if local.is_width_bounded() || local.is_height_bounded() {
1603                            let (mut w, mut h) = if local.is_width_bounded() {
1604                                let w = local.max_w;
1605                                let h = w / ratio;
1606                                (w, h)
1607                            } else {
1608                                let h = local.max_h;
1609                                let w = h * ratio;
1610                                (w, h)
1611                            };
1612                            if local.is_width_bounded()
1613                                && local.is_height_bounded()
1614                                && h > local.max_h
1615                            {
1616                                h = local.max_h;
1617                                w = h * ratio;
1618                            }
1619                            target_w = Some(w);
1620                            target_h = Some(h);
1621                        }
1622                    }
1623
1624                    if target_w.is_some() || target_h.is_some() {
1625                        local = local.tighten(target_w, target_h);
1626                    }
1627                }
1628                let base_child_constraints = local.deflate(*padding);
1629                let mut max_child = LayoutSize::ZERO;
1630                let mut measured_children: Vec<(NodeId, BoxConstraints, LayoutSize)> = Vec::new();
1631                if !rich_text_inline_children {
1632                    for child_id in &flow_children {
1633                        let (child_width, child_height, child_max_width, child_max_height) = self
1634                            .graph_state
1635                            .node(*child_id)
1636                            .map(|child| match &child.op {
1637                                LayoutOp::Box {
1638                                    width,
1639                                    height,
1640                                    max_width,
1641                                    max_height,
1642                                    ..
1643                                } => (*width, *height, *max_width, *max_height),
1644                                LayoutOp::Scroll {
1645                                    width,
1646                                    height,
1647                                    max_width,
1648                                    max_height,
1649                                    ..
1650                                } => (*width, *height, *max_width, *max_height),
1651                                LayoutOp::Embed { width, height, .. } => {
1652                                    (*width, *height, None, None)
1653                                }
1654                                _ => (None, None, None, None),
1655                            })
1656                            .unwrap_or((None, None, None, None));
1657                        let mut child_constraints = base_child_constraints;
1658                        let stretch_width = child_constraints.min_w == child_constraints.max_w
1659                            && child_width.is_none()
1660                            && child_max_width.is_none();
1661                        if stretch_width {
1662                            child_constraints.min_w = child_constraints.max_w;
1663                        } else {
1664                            child_constraints.min_w = 0.0;
1665                        }
1666                        let stretch_height = child_constraints.min_h == child_constraints.max_h
1667                            && child_height.is_none()
1668                            && child_max_height.is_none();
1669                        if stretch_height {
1670                            child_constraints.min_h = child_constraints.max_h;
1671                        } else {
1672                            child_constraints.min_h = 0.0;
1673                        }
1674                        let child_size = self.layout_node_constraints(
1675                            *child_id,
1676                            child_constraints,
1677                            LayoutPoint::ZERO,
1678                            out,
1679                            constraints_out,
1680                            measure_cache,
1681                            scroll_source,
1682                            false,
1683                            depth + 1,
1684                        )?;
1685                        max_child.width = max_child.width.max(child_size.width);
1686                        max_child.height = max_child.height.max(child_size.height);
1687                        measured_children.push((*child_id, child_constraints, child_size));
1688                    }
1689                }
1690                let padded = LayoutSize::new(
1691                    max_child.width + padding[0] + padding[1],
1692                    max_child.height + padding[2] + padding[3],
1693                );
1694                let size = local.constrain(padded);
1695                if record {
1696                    for (child_id, child_constraints, _child_size) in measured_children {
1697                        self.layout_node_constraints(
1698                            child_id,
1699                            child_constraints,
1700                            LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
1701                            out,
1702                            constraints_out,
1703                            measure_cache,
1704                            scroll_source,
1705                            record,
1706                            depth + 1,
1707                        )?;
1708                    }
1709                    if !abs_children.is_empty() {
1710                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
1711                        for child_id in abs_children {
1712                            self.layout_node_constraints(
1713                                child_id,
1714                                abs_constraints,
1715                                origin,
1716                                out,
1717                                constraints_out,
1718                                measure_cache,
1719                                scroll_source,
1720                                record,
1721                                depth + 1,
1722                            )?;
1723                        }
1724                    }
1725                }
1726                content_size = padded;
1727                size
1728            }
1729            LayoutOp::Flex {
1730                direction,
1731                wrap,
1732                padding,
1733                gap,
1734                align_items,
1735                justify_content,
1736                flex_grow,
1737                ..
1738            } => {
1739                let gap = gap.unwrap_or(0.0);
1740                let local = constraints.tighten(node.width, node.height);
1741                let inner = local.deflate(*padding);
1742                let is_row = matches!(direction, IrFlexDirection::Row);
1743
1744                let max_main = if is_row { inner.max_w } else { inner.max_h };
1745                let max_cross = if is_row { inner.max_h } else { inner.max_w };
1746                let min_main = if is_row { inner.min_w } else { inner.min_h };
1747                let min_cross = if is_row { inner.min_h } else { inner.min_w };
1748                let main_bounded = if is_row {
1749                    inner.is_width_bounded()
1750                } else {
1751                    inner.is_height_bounded()
1752                };
1753                let cross_bounded = if is_row {
1754                    inner.is_height_bounded()
1755                } else {
1756                    inner.is_width_bounded()
1757                };
1758
1759                if matches!(wrap, IrFlexWrap::Wrap | IrFlexWrap::WrapReverse) {
1760                    let mut lines: Vec<(Vec<(NodeId, LayoutSize, BoxConstraints)>, f32, f32)> =
1761                        Vec::new();
1762                    let mut line_children: Vec<(NodeId, LayoutSize, BoxConstraints)> = Vec::new();
1763                    let mut line_main = 0.0f32;
1764                    let mut line_cross = 0.0f32;
1765                    let mut max_line_main = 0.0f32;
1766
1767                    for child_id in &flow_children {
1768                        let child_constraints = if is_row {
1769                            BoxConstraints {
1770                                min_w: 0.0,
1771                                max_w: max_main,
1772                                min_h: 0.0,
1773                                max_h: max_cross,
1774                            }
1775                        } else {
1776                            BoxConstraints {
1777                                min_w: 0.0,
1778                                max_w: max_cross,
1779                                min_h: 0.0,
1780                                max_h: max_main,
1781                            }
1782                        };
1783                        let child_size = self.layout_node_constraints(
1784                            *child_id,
1785                            child_constraints,
1786                            LayoutPoint::ZERO,
1787                            out,
1788                            constraints_out,
1789                            measure_cache,
1790                            scroll_source,
1791                            false,
1792                            depth + 1,
1793                        )?;
1794                        let child_main = if is_row {
1795                            child_size.width
1796                        } else {
1797                            child_size.height
1798                        };
1799                        let child_cross = if is_row {
1800                            child_size.height
1801                        } else {
1802                            child_size.width
1803                        };
1804                        let next_main = if line_children.is_empty() {
1805                            child_main
1806                        } else {
1807                            line_main + gap + child_main
1808                        };
1809
1810                        if main_bounded && !line_children.is_empty() && next_main > max_main {
1811                            max_line_main = max_line_main.max(line_main);
1812                            lines.push((line_children, line_main, line_cross));
1813                            line_children = Vec::new();
1814                            line_main = 0.0;
1815                            line_cross = 0.0;
1816                        }
1817
1818                        if !line_children.is_empty() {
1819                            line_main += gap;
1820                        }
1821                        line_main += child_main;
1822                        line_cross = line_cross.max(child_cross);
1823                        line_children.push((*child_id, child_size, child_constraints));
1824                    }
1825
1826                    if !line_children.is_empty() {
1827                        max_line_main = max_line_main.max(line_main);
1828                        lines.push((line_children, line_main, line_cross));
1829                    }
1830
1831                    let mut container_main = if main_bounded && *flex_grow > 0.0 {
1832                        max_main
1833                    } else {
1834                        max_line_main
1835                    };
1836                    container_main = container_main.max(min_main);
1837                    let total_lines_cross: f32 =
1838                        lines.iter().map(|(_, _, cross)| *cross).sum::<f32>()
1839                            + gap * lines.len().saturating_sub(1) as f32;
1840                    let container_cross = total_lines_cross.max(min_cross);
1841                    let size = if is_row {
1842                        local.constrain(LayoutSize::new(
1843                            container_main + padding[0] + padding[1],
1844                            container_cross + padding[2] + padding[3],
1845                        ))
1846                    } else {
1847                        local.constrain(LayoutSize::new(
1848                            container_cross + padding[0] + padding[1],
1849                            container_main + padding[2] + padding[3],
1850                        ))
1851                    };
1852
1853                    let inner_main = if is_row {
1854                        size.width - padding[0] - padding[1]
1855                    } else {
1856                        size.height - padding[2] - padding[3]
1857                    };
1858                    let inner_cross = if is_row {
1859                        size.height - padding[2] - padding[3]
1860                    } else {
1861                        size.width - padding[0] - padding[1]
1862                    };
1863
1864                    let mut ordered_lines = lines;
1865                    if matches!(wrap, IrFlexWrap::WrapReverse) {
1866                        ordered_lines.reverse();
1867                    }
1868
1869                    let mut line_cursor = if matches!(wrap, IrFlexWrap::WrapReverse) {
1870                        (inner_cross - total_lines_cross).max(0.0)
1871                    } else {
1872                        0.0
1873                    };
1874
1875                    for (line_children, line_main, line_cross) in ordered_lines {
1876                        let remaining_space = (inner_main - line_main).max(0.0);
1877                        let mut extra_gap = 0.0;
1878                        let mut offset_main = 0.0;
1879                        match justify_content {
1880                            fission_ir::op::JustifyContent::Start => {}
1881                            fission_ir::op::JustifyContent::End => offset_main = remaining_space,
1882                            fission_ir::op::JustifyContent::Center => {
1883                                offset_main = remaining_space / 2.0
1884                            }
1885                            fission_ir::op::JustifyContent::SpaceBetween => {
1886                                if line_children.len() > 1 {
1887                                    extra_gap =
1888                                        remaining_space / (line_children.len() as f32 - 1.0);
1889                                }
1890                            }
1891                            fission_ir::op::JustifyContent::SpaceAround => {
1892                                if !line_children.is_empty() {
1893                                    extra_gap = remaining_space / line_children.len() as f32;
1894                                    offset_main = extra_gap / 2.0;
1895                                }
1896                            }
1897                            fission_ir::op::JustifyContent::SpaceEvenly => {
1898                                if !line_children.is_empty() {
1899                                    extra_gap =
1900                                        remaining_space / (line_children.len() as f32 + 1.0);
1901                                    offset_main = extra_gap;
1902                                }
1903                            }
1904                        }
1905
1906                        let mut cursor = offset_main;
1907                        for (child_id, child_size, mut child_constraints) in line_children {
1908                            let child_main = if is_row {
1909                                child_size.width
1910                            } else {
1911                                child_size.height
1912                            };
1913                            let child_cross = if is_row {
1914                                child_size.height
1915                            } else {
1916                                child_size.width
1917                            };
1918                            if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
1919                                if is_row {
1920                                    child_constraints.min_h = line_cross;
1921                                    child_constraints.max_h = line_cross;
1922                                } else {
1923                                    child_constraints.min_w = line_cross;
1924                                    child_constraints.max_w = line_cross;
1925                                }
1926                            }
1927                            let cross_offset = match align_items {
1928                                fission_ir::op::AlignItems::Start
1929                                | fission_ir::op::AlignItems::Stretch => 0.0,
1930                                fission_ir::op::AlignItems::End => {
1931                                    (line_cross - child_cross).max(0.0)
1932                                }
1933                                fission_ir::op::AlignItems::Center => {
1934                                    ((line_cross - child_cross) / 2.0).max(0.0)
1935                                }
1936                                fission_ir::op::AlignItems::Baseline => 0.0,
1937                            };
1938                            let child_origin = if is_row {
1939                                LayoutPoint::new(
1940                                    origin.x + padding[0] + cursor,
1941                                    origin.y + padding[2] + line_cursor + cross_offset,
1942                                )
1943                            } else {
1944                                LayoutPoint::new(
1945                                    origin.x + padding[0] + line_cursor + cross_offset,
1946                                    origin.y + padding[2] + cursor,
1947                                )
1948                            };
1949                            self.layout_node_constraints(
1950                                child_id,
1951                                child_constraints,
1952                                child_origin,
1953                                out,
1954                                constraints_out,
1955                                measure_cache,
1956                                scroll_source,
1957                                record,
1958                                depth + 1,
1959                            )?;
1960                            cursor += child_main + gap + extra_gap;
1961                        }
1962
1963                        line_cursor += line_cross + gap;
1964                    }
1965
1966                    if record && !abs_children.is_empty() {
1967                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
1968                        for child_id in abs_children {
1969                            self.layout_node_constraints(
1970                                child_id,
1971                                abs_constraints,
1972                                origin,
1973                                out,
1974                                constraints_out,
1975                                measure_cache,
1976                                scroll_source,
1977                                record,
1978                                depth + 1,
1979                            )?;
1980                        }
1981                    }
1982                    content_size = size;
1983                    size
1984                } else {
1985                    struct FlexChildEntry {
1986                        id: NodeId,
1987                        flex: f32,
1988                        size: LayoutSize,
1989                        constraints: BoxConstraints,
1990                        is_flex: bool,
1991                    }
1992                    let mut measured: Vec<FlexChildEntry> = Vec::new();
1993                    let mut total_flex = 0.0f32;
1994                    let mut nonflex_main = 0.0f32;
1995                    let mut max_child_cross = 0.0f32;
1996                    let treat_flex_as_nonflex = !main_bounded;
1997
1998                    for child_id in &flow_children {
1999                        let child = match self.graph_state.node(*child_id) {
2000                            Some(child) => child,
2001                            None => continue,
2002                        };
2003                        let flex = child.flex_grow;
2004                        if flex > 0.0 && !treat_flex_as_nonflex {
2005                            total_flex += flex;
2006                            measured.push(FlexChildEntry {
2007                                id: *child_id,
2008                                flex,
2009                                size: LayoutSize::ZERO,
2010                                constraints: BoxConstraints::loose(0.0, 0.0),
2011                                is_flex: true,
2012                            });
2013                            continue;
2014                        }
2015                        let child_constraints = if is_row {
2016                            let cross =
2017                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2018                                    && cross_bounded
2019                                {
2020                                    BoxConstraints {
2021                                        min_w: 0.0,
2022                                        max_w: f32::INFINITY,
2023                                        min_h: max_cross,
2024                                        max_h: max_cross,
2025                                    }
2026                                } else {
2027                                    BoxConstraints {
2028                                        min_w: 0.0,
2029                                        max_w: f32::INFINITY,
2030                                        min_h: 0.0,
2031                                        max_h: max_cross,
2032                                    }
2033                                };
2034                            cross
2035                        } else {
2036                            let cross =
2037                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2038                                    && cross_bounded
2039                                {
2040                                    BoxConstraints {
2041                                        min_w: max_cross,
2042                                        max_w: max_cross,
2043                                        min_h: 0.0,
2044                                        max_h: f32::INFINITY,
2045                                    }
2046                                } else {
2047                                    BoxConstraints {
2048                                        min_w: 0.0,
2049                                        max_w: max_cross,
2050                                        min_h: 0.0,
2051                                        max_h: f32::INFINITY,
2052                                    }
2053                                };
2054                            cross
2055                        };
2056                        let child_size = self.layout_node_constraints(
2057                            *child_id,
2058                            child_constraints,
2059                            LayoutPoint::ZERO,
2060                            out,
2061                            constraints_out,
2062                            measure_cache,
2063                            scroll_source,
2064                            false,
2065                            depth + 1,
2066                        )?;
2067                        let child_main = if is_row {
2068                            child_size.width
2069                        } else {
2070                            child_size.height
2071                        };
2072                        let child_cross = if is_row {
2073                            child_size.height
2074                        } else {
2075                            child_size.width
2076                        };
2077                        nonflex_main += child_main;
2078                        max_child_cross = max_child_cross.max(child_cross);
2079                        measured.push(FlexChildEntry {
2080                            id: *child_id,
2081                            flex,
2082                            size: child_size,
2083                            constraints: child_constraints,
2084                            is_flex: false,
2085                        });
2086                    }
2087
2088                    let gap_total = gap * flow_children.len().saturating_sub(1) as f32;
2089                    let remaining = if main_bounded {
2090                        (max_main - nonflex_main - gap_total).max(0.0)
2091                    } else {
2092                        0.0
2093                    };
2094
2095                    for entry in measured.iter_mut().filter(|e| e.is_flex) {
2096                        let flex = entry.flex;
2097                        let allocated = if main_bounded && total_flex > 0.0 {
2098                            remaining * (flex / total_flex)
2099                        } else {
2100                            0.0
2101                        };
2102                        let child_constraints = if is_row {
2103                            let cross =
2104                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2105                                    && cross_bounded
2106                                {
2107                                    BoxConstraints {
2108                                        min_w: allocated,
2109                                        max_w: allocated,
2110                                        min_h: max_cross,
2111                                        max_h: max_cross,
2112                                    }
2113                                } else {
2114                                    BoxConstraints {
2115                                        min_w: allocated,
2116                                        max_w: allocated,
2117                                        min_h: 0.0,
2118                                        max_h: max_cross,
2119                                    }
2120                                };
2121                            cross
2122                        } else {
2123                            let cross =
2124                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2125                                    && cross_bounded
2126                                {
2127                                    BoxConstraints {
2128                                        min_w: max_cross,
2129                                        max_w: max_cross,
2130                                        min_h: allocated,
2131                                        max_h: allocated,
2132                                    }
2133                                } else {
2134                                    BoxConstraints {
2135                                        min_w: 0.0,
2136                                        max_w: max_cross,
2137                                        min_h: allocated,
2138                                        max_h: allocated,
2139                                    }
2140                                };
2141                            cross
2142                        };
2143                        let child_size = self.layout_node_constraints(
2144                            entry.id,
2145                            child_constraints,
2146                            LayoutPoint::ZERO,
2147                            out,
2148                            constraints_out,
2149                            measure_cache,
2150                            scroll_source,
2151                            false,
2152                            depth + 1,
2153                        )?;
2154                        let child_cross = if is_row {
2155                            child_size.height
2156                        } else {
2157                            child_size.width
2158                        };
2159                        max_child_cross = max_child_cross.max(child_cross);
2160                        entry.size = child_size;
2161                        entry.constraints = child_constraints;
2162                    }
2163
2164                    let final_children_main: f32 = measured
2165                        .iter()
2166                        .map(|entry| {
2167                            if is_row {
2168                                entry.size.width
2169                            } else {
2170                                entry.size.height
2171                            }
2172                        })
2173                        .sum();
2174
2175                    let mut container_main = if main_bounded && *flex_grow > 0.0 {
2176                        max_main
2177                    } else {
2178                        final_children_main + gap_total
2179                    };
2180                    container_main = container_main.max(min_main);
2181
2182                    if main_bounded && final_children_main + gap_total > max_main {
2183                        // SHRINK logic
2184                        let mut total_shrink_scaled = 0.0f32;
2185                        for entry in &measured {
2186                            let child = self.graph_state.node(entry.id).unwrap();
2187                            let main_size = if is_row {
2188                                entry.size.width
2189                            } else {
2190                                entry.size.height
2191                            };
2192                            total_shrink_scaled += main_size * child.flex_shrink;
2193                        }
2194
2195                        if total_shrink_scaled > 0.0 {
2196                            let overflow = (final_children_main + gap_total) - max_main;
2197                            for entry in &mut measured {
2198                                let child = self.graph_state.node(entry.id).unwrap();
2199                                let main_size = if is_row {
2200                                    entry.size.width
2201                                } else {
2202                                    entry.size.height
2203                                };
2204                                let shrink_amount = (main_size * child.flex_shrink
2205                                    / total_shrink_scaled)
2206                                    * overflow;
2207                                // Don't shrink below a reasonable minimum. Items with
2208                                // flex_shrink > 0 can shrink but not to zero - preserve at
2209                                // least a small fraction of their natural size.
2210                                let floor = if child.flex_shrink > 0.0 {
2211                                    // Check for explicit min/fixed dimension
2212                                    let explicit_min = match &child.op {
2213                                        LayoutOp::Box {
2214                                            min_width,
2215                                            min_height,
2216                                            height,
2217                                            width,
2218                                            ..
2219                                        } => {
2220                                            if is_row {
2221                                                min_width.or(*width).unwrap_or(0.0)
2222                                            } else {
2223                                                min_height.or(*height).unwrap_or(0.0)
2224                                            }
2225                                        }
2226                                        _ => 0.0,
2227                                    };
2228                                    explicit_min
2229                                } else {
2230                                    main_size // flex_shrink == 0 means don't shrink at all
2231                                };
2232                                let new_main = (main_size - shrink_amount).max(floor);
2233
2234                                let mut child_constraints = entry.constraints;
2235                                if is_row {
2236                                    child_constraints.min_w = new_main;
2237                                    child_constraints.max_w = new_main;
2238                                } else {
2239                                    child_constraints.min_h = new_main;
2240                                    child_constraints.max_h = new_main;
2241                                }
2242                                let new_size = self.layout_node_constraints(
2243                                    entry.id,
2244                                    child_constraints,
2245                                    LayoutPoint::ZERO,
2246                                    out,
2247                                    constraints_out,
2248                                    measure_cache,
2249                                    scroll_source,
2250                                    false,
2251                                    depth + 1,
2252                                )?;
2253                                entry.size = new_size;
2254                                entry.constraints = child_constraints;
2255                            }
2256                        }
2257                    }
2258
2259                    let container_cross = max_child_cross.max(min_cross);
2260                    let size = if is_row {
2261                        local.constrain(LayoutSize::new(
2262                            container_main + padding[0] + padding[1],
2263                            container_cross + padding[2] + padding[3],
2264                        ))
2265                    } else {
2266                        local.constrain(LayoutSize::new(
2267                            container_cross + padding[0] + padding[1],
2268                            container_main + padding[2] + padding[3],
2269                        ))
2270                    };
2271
2272                    let inner_main = if is_row {
2273                        size.width - padding[0] - padding[1]
2274                    } else {
2275                        size.height - padding[2] - padding[3]
2276                    };
2277                    let inner_cross = if is_row {
2278                        size.height - padding[2] - padding[3]
2279                    } else {
2280                        size.width - padding[0] - padding[1]
2281                    };
2282
2283                    let final_children_main: f32 = measured
2284                        .iter()
2285                        .map(|entry| {
2286                            if is_row {
2287                                entry.size.width
2288                            } else {
2289                                entry.size.height
2290                            }
2291                        })
2292                        .sum();
2293
2294                    let remaining_space = (inner_main - final_children_main - gap_total).max(0.0);
2295                    let mut extra_gap = 0.0;
2296                    let mut offset_main = 0.0;
2297                    match justify_content {
2298                        fission_ir::op::JustifyContent::Start => {}
2299                        fission_ir::op::JustifyContent::End => offset_main = remaining_space,
2300                        fission_ir::op::JustifyContent::Center => {
2301                            offset_main = remaining_space / 2.0
2302                        }
2303                        fission_ir::op::JustifyContent::SpaceBetween => {
2304                            if measured.len() > 1 {
2305                                extra_gap = remaining_space / (measured.len() as f32 - 1.0);
2306                            }
2307                        }
2308                        fission_ir::op::JustifyContent::SpaceAround => {
2309                            if !measured.is_empty() {
2310                                extra_gap = remaining_space / measured.len() as f32;
2311                                offset_main = extra_gap / 2.0;
2312                            }
2313                        }
2314                        fission_ir::op::JustifyContent::SpaceEvenly => {
2315                            if !measured.is_empty() {
2316                                extra_gap = remaining_space / (measured.len() as f32 + 1.0);
2317                                offset_main = extra_gap;
2318                            }
2319                        }
2320                    }
2321
2322                    let mut cursor = offset_main;
2323                    for entry in measured {
2324                        let child_main = if is_row {
2325                            entry.size.width
2326                        } else {
2327                            entry.size.height
2328                        };
2329                        let child_cross = if is_row {
2330                            entry.size.height
2331                        } else {
2332                            entry.size.width
2333                        };
2334                        let cross_offset = match align_items {
2335                            fission_ir::op::AlignItems::Start
2336                            | fission_ir::op::AlignItems::Stretch => 0.0,
2337                            fission_ir::op::AlignItems::End => (inner_cross - child_cross).max(0.0),
2338                            fission_ir::op::AlignItems::Center => {
2339                                ((inner_cross - child_cross) / 2.0).max(0.0)
2340                            }
2341                            fission_ir::op::AlignItems::Baseline => 0.0,
2342                        };
2343                        let child_origin = if is_row {
2344                            LayoutPoint::new(
2345                                origin.x + padding[0] + cursor,
2346                                origin.y + padding[2] + cross_offset,
2347                            )
2348                        } else {
2349                            LayoutPoint::new(
2350                                origin.x + padding[0] + cross_offset,
2351                                origin.y + padding[2] + cursor,
2352                            )
2353                        };
2354
2355                        let mut child_constraints = entry.constraints;
2356                        if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
2357                            // Only stretch children that don't have an explicit cross-axis size.
2358                            let child_node = self.graph_state.node(entry.id);
2359                            let has_explicit_cross = child_node
2360                                .map(|n| match &n.op {
2361                                    LayoutOp::Box { width, height, .. } => {
2362                                        if is_row {
2363                                            height.is_some()
2364                                        } else {
2365                                            width.is_some()
2366                                        }
2367                                    }
2368                                    _ => false,
2369                                })
2370                                .unwrap_or(false);
2371                            if !has_explicit_cross {
2372                                if is_row {
2373                                    child_constraints.min_h = inner_cross;
2374                                    child_constraints.max_h = inner_cross;
2375                                } else {
2376                                    child_constraints.min_w = inner_cross;
2377                                    child_constraints.max_w = inner_cross;
2378                                }
2379                            }
2380                        }
2381
2382                        self.layout_node_constraints(
2383                            entry.id,
2384                            child_constraints,
2385                            child_origin,
2386                            out,
2387                            constraints_out,
2388                            measure_cache,
2389                            scroll_source,
2390                            record,
2391                            depth + 1,
2392                        )?;
2393                        cursor += child_main + gap + extra_gap;
2394                    }
2395
2396                    if record && !abs_children.is_empty() {
2397                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
2398                        for child_id in abs_children {
2399                            self.layout_node_constraints(
2400                                child_id,
2401                                abs_constraints,
2402                                origin,
2403                                out,
2404                                constraints_out,
2405                                measure_cache,
2406                                scroll_source,
2407                                record,
2408                                depth + 1,
2409                            )?;
2410                        }
2411                    }
2412                    content_size = size;
2413                    size
2414                }
2415            }
2416            LayoutOp::Grid {
2417                columns,
2418                rows,
2419                column_gap,
2420                row_gap,
2421                padding,
2422            } => {
2423                let gap_x = column_gap.unwrap_or(0.0);
2424                let gap_y = row_gap.unwrap_or(0.0);
2425                let inner = constraints.deflate(*padding);
2426                let bounded_w = inner.is_width_bounded();
2427                let bounded_h = inner.is_height_bounded();
2428                let available_w = if bounded_w { inner.max_w } else { 0.0 };
2429                let available_h = if bounded_h { inner.max_h } else { 0.0 };
2430
2431                let col_count = columns.len().max(1);
2432                let mut col_widths = vec![0.0f32; col_count];
2433                let mut fr_total = 0.0f32;
2434                let mut fixed_total = 0.0f32;
2435                for (i, track) in columns.iter().enumerate() {
2436                    match track {
2437                        GridTrack::Points(p) => {
2438                            col_widths[i] = *p;
2439                            fixed_total += *p;
2440                        }
2441                        GridTrack::Percent(p) => {
2442                            let w = if bounded_w {
2443                                available_w * (*p / 100.0)
2444                            } else {
2445                                0.0
2446                            };
2447                            col_widths[i] = w;
2448                            fixed_total += w;
2449                        }
2450                        GridTrack::Fr(f) => fr_total += *f,
2451                        _ => {}
2452                    }
2453                }
2454                if fr_total > 0.0 && bounded_w {
2455                    let remaining =
2456                        (available_w - fixed_total - gap_x * (col_count.saturating_sub(1) as f32))
2457                            .max(0.0);
2458                    for (i, track) in columns.iter().enumerate() {
2459                        if let GridTrack::Fr(f) = track {
2460                            col_widths[i] = remaining * (*f / fr_total);
2461                        }
2462                    }
2463                }
2464
2465                let child_count = flow_children.len();
2466                let row_count = if rows.is_empty() {
2467                    (child_count + col_count - 1) / col_count
2468                } else {
2469                    rows.len()
2470                };
2471                let mut row_heights = vec![0.0f32; row_count.max(1)];
2472
2473                if !rows.is_empty() {
2474                    let mut row_fr_total = 0.0f32;
2475                    let mut row_fixed_total = 0.0f32;
2476                    for (i, track) in rows.iter().enumerate() {
2477                        if i >= row_heights.len() {
2478                            break;
2479                        }
2480                        match track {
2481                            GridTrack::Points(p) => {
2482                                row_heights[i] = *p;
2483                                row_fixed_total += *p;
2484                            }
2485                            GridTrack::Percent(p) => {
2486                                let h = if bounded_h {
2487                                    available_h * (*p / 100.0)
2488                                } else {
2489                                    0.0
2490                                };
2491                                row_heights[i] = h;
2492                                row_fixed_total += h;
2493                            }
2494                            GridTrack::Fr(f) => row_fr_total += *f,
2495                            _ => {}
2496                        }
2497                    }
2498                    if row_fr_total > 0.0 && bounded_h {
2499                        let remaining = (available_h
2500                            - row_fixed_total
2501                            - gap_y * (row_heights.len().saturating_sub(1) as f32))
2502                            .max(0.0);
2503                        for (i, track) in rows.iter().enumerate() {
2504                            if let GridTrack::Fr(f) = track {
2505                                row_heights[i] = remaining * (*f / row_fr_total);
2506                            }
2507                        }
2508                    }
2509                }
2510
2511                let mut cell_assignments = Vec::new();
2512                let mut auto_row = 0;
2513                let mut auto_col = 0;
2514
2515                for child_id in &flow_children {
2516                    let child = self.graph_state.node(*child_id).unwrap();
2517                    let (row, col) = if let LayoutOp::GridItem {
2518                        row_start,
2519                        col_start,
2520                        ..
2521                    } = &child.op
2522                    {
2523                        let r = match row_start {
2524                            fission_ir::op::GridPlacement::Line(l) => {
2525                                (*l as usize).saturating_sub(1)
2526                            }
2527                            _ => auto_row,
2528                        };
2529                        let c = match col_start {
2530                            fission_ir::op::GridPlacement::Line(l) => {
2531                                (*l as usize).saturating_sub(1)
2532                            }
2533                            _ => auto_col,
2534                        };
2535                        (r, c)
2536                    } else {
2537                        let res = (auto_row, auto_col);
2538                        auto_col += 1;
2539                        if auto_col >= col_count {
2540                            auto_col = 0;
2541                            auto_row += 1;
2542                        }
2543                        res
2544                    };
2545                    cell_assignments.push((*child_id, row, col));
2546                }
2547
2548                for (child_id, row, col) in &cell_assignments {
2549                    if *row >= row_heights.len() || *col >= col_widths.len() {
2550                        continue;
2551                    }
2552                    let cell_w = col_widths[*col];
2553                    let cell_constraints = BoxConstraints {
2554                        min_w: cell_w,
2555                        max_w: cell_w,
2556                        min_h: 0.0,
2557                        max_h: if row_heights[*row] > 0.0 {
2558                            row_heights[*row]
2559                        } else {
2560                            f32::INFINITY
2561                        },
2562                    };
2563                    let child_size = self.layout_node_constraints(
2564                        *child_id,
2565                        cell_constraints,
2566                        LayoutPoint::ZERO,
2567                        out,
2568                        constraints_out,
2569                        measure_cache,
2570                        scroll_source,
2571                        false,
2572                        depth + 1,
2573                    )?;
2574                    if row_heights[*row] == 0.0 {
2575                        row_heights[*row] = child_size.height;
2576                    } else {
2577                        row_heights[*row] = row_heights[*row].max(child_size.height);
2578                    }
2579                }
2580
2581                let grid_w: f32 =
2582                    col_widths.iter().sum::<f32>() + gap_x * (col_count.saturating_sub(1) as f32);
2583                let grid_h: f32 = row_heights.iter().sum::<f32>()
2584                    + gap_y * (row_heights.len().saturating_sub(1) as f32);
2585                let size = constraints.constrain(LayoutSize::new(
2586                    grid_w + padding[0] + padding[1],
2587                    grid_h + padding[2] + padding[3],
2588                ));
2589
2590                if record {
2591                    let padding_origin_x = origin.x + padding[0];
2592                    let padding_origin_y = origin.y + padding[2];
2593                    for (child_id, row, col) in &cell_assignments {
2594                        if *row >= row_heights.len() || *col >= col_widths.len() {
2595                            continue;
2596                        }
2597                        let mut cell_x = padding_origin_x;
2598                        for i in 0..*col {
2599                            cell_x += col_widths[i] + gap_x;
2600                        }
2601                        let mut cell_y = padding_origin_y;
2602                        for i in 0..*row {
2603                            cell_y += row_heights[i] + gap_y;
2604                        }
2605                        let cell_w = col_widths[*col];
2606                        let cell_h = row_heights[*row];
2607                        let child_constraints = BoxConstraints {
2608                            min_w: cell_w,
2609                            max_w: cell_w,
2610                            min_h: cell_h,
2611                            max_h: cell_h,
2612                        };
2613                        self.layout_node_constraints(
2614                            *child_id,
2615                            child_constraints,
2616                            LayoutPoint::new(cell_x, cell_y),
2617                            out,
2618                            constraints_out,
2619                            measure_cache,
2620                            scroll_source,
2621                            record,
2622                            depth + 1,
2623                        )?;
2624                    }
2625                }
2626
2627                if record && !abs_children.is_empty() {
2628                    let abs_constraints = BoxConstraints::loose(size.width, size.height);
2629                    for child_id in abs_children {
2630                        self.layout_node_constraints(
2631                            child_id,
2632                            abs_constraints,
2633                            origin,
2634                            out,
2635                            constraints_out,
2636                            measure_cache,
2637                            scroll_source,
2638                            record,
2639                            depth + 1,
2640                        )?;
2641                    }
2642                }
2643                content_size = size;
2644                size
2645            }
2646            LayoutOp::GridItem { .. } => {
2647                let mut child_size = LayoutSize::ZERO;
2648                if let Some(child_id) = node.children_ids.first() {
2649                    child_size = self.layout_node_constraints(
2650                        *child_id,
2651                        constraints,
2652                        origin,
2653                        out,
2654                        constraints_out,
2655                        measure_cache,
2656                        scroll_source,
2657                        record,
2658                        depth + 1,
2659                    )?;
2660                }
2661                content_size = child_size;
2662                constraints.constrain(child_size)
2663            }
2664            LayoutOp::Scroll {
2665                direction,
2666                width,
2667                height,
2668                min_width,
2669                max_width,
2670                min_height,
2671                max_height,
2672                padding,
2673                ..
2674            } => {
2675                let mut local =
2676                    constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
2677                local = local.tighten(*width, *height);
2678                let is_horizontal = matches!(direction, FlexDirection::Row);
2679                let mut child_constraints = local.deflate(*padding);
2680                if is_horizontal {
2681                    child_constraints.min_w = 0.0;
2682                    child_constraints.max_w = f32::INFINITY;
2683                } else {
2684                    child_constraints.min_h = 0.0;
2685                    child_constraints.max_h = f32::INFINITY;
2686                }
2687                let mut child_size = LayoutSize::ZERO;
2688                if let Some(child_id) = flow_children.first() {
2689                    child_size = self.layout_node_constraints(
2690                        *child_id,
2691                        child_constraints,
2692                        LayoutPoint::ZERO,
2693                        out,
2694                        constraints_out,
2695                        measure_cache,
2696                        scroll_source,
2697                        false,
2698                        depth + 1,
2699                    )?;
2700                }
2701                let size = local.constrain(LayoutSize::new(
2702                    child_size.width + padding[0] + padding[1],
2703                    child_size.height + padding[2] + padding[3],
2704                ));
2705                if record {
2706                    if let Some(child_id) = flow_children.first() {
2707                        self.layout_node_constraints(
2708                            *child_id,
2709                            child_constraints,
2710                            LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
2711                            out,
2712                            constraints_out,
2713                            measure_cache,
2714                            scroll_source,
2715                            record,
2716                            depth + 1,
2717                        )?;
2718                    }
2719                    if !abs_children.is_empty() {
2720                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
2721                        for child_id in abs_children {
2722                            self.layout_node_constraints(
2723                                child_id,
2724                                abs_constraints,
2725                                origin,
2726                                out,
2727                                constraints_out,
2728                                measure_cache,
2729                                scroll_source,
2730                                record,
2731                                depth + 1,
2732                            )?;
2733                        }
2734                    }
2735                }
2736                content_size = child_size;
2737                size
2738            }
2739            LayoutOp::Align => {
2740                let child_constraints = BoxConstraints::loose(constraints.max_w, constraints.max_h);
2741                let mut child_size = LayoutSize::ZERO;
2742                if let Some(child_id) = flow_children.first() {
2743                    child_size = self.layout_node_constraints(
2744                        *child_id,
2745                        child_constraints,
2746                        LayoutPoint::ZERO,
2747                        out,
2748                        constraints_out,
2749                        measure_cache,
2750                        scroll_source,
2751                        false,
2752                        depth + 1,
2753                    )?;
2754                }
2755                let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
2756                    constraints.constrain(LayoutSize::new(
2757                        if constraints.is_width_bounded() {
2758                            constraints.max_w
2759                        } else {
2760                            child_size.width
2761                        },
2762                        if constraints.is_height_bounded() {
2763                            constraints.max_h
2764                        } else {
2765                            child_size.height
2766                        },
2767                    ))
2768                } else {
2769                    child_size
2770                };
2771                if let Some(child_id) = flow_children.first() {
2772                    let dx = ((size.width - child_size.width) / 2.0).max(0.0);
2773                    let dy = ((size.height - child_size.height) / 2.0).max(0.0);
2774                    self.layout_node_constraints(
2775                        *child_id,
2776                        child_constraints,
2777                        LayoutPoint::new(origin.x + dx, origin.y + dy),
2778                        out,
2779                        constraints_out,
2780                        measure_cache,
2781                        scroll_source,
2782                        record,
2783                        depth + 1,
2784                    )?;
2785                }
2786                if record && !abs_children.is_empty() {
2787                    let abs_constraints = BoxConstraints::loose(size.width, size.height);
2788                    for child_id in abs_children {
2789                        self.layout_node_constraints(
2790                            child_id,
2791                            abs_constraints,
2792                            origin,
2793                            out,
2794                            constraints_out,
2795                            measure_cache,
2796                            scroll_source,
2797                            record,
2798                            depth + 1,
2799                        )?;
2800                    }
2801                }
2802                content_size = child_size;
2803                size
2804            }
2805            LayoutOp::ZStack => {
2806                let mut max_child = LayoutSize::ZERO;
2807                for child_id in &flow_children {
2808                    let child_size = self.layout_node_constraints(
2809                        *child_id,
2810                        BoxConstraints::loose(constraints.max_w, constraints.max_h),
2811                        LayoutPoint::ZERO,
2812                        out,
2813                        constraints_out,
2814                        measure_cache,
2815                        scroll_source,
2816                        false,
2817                        depth + 1,
2818                    )?;
2819                    max_child.width = max_child.width.max(child_size.width);
2820                    max_child.height = max_child.height.max(child_size.height);
2821                }
2822                let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
2823                    constraints.constrain(LayoutSize::new(
2824                        if constraints.is_width_bounded() {
2825                            constraints.max_w
2826                        } else {
2827                            max_child.width
2828                        },
2829                        if constraints.is_height_bounded() {
2830                            constraints.max_h
2831                        } else {
2832                            max_child.height
2833                        },
2834                    ))
2835                } else {
2836                    max_child
2837                };
2838                for child_id in &flow_children {
2839                    let child_constraints = BoxConstraints::loose(size.width, size.height);
2840                    let child_origin = LayoutPoint::new(origin.x, origin.y);
2841                    self.layout_node_constraints(
2842                        *child_id,
2843                        child_constraints,
2844                        child_origin,
2845                        out,
2846                        constraints_out,
2847                        measure_cache,
2848                        scroll_source,
2849                        record,
2850                        depth + 1,
2851                    )?;
2852                }
2853                if record && !abs_children.is_empty() {
2854                    let abs_constraints = BoxConstraints::loose(size.width, size.height);
2855                    for child_id in abs_children {
2856                        self.layout_node_constraints(
2857                            child_id,
2858                            abs_constraints,
2859                            origin,
2860                            out,
2861                            constraints_out,
2862                            measure_cache,
2863                            scroll_source,
2864                            record,
2865                            depth + 1,
2866                        )?;
2867                    }
2868                }
2869                content_size = size;
2870                size
2871            }
2872            LayoutOp::Positioned {
2873                top,
2874                left,
2875                bottom,
2876                right,
2877                width,
2878                height,
2879            } => {
2880                let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
2881                let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
2882                let size = constraints.constrain(LayoutSize::new(target_w, target_h));
2883                let mut child_constraints = BoxConstraints::loose(size.width, size.height);
2884                if let (Some(l), Some(r)) = (left, right) {
2885                    let w = (size.width - l - r).max(0.0);
2886                    child_constraints = child_constraints.tighten(Some(w), None);
2887                }
2888                if let (Some(t), Some(b)) = (top, bottom) {
2889                    let h = (size.height - t - b).max(0.0);
2890                    child_constraints = child_constraints.tighten(None, Some(h));
2891                }
2892                child_constraints = child_constraints.tighten(*width, *height);
2893                if let Some(child_id) = node.children_ids.first() {
2894                    let child_size = self.layout_node_constraints(
2895                        *child_id,
2896                        child_constraints,
2897                        LayoutPoint::ZERO,
2898                        out,
2899                        constraints_out,
2900                        measure_cache,
2901                        scroll_source,
2902                        false,
2903                        depth + 1,
2904                    )?;
2905                    let x = left.unwrap_or_else(|| {
2906                        right
2907                            .map(|r| (size.width - r - child_size.width).max(0.0))
2908                            .unwrap_or(0.0)
2909                    });
2910                    let y = top.unwrap_or_else(|| {
2911                        bottom
2912                            .map(|b| (size.height - b - child_size.height).max(0.0))
2913                            .unwrap_or(0.0)
2914                    });
2915                    self.layout_node_constraints(
2916                        *child_id,
2917                        child_constraints,
2918                        LayoutPoint::new(origin.x + x, origin.y + y),
2919                        out,
2920                        constraints_out,
2921                        measure_cache,
2922                        scroll_source,
2923                        record,
2924                        depth + 1,
2925                    )?;
2926                }
2927                content_size = size;
2928                size
2929            }
2930            LayoutOp::Embed { width, height, .. } => {
2931                let local = constraints.tighten(*width, *height);
2932                let w = if local.is_width_bounded() {
2933                    local.max_w
2934                } else {
2935                    local.min_w
2936                };
2937                let h = if local.is_height_bounded() {
2938                    local.max_h
2939                } else {
2940                    local.min_h
2941                };
2942                let size = local.constrain(LayoutSize::new(w, h));
2943                content_size = size;
2944                size
2945            }
2946            LayoutOp::AbsoluteFill => {
2947                let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
2948                let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
2949                let size = constraints.constrain(LayoutSize::new(target_w, target_h));
2950                for child_id in self.graph_state.children_of(node_id) {
2951                    self.layout_node_constraints(
2952                        *child_id,
2953                        BoxConstraints::tight(size),
2954                        origin,
2955                        out,
2956                        constraints_out,
2957                        measure_cache,
2958                        scroll_source,
2959                        record,
2960                        depth + 1,
2961                    )?;
2962                }
2963                content_size = size;
2964                size
2965            }
2966            LayoutOp::Transform { .. } | LayoutOp::Clip { .. } => {
2967                let mut child_size = LayoutSize::ZERO;
2968                if let Some(child_id) = node.children_ids.first() {
2969                    child_size = self.layout_node_constraints(
2970                        *child_id,
2971                        constraints,
2972                        origin,
2973                        out,
2974                        constraints_out,
2975                        measure_cache,
2976                        scroll_source,
2977                        record,
2978                        depth + 1,
2979                    )?;
2980                }
2981                content_size = child_size;
2982                constraints.constrain(child_size)
2983            }
2984            LayoutOp::Flyout { anchor, content: _ } => {
2985                let loose = BoxConstraints::loose(
2986                    if constraints.is_width_bounded() {
2987                        constraints.max_w
2988                    } else {
2989                        f32::INFINITY
2990                    },
2991                    if constraints.is_height_bounded() {
2992                        constraints.max_h
2993                    } else {
2994                        f32::INFINITY
2995                    },
2996                );
2997                let mut child_size = LayoutSize::ZERO;
2998                for child_id in self.graph_state.children_of(node_id) {
2999                    child_size = self.layout_node_constraints(
3000                        *child_id,
3001                        loose,
3002                        origin,
3003                        out,
3004                        constraints_out,
3005                        measure_cache,
3006                        scroll_source,
3007                        false,
3008                        depth + 1,
3009                    )?;
3010                }
3011                if record {
3012                    let anchor_rect = out.get(anchor).map(|g| g.rect);
3013                    let place_x = anchor_rect.map(|r| r.x()).unwrap_or(origin.x);
3014                    let place_y = anchor_rect.map(|r| r.y() + r.height()).unwrap_or(origin.y);
3015                    for child_id in self.graph_state.children_of(node_id) {
3016                        self.layout_node_constraints(
3017                            *child_id,
3018                            loose,
3019                            LayoutPoint::new(place_x, place_y),
3020                            out,
3021                            constraints_out,
3022                            measure_cache,
3023                            scroll_source,
3024                            record,
3025                            depth + 1,
3026                        )?;
3027                    }
3028                }
3029                content_size = child_size;
3030                child_size
3031            }
3032        };
3033
3034        if let Some(runs) = &node.rich_text {
3035            if let Some(measurer) = &self.measurer {
3036                let node_max_w = match &node.op {
3037                    LayoutOp::Box { max_width, .. } => *max_width,
3038                    _ => None,
3039                };
3040                let avail_w = {
3041                    let from_constraints = if constraints.is_width_bounded() {
3042                        Some(constraints.max_w)
3043                    } else {
3044                        None
3045                    };
3046                    match (from_constraints, node_max_w) {
3047                        (Some(c), Some(m)) => Some(c.min(m)),
3048                        (Some(c), None) => Some(c),
3049                        (None, Some(m)) => Some(m),
3050                        (None, None) => None,
3051                    }
3052                };
3053                let rich_layout = measurer.layout_rich_text(runs, avail_w);
3054                let text_content = LayoutSize::new(rich_layout.width, rich_layout.height);
3055                let measured = constraints.constrain(text_content);
3056                if rich_text_inline_children
3057                    && rich_layout.inline_boxes.len() == flow_children.len()
3058                {
3059                    let result =
3060                        self.record_geometry(node_id, origin, measured, text_content, out, record);
3061                    if record {
3062                        let mut inline_boxes = rich_layout.inline_boxes;
3063                        inline_boxes.sort_by_key(|inline_box| inline_box.id);
3064                        for (child_id, inline_box) in flow_children.iter().zip(inline_boxes.iter())
3065                        {
3066                            self.layout_node_constraints(
3067                                *child_id,
3068                                BoxConstraints::tight(LayoutSize::new(
3069                                    inline_box.width,
3070                                    inline_box.height,
3071                                )),
3072                                LayoutPoint::new(origin.x + inline_box.x, origin.y + inline_box.y),
3073                                out,
3074                                constraints_out,
3075                                measure_cache,
3076                                scroll_source,
3077                                record,
3078                                depth + 1,
3079                            )?;
3080                        }
3081                    }
3082                    if !record {
3083                        measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3084                    }
3085                    return Ok(result);
3086                }
3087                if node.children_ids.is_empty() {
3088                    let result =
3089                        self.record_geometry(node_id, origin, measured, text_content, out, record);
3090                    if !record {
3091                        measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3092                    }
3093                    return Ok(result);
3094                }
3095                content_size.width = content_size.width.max(text_content.width);
3096                content_size.height = content_size.height.max(text_content.height);
3097            }
3098        }
3099
3100        let result = self.record_geometry(node_id, origin, size, content_size, out, record);
3101        if !record {
3102            measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3103        }
3104        Ok(result)
3105    }
3106
3107    fn record_geometry(
3108        &self,
3109        node_id: NodeId,
3110        origin: LayoutPoint,
3111        size: LayoutSize,
3112        content_size: LayoutSize,
3113        out: &mut HashMap<NodeId, LayoutNodeGeometry>,
3114        record: bool,
3115    ) -> LayoutSize {
3116        let mut rect_origin = origin;
3117        let mut rect_size = size;
3118        let mut rect_content = content_size;
3119        let mut had_non_finite = false;
3120
3121        if !rect_origin.x.is_finite() {
3122            rect_origin.x = 0.0;
3123            had_non_finite = true;
3124        }
3125        if !rect_origin.y.is_finite() {
3126            rect_origin.y = 0.0;
3127            had_non_finite = true;
3128        }
3129        if !rect_size.width.is_finite() {
3130            rect_size.width = 0.0;
3131            had_non_finite = true;
3132        }
3133        if !rect_size.height.is_finite() {
3134            rect_size.height = 0.0;
3135            had_non_finite = true;
3136        }
3137        if !rect_content.width.is_finite() {
3138            rect_content.width = 0.0;
3139            had_non_finite = true;
3140        }
3141        if !rect_content.height.is_finite() {
3142            rect_content.height = 0.0;
3143            had_non_finite = true;
3144        }
3145
3146        if had_non_finite {
3147            diag::emit(
3148                diag::DiagCategory::Invariants,
3149                diag::DiagLevel::Error,
3150                diag::DiagEventKind::InvariantViolation {
3151                    kind: "non_finite_layout".into(),
3152                    node: Some(node_id.as_u128()),
3153                    details: format!(
3154                        "origin=({:.2},{:.2}) size=({:.2},{:.2}) content=({:.2},{:.2})",
3155                        origin.x,
3156                        origin.y,
3157                        size.width,
3158                        size.height,
3159                        content_size.width,
3160                        content_size.height
3161                    ),
3162                    dump_ref: None,
3163                },
3164            );
3165        }
3166
3167        if record {
3168            let rect = LayoutRect::new(
3169                rect_origin.x,
3170                rect_origin.y,
3171                rect_size.width,
3172                rect_size.height,
3173            );
3174            out.insert(
3175                node_id,
3176                LayoutNodeGeometry {
3177                    rect,
3178                    content_size: rect_content,
3179                },
3180            );
3181        }
3182        rect_size
3183    }
3184}