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//! [`WidgetId`](fission_ir::WidgetId) to a [`LayoutRect`].
18//!
19//! # Example
20//!
21//! ```rust,no_run
22//! use fission_layout::*;
23//! use fission_ir::{WidgetId, LayoutOp};
24//!
25//! let mut engine = LayoutEngine::new();
26//! let root_id = WidgetId::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, WidgetId};
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::WidgetId;
54///
55/// // A closure works as a ScrollDataSource:
56/// let source = |_node: WidgetId| -> f32 { 0.0 };
57/// assert_eq!(source.get_offset(WidgetId::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: WidgetId) -> f32;
62}
63
64impl<F> ScrollDataSource for F
65where
66    F: Fn(WidgetId) -> f32,
67{
68    fn get_offset(&self, node_id: WidgetId) -> 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: WidgetId, 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<WidgetId>,
339    missing_parent_refs: Vec<(WidgetId, WidgetId)>,
340    missing_child_refs: Vec<(WidgetId, WidgetId)>,
341    parent_child_mismatches: Vec<(WidgetId, WidgetId, Option<WidgetId>)>,
342    cycle_nodes: Vec<WidgetId>,
343    root_nodes: Vec<WidgetId>,
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<WidgetId>,
391    node_fingerprints: HashMap<WidgetId, u64>,
392    nodes: HashMap<WidgetId, LayoutInputNode>,
393    parents: HashMap<WidgetId, Option<WidgetId>>,
394    children: HashMap<WidgetId, Vec<WidgetId>>,
395    roots: Vec<WidgetId>,
396    validation: LayoutGraphValidationState,
397}
398
399#[derive(Debug, Clone, Default)]
400struct IncrementalLayoutReuseState {
401    previous_snapshot: LayoutSnapshot,
402    dirty_ancestors: HashSet<WidgetId>,
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: WidgetId) -> Option<&LayoutInputNode> {
532        self.nodes.get(&node_id)
533    }
534
535    fn children_of(&self, node_id: WidgetId) -> &[WidgetId] {
536        self.children
537            .get(&node_id)
538            .map(Vec::as_slice)
539            .unwrap_or(&[])
540    }
541
542    fn parent_of(&self, node_id: WidgetId) -> Option<WidgetId> {
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<WidgetId> {
553        fn dfs(
554            node_id: WidgetId,
555            children: &HashMap<WidgetId, Vec<WidgetId>>,
556            visited: &mut HashSet<WidgetId>,
557            stack: &mut HashSet<WidgetId>,
558            cycle_nodes: &mut Vec<WidgetId>,
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, WidgetId};
599
600    fn box_node(
601        id: WidgetId,
602        parent_id: Option<WidgetId>,
603        children_ids: Vec<WidgetId>,
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 = WidgetId::from_u128(1);
633        let first = WidgetId::from_u128(2);
634        let second = WidgetId::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 = WidgetId::from_u128(10);
653        let first = WidgetId::from_u128(11);
654        let second = WidgetId::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::WidgetId;
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 [`WidgetId`].
781    pub nodes: HashMap<WidgetId, 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<WidgetId, 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: WidgetId) -> 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: WidgetId) -> 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: WidgetId) -> 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: WidgetId,
830    /// The parent node's ID, or `None` for the root.
831    pub parent_id: Option<WidgetId>,
832    /// The layout operation this node performs.
833    pub op: LayoutOp,
834    /// Ordered list of child node IDs.
835    pub children_ids: Vec<WidgetId>,
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::WidgetId;
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: WidgetId) -> 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(
1149        &self,
1150        input_nodes: &[LayoutInputNode],
1151        root: WidgetId,
1152    ) -> Result<()> {
1153        if self.graph_state.matches_input_nodes(input_nodes) {
1154            return self.validate_graph_state(root);
1155        }
1156
1157        let node_map: HashMap<WidgetId, &LayoutInputNode> =
1158            input_nodes.iter().map(|n| (n.id, n)).collect();
1159        // Parent/child consistency
1160        for n in input_nodes {
1161            for child in &n.children_ids {
1162                let child_node = node_map
1163                    .get(child)
1164                    .ok_or_else(|| anyhow::anyhow!("[verify] child {:?} not found", child))?;
1165                if child_node.parent_id != Some(n.id) {
1166                    anyhow::bail!("[verify] parent/child mismatch parent={:?} child={:?} child.parent_id={:?}", n.id, child, child_node.parent_id);
1167                }
1168            }
1169        }
1170        // Cycle via DFS
1171        fn dfs(
1172            id: WidgetId,
1173            map: &HashMap<WidgetId, &LayoutInputNode>,
1174            visited: &mut HashSet<WidgetId>,
1175            stack: &mut HashSet<WidgetId>,
1176        ) -> Result<()> {
1177            if !visited.insert(id) {
1178                return Ok(());
1179            }
1180            stack.insert(id);
1181            let node = map
1182                .get(&id)
1183                .ok_or_else(|| anyhow::anyhow!("[verify] missing node {:?}", id))?;
1184            for child in &node.children_ids {
1185                if stack.contains(child) {
1186                    anyhow::bail!("[verify] cycle detected at {:?} -> {:?}", id, child);
1187                }
1188                dfs(*child, map, visited, stack)?;
1189            }
1190            stack.remove(&id);
1191            Ok(())
1192        }
1193        let mut visited = HashSet::new();
1194        let mut stack = HashSet::new();
1195        dfs(root, &node_map, &mut visited, &mut stack)?;
1196        Ok(())
1197    }
1198
1199    /// Computes layout for the entire node tree and returns a snapshot.
1200    ///
1201    /// This is the main entry point. It runs the constraint-based layout algorithm
1202    /// starting from `root_node_id`, using `viewport_size` as the root constraints,
1203    /// and querying `scroll_source` for scroll offsets. After layout, it emits scroll
1204    /// diagnostics for debugging.
1205    ///
1206    /// # Arguments
1207    ///
1208    /// * `input_nodes` -- The flat list of all layout nodes.
1209    /// * `root_node_id` -- Which node is the root of the tree.
1210    /// * `viewport_size` -- The size of the window/screen.
1211    /// * `scroll_source` -- Provides scroll offsets for scroll containers.
1212    ///
1213    /// # Errors
1214    ///
1215    /// Returns `Err` if a cycle is detected or a required node is missing.
1216    pub fn compute_layout(
1217        &mut self,
1218        input_nodes: &[LayoutInputNode],
1219        root_node_id: WidgetId,
1220        viewport_size: LayoutSize,
1221        scroll_source: &impl ScrollDataSource,
1222    ) -> Result<LayoutSnapshot> {
1223        self.ensure_graph_state(input_nodes);
1224        self.validate_graph_state(root_node_id)?;
1225        let snapshot = self.compute_layout_constraints(
1226            input_nodes,
1227            root_node_id,
1228            viewport_size,
1229            scroll_source,
1230        )?;
1231        self.emit_scroll_diagnostics(&snapshot);
1232        Ok(snapshot)
1233    }
1234
1235    /// InternalLower-level layout that skips scroll diagnostics.
1236    ///
1237    /// Same as [`compute_layout`](LayoutEngine::compute_layout) but does not emit
1238    /// diagnostic events. Useful when you need the snapshot but not the debug output.
1239    pub fn compute_layout_constraints(
1240        &mut self,
1241        input_nodes: &[LayoutInputNode],
1242        root_node_id: WidgetId,
1243        viewport_size: LayoutSize,
1244        scroll_source: &impl ScrollDataSource,
1245    ) -> Result<LayoutSnapshot> {
1246        self.ensure_graph_state(input_nodes);
1247        self.validate_graph_state(root_node_id)?;
1248
1249        // Root constraints should be tight to the viewport size if no explicit size is given
1250        let mut constraints = BoxConstraints::tight(viewport_size);
1251        if let Some(root) = self.graph_state.node(root_node_id) {
1252            // Only loosen if explicit dimensions are provided for the root node
1253            if root.width.is_some() || root.height.is_some() {
1254                constraints = BoxConstraints::loose(viewport_size.width, viewport_size.height)
1255                    .tighten(root.width, root.height);
1256            }
1257        }
1258
1259        let mut snapshot = LayoutSnapshot::new(viewport_size);
1260        let mut measure_cache = HashMap::new();
1261        self.layout_node_constraints(
1262            root_node_id,
1263            constraints,
1264            LayoutPoint::ZERO,
1265            &mut snapshot.nodes,
1266            &mut snapshot.constraints,
1267            &mut measure_cache,
1268            scroll_source,
1269            true,
1270            0,
1271        )?;
1272
1273        let visual_location = |node_id: WidgetId| -> Option<LayoutPoint> {
1274            let mut pos = snapshot.nodes.get(&node_id)?.rect.origin;
1275            let mut current = self.graph_state.parent_of(node_id);
1276            while let Some(parent_id) = current {
1277                if let Some(parent) = self.graph_state.node(parent_id) {
1278                    if let LayoutOp::Scroll { direction, .. } = &parent.op {
1279                        let offset = scroll_source.get_offset(parent_id);
1280                        match direction {
1281                            FlexDirection::Row => pos.x -= offset,
1282                            FlexDirection::Column => pos.y -= offset,
1283                        }
1284                    }
1285                    current = self.graph_state.parent_of(parent_id);
1286                } else {
1287                    break;
1288                }
1289            }
1290            Some(pos)
1291        };
1292
1293        let mut flyout_abs_overrides: HashMap<WidgetId, (f32, f32)> = HashMap::new();
1294        for node in self.graph_state.ordered_nodes() {
1295            if let LayoutOp::Flyout { anchor, content } = node.op {
1296                if let (Some(anchor_geom), Some(content_geom)) =
1297                    (snapshot.nodes.get(&anchor), snapshot.nodes.get(&content))
1298                {
1299                    if let Some(anchor_abs) = visual_location(anchor) {
1300                        let content_w = content_geom.rect.width();
1301                        let content_h = content_geom.rect.height();
1302                        let anchor_h = anchor_geom.rect.height();
1303                        let max_left = (snapshot.viewport_size.width - content_w).max(0.0);
1304                        let left_rel = anchor_abs.x.clamp(0.0, max_left);
1305
1306                        let below_top = anchor_abs.y + anchor_h;
1307                        let max_top = (snapshot.viewport_size.height - content_h).max(0.0);
1308                        let top_rel = if below_top + content_h <= snapshot.viewport_size.height {
1309                            below_top
1310                        } else {
1311                            let above_top = anchor_abs.y - content_h;
1312                            if above_top >= 0.0 {
1313                                above_top
1314                            } else {
1315                                below_top.clamp(0.0, max_top)
1316                            }
1317                        };
1318                        flyout_abs_overrides.insert(content, (left_rel, top_rel));
1319                    }
1320                }
1321            }
1322        }
1323
1324        if !flyout_abs_overrides.is_empty() {
1325            for (nid, (abs_x, abs_y)) in flyout_abs_overrides {
1326                if let Some(current) = snapshot.nodes.get(&nid) {
1327                    let dx = abs_x - current.rect.origin.x;
1328                    let dy = abs_y - current.rect.origin.y;
1329                    let mut stack = vec![(nid, 0usize)];
1330                    while let Some((current_id, depth)) = stack.pop() {
1331                        if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1332                            return Err(self.layout_depth_overflow(current_id, depth));
1333                        }
1334                        if let Some(geometry) = snapshot.nodes.get_mut(&current_id) {
1335                            geometry.rect.origin.x += dx;
1336                            geometry.rect.origin.y += dy;
1337                        }
1338                        for child_id in self.graph_state.children_of(current_id).iter().rev() {
1339                            stack.push((*child_id, depth + 1));
1340                        }
1341                    }
1342                }
1343            }
1344        }
1345
1346        self.graph_state.mark_layout_complete();
1347        self.incremental_reuse = None;
1348
1349        Ok(snapshot)
1350    }
1351
1352    pub fn compute_layout_incremental(
1353        &mut self,
1354        input_nodes: &[LayoutInputNode],
1355        root_node_id: WidgetId,
1356        viewport_size: LayoutSize,
1357        scroll_source: &impl ScrollDataSource,
1358        previous_snapshot: &LayoutSnapshot,
1359        dirty_nodes: &HashSet<WidgetId>,
1360    ) -> Result<LayoutSnapshot> {
1361        self.ensure_graph_state(input_nodes);
1362        self.validate_graph_state(root_node_id)?;
1363
1364        let mut dirty_ancestors = HashSet::new();
1365        for node_id in dirty_nodes {
1366            let mut current = Some(*node_id);
1367            while let Some(id) = current {
1368                if !dirty_ancestors.insert(id) {
1369                    break;
1370                }
1371                current = self.graph_state.parent_of(id);
1372            }
1373        }
1374        dirty_ancestors.insert(root_node_id);
1375
1376        self.incremental_reuse = Some(IncrementalLayoutReuseState {
1377            previous_snapshot: previous_snapshot.clone(),
1378            dirty_ancestors,
1379        });
1380        let result = self.compute_layout_constraints(
1381            input_nodes,
1382            root_node_id,
1383            viewport_size,
1384            scroll_source,
1385        );
1386        self.incremental_reuse = None;
1387        result
1388    }
1389
1390    fn emit_scroll_diagnostics(&self, snapshot: &LayoutSnapshot) {
1391        use fission_diagnostics::prelude as diag;
1392        let trace_scroll = std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
1393        for n in self.graph_state.ordered_nodes() {
1394            if let LayoutOp::Scroll { .. } = n.op {
1395                if let Some(g) = snapshot.nodes.get(&n.id) {
1396                    let note = if g.rect.height() <= 0.0 {
1397                        let parent_op = n
1398                            .parent_id
1399                            .and_then(|pid| self.graph_state.node(pid))
1400                            .map(|p| format!("{:?}", p.op));
1401                        let parent_constraints = n
1402                            .parent_id
1403                            .and_then(|pid| snapshot.constraints.get(&pid))
1404                            .copied();
1405                        snapshot
1406                            .constraints
1407                            .get(&n.id)
1408                            .map(|c| {
1409                                format!(
1410                                    "op={:?} parent={:?} parent_op={:?} parent_constraints={:?} constraints={:?}",
1411                                    n.op,
1412                                    n.parent_id,
1413                                    parent_op,
1414                                    parent_constraints,
1415                                    c
1416                                )
1417                            })
1418                    } else {
1419                        None
1420                    };
1421                    diag::emit(
1422                        diag::DiagCategory::Layout,
1423                        diag::DiagLevel::Debug,
1424                        diag::DiagEventKind::ScrollExtent {
1425                            node: n.id.as_u128(),
1426                            viewport_w: g.rect.width(),
1427                            viewport_h: g.rect.height(),
1428                            content_w: g.content_size.width,
1429                            content_h: g.content_size.height,
1430                            note,
1431                        },
1432                    );
1433                    if trace_scroll {
1434                        eprintln!(
1435                            "[scroll-trace] node={} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
1436                            n.id.as_u128(),
1437                            g.rect.width(),
1438                            g.rect.height(),
1439                            g.content_size.width,
1440                            g.content_size.height
1441                        );
1442                    }
1443                }
1444            }
1445        }
1446    }
1447
1448    fn layout_depth_overflow(&self, node_id: WidgetId, depth: usize) -> anyhow::Error {
1449        let details = format!(
1450            "layout recursion depth {} exceeded max {} at node {}",
1451            depth,
1452            Self::MAX_LAYOUT_RECURSION_DEPTH,
1453            node_id.as_u128()
1454        );
1455        diag::emit(
1456            diag::DiagCategory::Invariants,
1457            diag::DiagLevel::Error,
1458            diag::DiagEventKind::InvariantViolation {
1459                kind: "layout_recursion_depth".into(),
1460                node: Some(node_id.as_u128()),
1461                details: details.clone(),
1462                dump_ref: None,
1463            },
1464        );
1465        anyhow::anyhow!(details)
1466    }
1467
1468    fn copy_cached_subtree(
1469        &self,
1470        node_id: WidgetId,
1471        origin: LayoutPoint,
1472        current_constraints: BoxConstraints,
1473        out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
1474        constraints_out: &mut HashMap<WidgetId, BoxConstraints>,
1475    ) -> Result<Option<LayoutSize>> {
1476        let Some(reuse) = self.incremental_reuse.as_ref() else {
1477            return Ok(None);
1478        };
1479        if reuse.dirty_ancestors.contains(&node_id) {
1480            return Ok(None);
1481        }
1482
1483        let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(&node_id) else {
1484            return Ok(None);
1485        };
1486        let Some(previous_constraints) = reuse.previous_snapshot.constraints.get(&node_id).copied()
1487        else {
1488            return Ok(None);
1489        };
1490        if previous_constraints != current_constraints {
1491            return Ok(None);
1492        }
1493
1494        let dx = origin.x - previous_geometry.rect.origin.x;
1495        let dy = origin.y - previous_geometry.rect.origin.y;
1496        let mut stack = vec![(node_id, 0usize)];
1497        while let Some((current_id, depth)) = stack.pop() {
1498            if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1499                return Err(self.layout_depth_overflow(current_id, depth));
1500            }
1501            let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(&current_id) else {
1502                return Ok(None);
1503            };
1504            let Some(previous_constraints) = reuse
1505                .previous_snapshot
1506                .constraints
1507                .get(&current_id)
1508                .copied()
1509            else {
1510                return Ok(None);
1511            };
1512
1513            let mut geometry = previous_geometry.clone();
1514            geometry.rect.origin.x += dx;
1515            geometry.rect.origin.y += dy;
1516            out.insert(current_id, geometry);
1517            constraints_out.insert(current_id, previous_constraints);
1518
1519            let children = self.graph_state.children_of(current_id);
1520            for child_id in children.iter().rev() {
1521                stack.push((*child_id, depth + 1));
1522            }
1523        }
1524
1525        Ok(Some(previous_geometry.content_size))
1526    }
1527
1528    fn layout_node_constraints(
1529        &self,
1530        node_id: WidgetId,
1531        constraints: BoxConstraints,
1532        origin: LayoutPoint,
1533        out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
1534        constraints_out: &mut HashMap<WidgetId, BoxConstraints>,
1535        measure_cache: &mut HashMap<MeasureCacheKey, LayoutSize>,
1536        scroll_source: &impl ScrollDataSource,
1537        record: bool,
1538        depth: usize,
1539    ) -> Result<LayoutSize> {
1540        if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1541            return Err(self.layout_depth_overflow(node_id, depth));
1542        }
1543        if !record {
1544            let cache_key = MeasureCacheKey::new(node_id, constraints);
1545            if let Some(cached) = measure_cache.get(&cache_key).copied() {
1546                return Ok(cached);
1547            }
1548        }
1549        let node = match self.graph_state.node(node_id) {
1550            Some(node) => node,
1551            None => return Ok(LayoutSize::ZERO),
1552        };
1553
1554        if record {
1555            constraints_out.insert(node_id, constraints);
1556        }
1557
1558        if record {
1559            if let Some(reused) =
1560                self.copy_cached_subtree(node_id, origin, constraints, out, constraints_out)?
1561            {
1562                return Ok(reused);
1563            }
1564        }
1565
1566        let mut flow_children: Vec<WidgetId> = Vec::new();
1567        let mut abs_children: Vec<WidgetId> = Vec::new();
1568        for child_id in self.graph_state.children_of(node_id) {
1569            let is_absolute = matches!(
1570                self.graph_state.node(*child_id).map(|n| &n.op),
1571                Some(LayoutOp::AbsoluteFill) | Some(LayoutOp::Positioned { .. })
1572            );
1573            if is_absolute {
1574                abs_children.push(*child_id);
1575            } else {
1576                flow_children.push(*child_id);
1577            }
1578        }
1579        let rich_text_inline_children = node.rich_text.is_some() && !flow_children.is_empty();
1580
1581        let mut content_size;
1582        let size = match &node.op {
1583            LayoutOp::Box {
1584                width,
1585                height,
1586                min_width,
1587                max_width,
1588                min_height,
1589                max_height,
1590                padding,
1591                aspect_ratio,
1592                ..
1593            } => {
1594                let mut local =
1595                    constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
1596                local = local.tighten(*width, *height);
1597                if let Some(ratio) = aspect_ratio.filter(|r| *r > 0.0) {
1598                    let mut target_w = *width;
1599                    let mut target_h = *height;
1600
1601                    if target_w.is_some() && target_h.is_none() {
1602                        target_h = Some(target_w.unwrap() / ratio);
1603                    } else if target_h.is_some() && target_w.is_none() {
1604                        target_w = Some(target_h.unwrap() * ratio);
1605                    } else if target_w.is_none() && target_h.is_none() {
1606                        if local.is_width_bounded() || local.is_height_bounded() {
1607                            let (mut w, mut h) = if local.is_width_bounded() {
1608                                let w = local.max_w;
1609                                let h = w / ratio;
1610                                (w, h)
1611                            } else {
1612                                let h = local.max_h;
1613                                let w = h * ratio;
1614                                (w, h)
1615                            };
1616                            if local.is_width_bounded()
1617                                && local.is_height_bounded()
1618                                && h > local.max_h
1619                            {
1620                                h = local.max_h;
1621                                w = h * ratio;
1622                            }
1623                            target_w = Some(w);
1624                            target_h = Some(h);
1625                        }
1626                    }
1627
1628                    if target_w.is_some() || target_h.is_some() {
1629                        local = local.tighten(target_w, target_h);
1630                    }
1631                }
1632                let base_child_constraints = local.deflate(*padding);
1633                let mut max_child = LayoutSize::ZERO;
1634                let mut measured_children: Vec<(WidgetId, BoxConstraints, LayoutSize)> = Vec::new();
1635                if !rich_text_inline_children {
1636                    for child_id in &flow_children {
1637                        let (child_width, child_height, child_max_width, child_max_height) = self
1638                            .graph_state
1639                            .node(*child_id)
1640                            .map(|child| match &child.op {
1641                                LayoutOp::Box {
1642                                    width,
1643                                    height,
1644                                    max_width,
1645                                    max_height,
1646                                    ..
1647                                } => (*width, *height, *max_width, *max_height),
1648                                LayoutOp::Scroll {
1649                                    width,
1650                                    height,
1651                                    max_width,
1652                                    max_height,
1653                                    ..
1654                                } => (*width, *height, *max_width, *max_height),
1655                                LayoutOp::Embed { width, height, .. } => {
1656                                    (*width, *height, None, None)
1657                                }
1658                                _ => (None, None, None, None),
1659                            })
1660                            .unwrap_or((None, None, None, None));
1661                        let mut child_constraints = base_child_constraints;
1662                        let stretch_width = child_constraints.min_w == child_constraints.max_w
1663                            && child_width.is_none()
1664                            && child_max_width.is_none();
1665                        if stretch_width {
1666                            child_constraints.min_w = child_constraints.max_w;
1667                        } else {
1668                            child_constraints.min_w = 0.0;
1669                        }
1670                        let stretch_height = child_constraints.min_h == child_constraints.max_h
1671                            && child_height.is_none()
1672                            && child_max_height.is_none();
1673                        if stretch_height {
1674                            child_constraints.min_h = child_constraints.max_h;
1675                        } else {
1676                            child_constraints.min_h = 0.0;
1677                        }
1678                        let child_size = self.layout_node_constraints(
1679                            *child_id,
1680                            child_constraints,
1681                            LayoutPoint::ZERO,
1682                            out,
1683                            constraints_out,
1684                            measure_cache,
1685                            scroll_source,
1686                            false,
1687                            depth + 1,
1688                        )?;
1689                        max_child.width = max_child.width.max(child_size.width);
1690                        max_child.height = max_child.height.max(child_size.height);
1691                        measured_children.push((*child_id, child_constraints, child_size));
1692                    }
1693                }
1694                let padded = LayoutSize::new(
1695                    max_child.width + padding[0] + padding[1],
1696                    max_child.height + padding[2] + padding[3],
1697                );
1698                let size = local.constrain(padded);
1699                if record {
1700                    for (child_id, child_constraints, _child_size) in measured_children {
1701                        self.layout_node_constraints(
1702                            child_id,
1703                            child_constraints,
1704                            LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
1705                            out,
1706                            constraints_out,
1707                            measure_cache,
1708                            scroll_source,
1709                            record,
1710                            depth + 1,
1711                        )?;
1712                    }
1713                    if !abs_children.is_empty() {
1714                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
1715                        for child_id in abs_children {
1716                            self.layout_node_constraints(
1717                                child_id,
1718                                abs_constraints,
1719                                origin,
1720                                out,
1721                                constraints_out,
1722                                measure_cache,
1723                                scroll_source,
1724                                record,
1725                                depth + 1,
1726                            )?;
1727                        }
1728                    }
1729                }
1730                content_size = padded;
1731                size
1732            }
1733            LayoutOp::Flex {
1734                direction,
1735                wrap,
1736                padding,
1737                gap,
1738                align_items,
1739                justify_content,
1740                flex_grow,
1741                ..
1742            } => {
1743                let gap = gap.unwrap_or(0.0);
1744                let local = constraints.tighten(node.width, node.height);
1745                let inner = local.deflate(*padding);
1746                let is_row = matches!(direction, IrFlexDirection::Row);
1747
1748                let max_main = if is_row { inner.max_w } else { inner.max_h };
1749                let max_cross = if is_row { inner.max_h } else { inner.max_w };
1750                let min_main = if is_row { inner.min_w } else { inner.min_h };
1751                let min_cross = if is_row { inner.min_h } else { inner.min_w };
1752                let main_bounded = if is_row {
1753                    inner.is_width_bounded()
1754                } else {
1755                    inner.is_height_bounded()
1756                };
1757                let cross_bounded = if is_row {
1758                    inner.is_height_bounded()
1759                } else {
1760                    inner.is_width_bounded()
1761                };
1762
1763                if matches!(wrap, IrFlexWrap::Wrap | IrFlexWrap::WrapReverse) {
1764                    let mut lines: Vec<(Vec<(WidgetId, LayoutSize, BoxConstraints)>, f32, f32)> =
1765                        Vec::new();
1766                    let mut line_children: Vec<(WidgetId, LayoutSize, BoxConstraints)> = Vec::new();
1767                    let mut line_main = 0.0f32;
1768                    let mut line_cross = 0.0f32;
1769                    let mut max_line_main = 0.0f32;
1770
1771                    for child_id in &flow_children {
1772                        let child_constraints = if is_row {
1773                            BoxConstraints {
1774                                min_w: 0.0,
1775                                max_w: max_main,
1776                                min_h: 0.0,
1777                                max_h: max_cross,
1778                            }
1779                        } else {
1780                            BoxConstraints {
1781                                min_w: 0.0,
1782                                max_w: max_cross,
1783                                min_h: 0.0,
1784                                max_h: max_main,
1785                            }
1786                        };
1787                        let child_size = self.layout_node_constraints(
1788                            *child_id,
1789                            child_constraints,
1790                            LayoutPoint::ZERO,
1791                            out,
1792                            constraints_out,
1793                            measure_cache,
1794                            scroll_source,
1795                            false,
1796                            depth + 1,
1797                        )?;
1798                        let child_main = if is_row {
1799                            child_size.width
1800                        } else {
1801                            child_size.height
1802                        };
1803                        let child_cross = if is_row {
1804                            child_size.height
1805                        } else {
1806                            child_size.width
1807                        };
1808                        let next_main = if line_children.is_empty() {
1809                            child_main
1810                        } else {
1811                            line_main + gap + child_main
1812                        };
1813
1814                        if main_bounded && !line_children.is_empty() && next_main > max_main {
1815                            max_line_main = max_line_main.max(line_main);
1816                            lines.push((line_children, line_main, line_cross));
1817                            line_children = Vec::new();
1818                            line_main = 0.0;
1819                            line_cross = 0.0;
1820                        }
1821
1822                        if !line_children.is_empty() {
1823                            line_main += gap;
1824                        }
1825                        line_main += child_main;
1826                        line_cross = line_cross.max(child_cross);
1827                        line_children.push((*child_id, child_size, child_constraints));
1828                    }
1829
1830                    if !line_children.is_empty() {
1831                        max_line_main = max_line_main.max(line_main);
1832                        lines.push((line_children, line_main, line_cross));
1833                    }
1834
1835                    let mut container_main = if main_bounded && *flex_grow > 0.0 {
1836                        max_main
1837                    } else {
1838                        max_line_main
1839                    };
1840                    container_main = container_main.max(min_main);
1841                    let total_lines_cross: f32 =
1842                        lines.iter().map(|(_, _, cross)| *cross).sum::<f32>()
1843                            + gap * lines.len().saturating_sub(1) as f32;
1844                    let container_cross = total_lines_cross.max(min_cross);
1845                    let size = if is_row {
1846                        local.constrain(LayoutSize::new(
1847                            container_main + padding[0] + padding[1],
1848                            container_cross + padding[2] + padding[3],
1849                        ))
1850                    } else {
1851                        local.constrain(LayoutSize::new(
1852                            container_cross + padding[0] + padding[1],
1853                            container_main + padding[2] + padding[3],
1854                        ))
1855                    };
1856
1857                    let inner_main = if is_row {
1858                        size.width - padding[0] - padding[1]
1859                    } else {
1860                        size.height - padding[2] - padding[3]
1861                    };
1862                    let inner_cross = if is_row {
1863                        size.height - padding[2] - padding[3]
1864                    } else {
1865                        size.width - padding[0] - padding[1]
1866                    };
1867
1868                    let mut ordered_lines = lines;
1869                    if matches!(wrap, IrFlexWrap::WrapReverse) {
1870                        ordered_lines.reverse();
1871                    }
1872
1873                    let mut line_cursor = if matches!(wrap, IrFlexWrap::WrapReverse) {
1874                        (inner_cross - total_lines_cross).max(0.0)
1875                    } else {
1876                        0.0
1877                    };
1878
1879                    for (line_children, line_main, line_cross) in ordered_lines {
1880                        let remaining_space = (inner_main - line_main).max(0.0);
1881                        let mut extra_gap = 0.0;
1882                        let mut offset_main = 0.0;
1883                        match justify_content {
1884                            fission_ir::op::JustifyContent::Start => {}
1885                            fission_ir::op::JustifyContent::End => offset_main = remaining_space,
1886                            fission_ir::op::JustifyContent::Center => {
1887                                offset_main = remaining_space / 2.0
1888                            }
1889                            fission_ir::op::JustifyContent::SpaceBetween => {
1890                                if line_children.len() > 1 {
1891                                    extra_gap =
1892                                        remaining_space / (line_children.len() as f32 - 1.0);
1893                                }
1894                            }
1895                            fission_ir::op::JustifyContent::SpaceAround => {
1896                                if !line_children.is_empty() {
1897                                    extra_gap = remaining_space / line_children.len() as f32;
1898                                    offset_main = extra_gap / 2.0;
1899                                }
1900                            }
1901                            fission_ir::op::JustifyContent::SpaceEvenly => {
1902                                if !line_children.is_empty() {
1903                                    extra_gap =
1904                                        remaining_space / (line_children.len() as f32 + 1.0);
1905                                    offset_main = extra_gap;
1906                                }
1907                            }
1908                        }
1909
1910                        let mut cursor = offset_main;
1911                        for (child_id, child_size, mut child_constraints) in line_children {
1912                            let child_main = if is_row {
1913                                child_size.width
1914                            } else {
1915                                child_size.height
1916                            };
1917                            let child_cross = if is_row {
1918                                child_size.height
1919                            } else {
1920                                child_size.width
1921                            };
1922                            if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
1923                                if is_row {
1924                                    child_constraints.min_h = line_cross;
1925                                    child_constraints.max_h = line_cross;
1926                                } else {
1927                                    child_constraints.min_w = line_cross;
1928                                    child_constraints.max_w = line_cross;
1929                                }
1930                            }
1931                            let cross_offset = match align_items {
1932                                fission_ir::op::AlignItems::Start
1933                                | fission_ir::op::AlignItems::Stretch => 0.0,
1934                                fission_ir::op::AlignItems::End => {
1935                                    (line_cross - child_cross).max(0.0)
1936                                }
1937                                fission_ir::op::AlignItems::Center => {
1938                                    ((line_cross - child_cross) / 2.0).max(0.0)
1939                                }
1940                                fission_ir::op::AlignItems::Baseline => 0.0,
1941                            };
1942                            let child_origin = if is_row {
1943                                LayoutPoint::new(
1944                                    origin.x + padding[0] + cursor,
1945                                    origin.y + padding[2] + line_cursor + cross_offset,
1946                                )
1947                            } else {
1948                                LayoutPoint::new(
1949                                    origin.x + padding[0] + line_cursor + cross_offset,
1950                                    origin.y + padding[2] + cursor,
1951                                )
1952                            };
1953                            self.layout_node_constraints(
1954                                child_id,
1955                                child_constraints,
1956                                child_origin,
1957                                out,
1958                                constraints_out,
1959                                measure_cache,
1960                                scroll_source,
1961                                record,
1962                                depth + 1,
1963                            )?;
1964                            cursor += child_main + gap + extra_gap;
1965                        }
1966
1967                        line_cursor += line_cross + gap;
1968                    }
1969
1970                    if record && !abs_children.is_empty() {
1971                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
1972                        for child_id in abs_children {
1973                            self.layout_node_constraints(
1974                                child_id,
1975                                abs_constraints,
1976                                origin,
1977                                out,
1978                                constraints_out,
1979                                measure_cache,
1980                                scroll_source,
1981                                record,
1982                                depth + 1,
1983                            )?;
1984                        }
1985                    }
1986                    content_size = size;
1987                    size
1988                } else {
1989                    struct FlexChildEntry {
1990                        id: WidgetId,
1991                        flex: f32,
1992                        size: LayoutSize,
1993                        constraints: BoxConstraints,
1994                        is_flex: bool,
1995                    }
1996                    let mut measured: Vec<FlexChildEntry> = Vec::new();
1997                    let mut total_flex = 0.0f32;
1998                    let mut nonflex_main = 0.0f32;
1999                    let mut max_child_cross = 0.0f32;
2000                    let treat_flex_as_nonflex = !main_bounded;
2001
2002                    for child_id in &flow_children {
2003                        let child = match self.graph_state.node(*child_id) {
2004                            Some(child) => child,
2005                            None => continue,
2006                        };
2007                        let flex = child.flex_grow;
2008                        if flex > 0.0 && !treat_flex_as_nonflex {
2009                            total_flex += flex;
2010                            measured.push(FlexChildEntry {
2011                                id: *child_id,
2012                                flex,
2013                                size: LayoutSize::ZERO,
2014                                constraints: BoxConstraints::loose(0.0, 0.0),
2015                                is_flex: true,
2016                            });
2017                            continue;
2018                        }
2019                        let child_constraints = if is_row {
2020                            let cross =
2021                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2022                                    && cross_bounded
2023                                {
2024                                    BoxConstraints {
2025                                        min_w: 0.0,
2026                                        max_w: f32::INFINITY,
2027                                        min_h: max_cross,
2028                                        max_h: max_cross,
2029                                    }
2030                                } else {
2031                                    BoxConstraints {
2032                                        min_w: 0.0,
2033                                        max_w: f32::INFINITY,
2034                                        min_h: 0.0,
2035                                        max_h: max_cross,
2036                                    }
2037                                };
2038                            cross
2039                        } else {
2040                            let cross =
2041                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2042                                    && cross_bounded
2043                                {
2044                                    BoxConstraints {
2045                                        min_w: max_cross,
2046                                        max_w: max_cross,
2047                                        min_h: 0.0,
2048                                        max_h: f32::INFINITY,
2049                                    }
2050                                } else {
2051                                    BoxConstraints {
2052                                        min_w: 0.0,
2053                                        max_w: max_cross,
2054                                        min_h: 0.0,
2055                                        max_h: f32::INFINITY,
2056                                    }
2057                                };
2058                            cross
2059                        };
2060                        let child_size = self.layout_node_constraints(
2061                            *child_id,
2062                            child_constraints,
2063                            LayoutPoint::ZERO,
2064                            out,
2065                            constraints_out,
2066                            measure_cache,
2067                            scroll_source,
2068                            false,
2069                            depth + 1,
2070                        )?;
2071                        let child_main = if is_row {
2072                            child_size.width
2073                        } else {
2074                            child_size.height
2075                        };
2076                        let child_cross = if is_row {
2077                            child_size.height
2078                        } else {
2079                            child_size.width
2080                        };
2081                        nonflex_main += child_main;
2082                        max_child_cross = max_child_cross.max(child_cross);
2083                        measured.push(FlexChildEntry {
2084                            id: *child_id,
2085                            flex,
2086                            size: child_size,
2087                            constraints: child_constraints,
2088                            is_flex: false,
2089                        });
2090                    }
2091
2092                    let gap_total = gap * flow_children.len().saturating_sub(1) as f32;
2093                    let remaining = if main_bounded {
2094                        (max_main - nonflex_main - gap_total).max(0.0)
2095                    } else {
2096                        0.0
2097                    };
2098
2099                    for entry in measured.iter_mut().filter(|e| e.is_flex) {
2100                        let flex = entry.flex;
2101                        let allocated = if main_bounded && total_flex > 0.0 {
2102                            remaining * (flex / total_flex)
2103                        } else {
2104                            0.0
2105                        };
2106                        let child_constraints = if is_row {
2107                            let cross =
2108                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2109                                    && cross_bounded
2110                                {
2111                                    BoxConstraints {
2112                                        min_w: allocated,
2113                                        max_w: allocated,
2114                                        min_h: max_cross,
2115                                        max_h: max_cross,
2116                                    }
2117                                } else {
2118                                    BoxConstraints {
2119                                        min_w: allocated,
2120                                        max_w: allocated,
2121                                        min_h: 0.0,
2122                                        max_h: max_cross,
2123                                    }
2124                                };
2125                            cross
2126                        } else {
2127                            let cross =
2128                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2129                                    && cross_bounded
2130                                {
2131                                    BoxConstraints {
2132                                        min_w: max_cross,
2133                                        max_w: max_cross,
2134                                        min_h: allocated,
2135                                        max_h: allocated,
2136                                    }
2137                                } else {
2138                                    BoxConstraints {
2139                                        min_w: 0.0,
2140                                        max_w: max_cross,
2141                                        min_h: allocated,
2142                                        max_h: allocated,
2143                                    }
2144                                };
2145                            cross
2146                        };
2147                        let child_size = self.layout_node_constraints(
2148                            entry.id,
2149                            child_constraints,
2150                            LayoutPoint::ZERO,
2151                            out,
2152                            constraints_out,
2153                            measure_cache,
2154                            scroll_source,
2155                            false,
2156                            depth + 1,
2157                        )?;
2158                        let child_cross = if is_row {
2159                            child_size.height
2160                        } else {
2161                            child_size.width
2162                        };
2163                        max_child_cross = max_child_cross.max(child_cross);
2164                        entry.size = child_size;
2165                        entry.constraints = child_constraints;
2166                    }
2167
2168                    let final_children_main: f32 = measured
2169                        .iter()
2170                        .map(|entry| {
2171                            if is_row {
2172                                entry.size.width
2173                            } else {
2174                                entry.size.height
2175                            }
2176                        })
2177                        .sum();
2178
2179                    let mut container_main = if main_bounded && *flex_grow > 0.0 {
2180                        max_main
2181                    } else {
2182                        final_children_main + gap_total
2183                    };
2184                    container_main = container_main.max(min_main);
2185
2186                    if main_bounded && final_children_main + gap_total > max_main {
2187                        // SHRINK logic
2188                        let mut total_shrink_scaled = 0.0f32;
2189                        for entry in &measured {
2190                            let child = self.graph_state.node(entry.id).unwrap();
2191                            let main_size = if is_row {
2192                                entry.size.width
2193                            } else {
2194                                entry.size.height
2195                            };
2196                            total_shrink_scaled += main_size * child.flex_shrink;
2197                        }
2198
2199                        if total_shrink_scaled > 0.0 {
2200                            let overflow = (final_children_main + gap_total) - max_main;
2201                            for entry in &mut measured {
2202                                let child = self.graph_state.node(entry.id).unwrap();
2203                                let main_size = if is_row {
2204                                    entry.size.width
2205                                } else {
2206                                    entry.size.height
2207                                };
2208                                let shrink_amount = (main_size * child.flex_shrink
2209                                    / total_shrink_scaled)
2210                                    * overflow;
2211                                // Don't shrink below a reasonable minimum. Items with
2212                                // flex_shrink > 0 can shrink but not to zero - preserve at
2213                                // least a small fraction of their natural size.
2214                                let floor = if child.flex_shrink > 0.0 {
2215                                    // Check for explicit min/fixed dimension
2216                                    let explicit_min = match &child.op {
2217                                        LayoutOp::Box {
2218                                            min_width,
2219                                            min_height,
2220                                            height,
2221                                            width,
2222                                            ..
2223                                        } => {
2224                                            if is_row {
2225                                                min_width.or(*width).unwrap_or(0.0)
2226                                            } else {
2227                                                min_height.or(*height).unwrap_or(0.0)
2228                                            }
2229                                        }
2230                                        _ => 0.0,
2231                                    };
2232                                    explicit_min
2233                                } else {
2234                                    main_size // flex_shrink == 0 means don't shrink at all
2235                                };
2236                                let new_main = (main_size - shrink_amount).max(floor);
2237
2238                                let mut child_constraints = entry.constraints;
2239                                if is_row {
2240                                    child_constraints.min_w = new_main;
2241                                    child_constraints.max_w = new_main;
2242                                } else {
2243                                    child_constraints.min_h = new_main;
2244                                    child_constraints.max_h = new_main;
2245                                }
2246                                let new_size = self.layout_node_constraints(
2247                                    entry.id,
2248                                    child_constraints,
2249                                    LayoutPoint::ZERO,
2250                                    out,
2251                                    constraints_out,
2252                                    measure_cache,
2253                                    scroll_source,
2254                                    false,
2255                                    depth + 1,
2256                                )?;
2257                                entry.size = new_size;
2258                                entry.constraints = child_constraints;
2259                            }
2260                        }
2261                    }
2262
2263                    let container_cross = max_child_cross.max(min_cross);
2264                    let size = if is_row {
2265                        local.constrain(LayoutSize::new(
2266                            container_main + padding[0] + padding[1],
2267                            container_cross + padding[2] + padding[3],
2268                        ))
2269                    } else {
2270                        local.constrain(LayoutSize::new(
2271                            container_cross + padding[0] + padding[1],
2272                            container_main + padding[2] + padding[3],
2273                        ))
2274                    };
2275
2276                    let inner_main = if is_row {
2277                        size.width - padding[0] - padding[1]
2278                    } else {
2279                        size.height - padding[2] - padding[3]
2280                    };
2281                    let inner_cross = if is_row {
2282                        size.height - padding[2] - padding[3]
2283                    } else {
2284                        size.width - padding[0] - padding[1]
2285                    };
2286
2287                    let final_children_main: f32 = measured
2288                        .iter()
2289                        .map(|entry| {
2290                            if is_row {
2291                                entry.size.width
2292                            } else {
2293                                entry.size.height
2294                            }
2295                        })
2296                        .sum();
2297
2298                    let remaining_space = (inner_main - final_children_main - gap_total).max(0.0);
2299                    let mut extra_gap = 0.0;
2300                    let mut offset_main = 0.0;
2301                    match justify_content {
2302                        fission_ir::op::JustifyContent::Start => {}
2303                        fission_ir::op::JustifyContent::End => offset_main = remaining_space,
2304                        fission_ir::op::JustifyContent::Center => {
2305                            offset_main = remaining_space / 2.0
2306                        }
2307                        fission_ir::op::JustifyContent::SpaceBetween => {
2308                            if measured.len() > 1 {
2309                                extra_gap = remaining_space / (measured.len() as f32 - 1.0);
2310                            }
2311                        }
2312                        fission_ir::op::JustifyContent::SpaceAround => {
2313                            if !measured.is_empty() {
2314                                extra_gap = remaining_space / measured.len() as f32;
2315                                offset_main = extra_gap / 2.0;
2316                            }
2317                        }
2318                        fission_ir::op::JustifyContent::SpaceEvenly => {
2319                            if !measured.is_empty() {
2320                                extra_gap = remaining_space / (measured.len() as f32 + 1.0);
2321                                offset_main = extra_gap;
2322                            }
2323                        }
2324                    }
2325
2326                    let mut cursor = offset_main;
2327                    for entry in measured {
2328                        let child_main = if is_row {
2329                            entry.size.width
2330                        } else {
2331                            entry.size.height
2332                        };
2333                        let child_cross = if is_row {
2334                            entry.size.height
2335                        } else {
2336                            entry.size.width
2337                        };
2338                        let cross_offset = match align_items {
2339                            fission_ir::op::AlignItems::Start
2340                            | fission_ir::op::AlignItems::Stretch => 0.0,
2341                            fission_ir::op::AlignItems::End => (inner_cross - child_cross).max(0.0),
2342                            fission_ir::op::AlignItems::Center => {
2343                                ((inner_cross - child_cross) / 2.0).max(0.0)
2344                            }
2345                            fission_ir::op::AlignItems::Baseline => 0.0,
2346                        };
2347                        let child_origin = if is_row {
2348                            LayoutPoint::new(
2349                                origin.x + padding[0] + cursor,
2350                                origin.y + padding[2] + cross_offset,
2351                            )
2352                        } else {
2353                            LayoutPoint::new(
2354                                origin.x + padding[0] + cross_offset,
2355                                origin.y + padding[2] + cursor,
2356                            )
2357                        };
2358
2359                        let mut child_constraints = entry.constraints;
2360                        if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
2361                            // Only stretch children that don't have an explicit cross-axis size.
2362                            let child_node = self.graph_state.node(entry.id);
2363                            let has_explicit_cross = child_node
2364                                .map(|n| match &n.op {
2365                                    LayoutOp::Box { width, height, .. } => {
2366                                        if is_row {
2367                                            height.is_some()
2368                                        } else {
2369                                            width.is_some()
2370                                        }
2371                                    }
2372                                    _ => false,
2373                                })
2374                                .unwrap_or(false);
2375                            if !has_explicit_cross {
2376                                if is_row {
2377                                    child_constraints.min_h = inner_cross;
2378                                    child_constraints.max_h = inner_cross;
2379                                } else {
2380                                    child_constraints.min_w = inner_cross;
2381                                    child_constraints.max_w = inner_cross;
2382                                }
2383                            }
2384                        }
2385
2386                        self.layout_node_constraints(
2387                            entry.id,
2388                            child_constraints,
2389                            child_origin,
2390                            out,
2391                            constraints_out,
2392                            measure_cache,
2393                            scroll_source,
2394                            record,
2395                            depth + 1,
2396                        )?;
2397                        cursor += child_main + gap + extra_gap;
2398                    }
2399
2400                    if record && !abs_children.is_empty() {
2401                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
2402                        for child_id in abs_children {
2403                            self.layout_node_constraints(
2404                                child_id,
2405                                abs_constraints,
2406                                origin,
2407                                out,
2408                                constraints_out,
2409                                measure_cache,
2410                                scroll_source,
2411                                record,
2412                                depth + 1,
2413                            )?;
2414                        }
2415                    }
2416                    content_size = size;
2417                    size
2418                }
2419            }
2420            LayoutOp::Grid {
2421                columns,
2422                rows,
2423                column_gap,
2424                row_gap,
2425                padding,
2426            } => {
2427                let gap_x = column_gap.unwrap_or(0.0);
2428                let gap_y = row_gap.unwrap_or(0.0);
2429                let inner = constraints.deflate(*padding);
2430                let bounded_w = inner.is_width_bounded();
2431                let bounded_h = inner.is_height_bounded();
2432                let available_w = if bounded_w { inner.max_w } else { 0.0 };
2433                let available_h = if bounded_h { inner.max_h } else { 0.0 };
2434
2435                let col_count = columns.len().max(1);
2436                let mut col_widths = vec![0.0f32; col_count];
2437                let mut fr_total = 0.0f32;
2438                let mut fixed_total = 0.0f32;
2439                for (i, track) in columns.iter().enumerate() {
2440                    match track {
2441                        GridTrack::Points(p) => {
2442                            col_widths[i] = *p;
2443                            fixed_total += *p;
2444                        }
2445                        GridTrack::Percent(p) => {
2446                            let w = if bounded_w {
2447                                available_w * (*p / 100.0)
2448                            } else {
2449                                0.0
2450                            };
2451                            col_widths[i] = w;
2452                            fixed_total += w;
2453                        }
2454                        GridTrack::Fr(f) => fr_total += *f,
2455                        _ => {}
2456                    }
2457                }
2458                if fr_total > 0.0 && bounded_w {
2459                    let remaining =
2460                        (available_w - fixed_total - gap_x * (col_count.saturating_sub(1) as f32))
2461                            .max(0.0);
2462                    for (i, track) in columns.iter().enumerate() {
2463                        if let GridTrack::Fr(f) = track {
2464                            col_widths[i] = remaining * (*f / fr_total);
2465                        }
2466                    }
2467                }
2468
2469                let child_count = flow_children.len();
2470                let row_count = if rows.is_empty() {
2471                    (child_count + col_count - 1) / col_count
2472                } else {
2473                    rows.len()
2474                };
2475                let mut row_heights = vec![0.0f32; row_count.max(1)];
2476
2477                if !rows.is_empty() {
2478                    let mut row_fr_total = 0.0f32;
2479                    let mut row_fixed_total = 0.0f32;
2480                    for (i, track) in rows.iter().enumerate() {
2481                        if i >= row_heights.len() {
2482                            break;
2483                        }
2484                        match track {
2485                            GridTrack::Points(p) => {
2486                                row_heights[i] = *p;
2487                                row_fixed_total += *p;
2488                            }
2489                            GridTrack::Percent(p) => {
2490                                let h = if bounded_h {
2491                                    available_h * (*p / 100.0)
2492                                } else {
2493                                    0.0
2494                                };
2495                                row_heights[i] = h;
2496                                row_fixed_total += h;
2497                            }
2498                            GridTrack::Fr(f) => row_fr_total += *f,
2499                            _ => {}
2500                        }
2501                    }
2502                    if row_fr_total > 0.0 && bounded_h {
2503                        let remaining = (available_h
2504                            - row_fixed_total
2505                            - gap_y * (row_heights.len().saturating_sub(1) as f32))
2506                            .max(0.0);
2507                        for (i, track) in rows.iter().enumerate() {
2508                            if let GridTrack::Fr(f) = track {
2509                                row_heights[i] = remaining * (*f / row_fr_total);
2510                            }
2511                        }
2512                    }
2513                }
2514
2515                let mut cell_assignments = Vec::new();
2516                let mut auto_row = 0;
2517                let mut auto_col = 0;
2518
2519                for child_id in &flow_children {
2520                    let child = self.graph_state.node(*child_id).unwrap();
2521                    let (row, col) = if let LayoutOp::GridItem {
2522                        row_start,
2523                        col_start,
2524                        ..
2525                    } = &child.op
2526                    {
2527                        let r = match row_start {
2528                            fission_ir::op::GridPlacement::Line(l) => {
2529                                (*l as usize).saturating_sub(1)
2530                            }
2531                            _ => auto_row,
2532                        };
2533                        let c = match col_start {
2534                            fission_ir::op::GridPlacement::Line(l) => {
2535                                (*l as usize).saturating_sub(1)
2536                            }
2537                            _ => auto_col,
2538                        };
2539                        (r, c)
2540                    } else {
2541                        let res = (auto_row, auto_col);
2542                        auto_col += 1;
2543                        if auto_col >= col_count {
2544                            auto_col = 0;
2545                            auto_row += 1;
2546                        }
2547                        res
2548                    };
2549                    cell_assignments.push((*child_id, row, col));
2550                }
2551
2552                for (child_id, row, col) in &cell_assignments {
2553                    if *row >= row_heights.len() || *col >= col_widths.len() {
2554                        continue;
2555                    }
2556                    let cell_w = col_widths[*col];
2557                    let cell_constraints = BoxConstraints {
2558                        min_w: cell_w,
2559                        max_w: cell_w,
2560                        min_h: 0.0,
2561                        max_h: if row_heights[*row] > 0.0 {
2562                            row_heights[*row]
2563                        } else {
2564                            f32::INFINITY
2565                        },
2566                    };
2567                    let child_size = self.layout_node_constraints(
2568                        *child_id,
2569                        cell_constraints,
2570                        LayoutPoint::ZERO,
2571                        out,
2572                        constraints_out,
2573                        measure_cache,
2574                        scroll_source,
2575                        false,
2576                        depth + 1,
2577                    )?;
2578                    if row_heights[*row] == 0.0 {
2579                        row_heights[*row] = child_size.height;
2580                    } else {
2581                        row_heights[*row] = row_heights[*row].max(child_size.height);
2582                    }
2583                }
2584
2585                let grid_w: f32 =
2586                    col_widths.iter().sum::<f32>() + gap_x * (col_count.saturating_sub(1) as f32);
2587                let grid_h: f32 = row_heights.iter().sum::<f32>()
2588                    + gap_y * (row_heights.len().saturating_sub(1) as f32);
2589                let size = constraints.constrain(LayoutSize::new(
2590                    grid_w + padding[0] + padding[1],
2591                    grid_h + padding[2] + padding[3],
2592                ));
2593
2594                if record {
2595                    let padding_origin_x = origin.x + padding[0];
2596                    let padding_origin_y = origin.y + padding[2];
2597                    for (child_id, row, col) in &cell_assignments {
2598                        if *row >= row_heights.len() || *col >= col_widths.len() {
2599                            continue;
2600                        }
2601                        let mut cell_x = padding_origin_x;
2602                        for i in 0..*col {
2603                            cell_x += col_widths[i] + gap_x;
2604                        }
2605                        let mut cell_y = padding_origin_y;
2606                        for i in 0..*row {
2607                            cell_y += row_heights[i] + gap_y;
2608                        }
2609                        let cell_w = col_widths[*col];
2610                        let cell_h = row_heights[*row];
2611                        let child_constraints = BoxConstraints {
2612                            min_w: cell_w,
2613                            max_w: cell_w,
2614                            min_h: cell_h,
2615                            max_h: cell_h,
2616                        };
2617                        self.layout_node_constraints(
2618                            *child_id,
2619                            child_constraints,
2620                            LayoutPoint::new(cell_x, cell_y),
2621                            out,
2622                            constraints_out,
2623                            measure_cache,
2624                            scroll_source,
2625                            record,
2626                            depth + 1,
2627                        )?;
2628                    }
2629                }
2630
2631                if record && !abs_children.is_empty() {
2632                    let abs_constraints = BoxConstraints::loose(size.width, size.height);
2633                    for child_id in abs_children {
2634                        self.layout_node_constraints(
2635                            child_id,
2636                            abs_constraints,
2637                            origin,
2638                            out,
2639                            constraints_out,
2640                            measure_cache,
2641                            scroll_source,
2642                            record,
2643                            depth + 1,
2644                        )?;
2645                    }
2646                }
2647                content_size = size;
2648                size
2649            }
2650            LayoutOp::GridItem { .. } => {
2651                let mut child_size = LayoutSize::ZERO;
2652                if let Some(child_id) = node.children_ids.first() {
2653                    child_size = self.layout_node_constraints(
2654                        *child_id,
2655                        constraints,
2656                        origin,
2657                        out,
2658                        constraints_out,
2659                        measure_cache,
2660                        scroll_source,
2661                        record,
2662                        depth + 1,
2663                    )?;
2664                }
2665                content_size = child_size;
2666                constraints.constrain(child_size)
2667            }
2668            LayoutOp::Scroll {
2669                direction,
2670                width,
2671                height,
2672                min_width,
2673                max_width,
2674                min_height,
2675                max_height,
2676                padding,
2677                ..
2678            } => {
2679                let mut local =
2680                    constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
2681                local = local.tighten(*width, *height);
2682                let is_horizontal = matches!(direction, FlexDirection::Row);
2683                let mut child_constraints = local.deflate(*padding);
2684                if is_horizontal {
2685                    child_constraints.min_w = 0.0;
2686                    child_constraints.max_w = f32::INFINITY;
2687                } else {
2688                    child_constraints.min_h = 0.0;
2689                    child_constraints.max_h = f32::INFINITY;
2690                }
2691                let mut child_size = LayoutSize::ZERO;
2692                if let Some(child_id) = flow_children.first() {
2693                    child_size = self.layout_node_constraints(
2694                        *child_id,
2695                        child_constraints,
2696                        LayoutPoint::ZERO,
2697                        out,
2698                        constraints_out,
2699                        measure_cache,
2700                        scroll_source,
2701                        false,
2702                        depth + 1,
2703                    )?;
2704                }
2705                let size = local.constrain(LayoutSize::new(
2706                    child_size.width + padding[0] + padding[1],
2707                    child_size.height + padding[2] + padding[3],
2708                ));
2709                if record {
2710                    if let Some(child_id) = flow_children.first() {
2711                        self.layout_node_constraints(
2712                            *child_id,
2713                            child_constraints,
2714                            LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
2715                            out,
2716                            constraints_out,
2717                            measure_cache,
2718                            scroll_source,
2719                            record,
2720                            depth + 1,
2721                        )?;
2722                    }
2723                    if !abs_children.is_empty() {
2724                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
2725                        for child_id in abs_children {
2726                            self.layout_node_constraints(
2727                                child_id,
2728                                abs_constraints,
2729                                origin,
2730                                out,
2731                                constraints_out,
2732                                measure_cache,
2733                                scroll_source,
2734                                record,
2735                                depth + 1,
2736                            )?;
2737                        }
2738                    }
2739                }
2740                content_size = child_size;
2741                size
2742            }
2743            LayoutOp::Align => {
2744                let child_constraints = BoxConstraints::loose(constraints.max_w, constraints.max_h);
2745                let mut child_size = LayoutSize::ZERO;
2746                if let Some(child_id) = flow_children.first() {
2747                    child_size = self.layout_node_constraints(
2748                        *child_id,
2749                        child_constraints,
2750                        LayoutPoint::ZERO,
2751                        out,
2752                        constraints_out,
2753                        measure_cache,
2754                        scroll_source,
2755                        false,
2756                        depth + 1,
2757                    )?;
2758                }
2759                let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
2760                    constraints.constrain(LayoutSize::new(
2761                        if constraints.is_width_bounded() {
2762                            constraints.max_w
2763                        } else {
2764                            child_size.width
2765                        },
2766                        if constraints.is_height_bounded() {
2767                            constraints.max_h
2768                        } else {
2769                            child_size.height
2770                        },
2771                    ))
2772                } else {
2773                    child_size
2774                };
2775                if let Some(child_id) = flow_children.first() {
2776                    let dx = ((size.width - child_size.width) / 2.0).max(0.0);
2777                    let dy = ((size.height - child_size.height) / 2.0).max(0.0);
2778                    self.layout_node_constraints(
2779                        *child_id,
2780                        child_constraints,
2781                        LayoutPoint::new(origin.x + dx, origin.y + dy),
2782                        out,
2783                        constraints_out,
2784                        measure_cache,
2785                        scroll_source,
2786                        record,
2787                        depth + 1,
2788                    )?;
2789                }
2790                if record && !abs_children.is_empty() {
2791                    let abs_constraints = BoxConstraints::loose(size.width, size.height);
2792                    for child_id in abs_children {
2793                        self.layout_node_constraints(
2794                            child_id,
2795                            abs_constraints,
2796                            origin,
2797                            out,
2798                            constraints_out,
2799                            measure_cache,
2800                            scroll_source,
2801                            record,
2802                            depth + 1,
2803                        )?;
2804                    }
2805                }
2806                content_size = child_size;
2807                size
2808            }
2809            LayoutOp::ZStack => {
2810                let mut max_child = LayoutSize::ZERO;
2811                for child_id in &flow_children {
2812                    let child_size = self.layout_node_constraints(
2813                        *child_id,
2814                        BoxConstraints::loose(constraints.max_w, constraints.max_h),
2815                        LayoutPoint::ZERO,
2816                        out,
2817                        constraints_out,
2818                        measure_cache,
2819                        scroll_source,
2820                        false,
2821                        depth + 1,
2822                    )?;
2823                    max_child.width = max_child.width.max(child_size.width);
2824                    max_child.height = max_child.height.max(child_size.height);
2825                }
2826                let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
2827                    constraints.constrain(LayoutSize::new(
2828                        if constraints.is_width_bounded() {
2829                            constraints.max_w
2830                        } else {
2831                            max_child.width
2832                        },
2833                        if constraints.is_height_bounded() {
2834                            constraints.max_h
2835                        } else {
2836                            max_child.height
2837                        },
2838                    ))
2839                } else {
2840                    max_child
2841                };
2842                for child_id in &flow_children {
2843                    let child_constraints = BoxConstraints::loose(size.width, size.height);
2844                    let child_origin = LayoutPoint::new(origin.x, origin.y);
2845                    self.layout_node_constraints(
2846                        *child_id,
2847                        child_constraints,
2848                        child_origin,
2849                        out,
2850                        constraints_out,
2851                        measure_cache,
2852                        scroll_source,
2853                        record,
2854                        depth + 1,
2855                    )?;
2856                }
2857                if record && !abs_children.is_empty() {
2858                    let abs_constraints = BoxConstraints::loose(size.width, size.height);
2859                    for child_id in abs_children {
2860                        self.layout_node_constraints(
2861                            child_id,
2862                            abs_constraints,
2863                            origin,
2864                            out,
2865                            constraints_out,
2866                            measure_cache,
2867                            scroll_source,
2868                            record,
2869                            depth + 1,
2870                        )?;
2871                    }
2872                }
2873                content_size = size;
2874                size
2875            }
2876            LayoutOp::Positioned {
2877                top,
2878                left,
2879                bottom,
2880                right,
2881                width,
2882                height,
2883            } => {
2884                let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
2885                let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
2886                let size = constraints.constrain(LayoutSize::new(target_w, target_h));
2887                let mut child_constraints = BoxConstraints::loose(size.width, size.height);
2888                if let (Some(l), Some(r)) = (left, right) {
2889                    let w = (size.width - l - r).max(0.0);
2890                    child_constraints = child_constraints.tighten(Some(w), None);
2891                }
2892                if let (Some(t), Some(b)) = (top, bottom) {
2893                    let h = (size.height - t - b).max(0.0);
2894                    child_constraints = child_constraints.tighten(None, Some(h));
2895                }
2896                child_constraints = child_constraints.tighten(*width, *height);
2897                if let Some(child_id) = node.children_ids.first() {
2898                    let child_size = self.layout_node_constraints(
2899                        *child_id,
2900                        child_constraints,
2901                        LayoutPoint::ZERO,
2902                        out,
2903                        constraints_out,
2904                        measure_cache,
2905                        scroll_source,
2906                        false,
2907                        depth + 1,
2908                    )?;
2909                    let x = left.unwrap_or_else(|| {
2910                        right
2911                            .map(|r| (size.width - r - child_size.width).max(0.0))
2912                            .unwrap_or(0.0)
2913                    });
2914                    let y = top.unwrap_or_else(|| {
2915                        bottom
2916                            .map(|b| (size.height - b - child_size.height).max(0.0))
2917                            .unwrap_or(0.0)
2918                    });
2919                    self.layout_node_constraints(
2920                        *child_id,
2921                        child_constraints,
2922                        LayoutPoint::new(origin.x + x, origin.y + y),
2923                        out,
2924                        constraints_out,
2925                        measure_cache,
2926                        scroll_source,
2927                        record,
2928                        depth + 1,
2929                    )?;
2930                }
2931                content_size = size;
2932                size
2933            }
2934            LayoutOp::Embed { width, height, .. } => {
2935                let local = constraints.tighten(*width, *height);
2936                let w = if local.is_width_bounded() {
2937                    local.max_w
2938                } else {
2939                    local.min_w
2940                };
2941                let h = if local.is_height_bounded() {
2942                    local.max_h
2943                } else {
2944                    local.min_h
2945                };
2946                let size = local.constrain(LayoutSize::new(w, h));
2947                content_size = size;
2948                size
2949            }
2950            LayoutOp::AbsoluteFill => {
2951                let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
2952                let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
2953                let size = constraints.constrain(LayoutSize::new(target_w, target_h));
2954                for child_id in self.graph_state.children_of(node_id) {
2955                    self.layout_node_constraints(
2956                        *child_id,
2957                        BoxConstraints::tight(size),
2958                        origin,
2959                        out,
2960                        constraints_out,
2961                        measure_cache,
2962                        scroll_source,
2963                        record,
2964                        depth + 1,
2965                    )?;
2966                }
2967                content_size = size;
2968                size
2969            }
2970            LayoutOp::Transform { .. } | LayoutOp::Clip { .. } => {
2971                let mut child_size = LayoutSize::ZERO;
2972                if let Some(child_id) = node.children_ids.first() {
2973                    child_size = self.layout_node_constraints(
2974                        *child_id,
2975                        constraints,
2976                        origin,
2977                        out,
2978                        constraints_out,
2979                        measure_cache,
2980                        scroll_source,
2981                        record,
2982                        depth + 1,
2983                    )?;
2984                }
2985                content_size = child_size;
2986                constraints.constrain(child_size)
2987            }
2988            LayoutOp::Flyout { anchor, content: _ } => {
2989                let loose = BoxConstraints::loose(
2990                    if constraints.is_width_bounded() {
2991                        constraints.max_w
2992                    } else {
2993                        f32::INFINITY
2994                    },
2995                    if constraints.is_height_bounded() {
2996                        constraints.max_h
2997                    } else {
2998                        f32::INFINITY
2999                    },
3000                );
3001                let mut child_size = LayoutSize::ZERO;
3002                for child_id in self.graph_state.children_of(node_id) {
3003                    child_size = self.layout_node_constraints(
3004                        *child_id,
3005                        loose,
3006                        origin,
3007                        out,
3008                        constraints_out,
3009                        measure_cache,
3010                        scroll_source,
3011                        false,
3012                        depth + 1,
3013                    )?;
3014                }
3015                if record {
3016                    let anchor_rect = out.get(anchor).map(|g| g.rect);
3017                    let place_x = anchor_rect.map(|r| r.x()).unwrap_or(origin.x);
3018                    let place_y = anchor_rect.map(|r| r.y() + r.height()).unwrap_or(origin.y);
3019                    for child_id in self.graph_state.children_of(node_id) {
3020                        self.layout_node_constraints(
3021                            *child_id,
3022                            loose,
3023                            LayoutPoint::new(place_x, place_y),
3024                            out,
3025                            constraints_out,
3026                            measure_cache,
3027                            scroll_source,
3028                            record,
3029                            depth + 1,
3030                        )?;
3031                    }
3032                }
3033                content_size = child_size;
3034                child_size
3035            }
3036        };
3037
3038        if let Some(runs) = &node.rich_text {
3039            if let Some(measurer) = &self.measurer {
3040                let node_max_w = match &node.op {
3041                    LayoutOp::Box { max_width, .. } => *max_width,
3042                    _ => None,
3043                };
3044                let avail_w = {
3045                    let from_constraints = if constraints.is_width_bounded() {
3046                        Some(constraints.max_w)
3047                    } else {
3048                        None
3049                    };
3050                    match (from_constraints, node_max_w) {
3051                        (Some(c), Some(m)) => Some(c.min(m)),
3052                        (Some(c), None) => Some(c),
3053                        (None, Some(m)) => Some(m),
3054                        (None, None) => None,
3055                    }
3056                };
3057                let rich_layout = measurer.layout_rich_text(runs, avail_w);
3058                let text_content = LayoutSize::new(rich_layout.width, rich_layout.height);
3059                let measured = constraints.constrain(text_content);
3060                if rich_text_inline_children
3061                    && rich_layout.inline_boxes.len() == flow_children.len()
3062                {
3063                    let result =
3064                        self.record_geometry(node_id, origin, measured, text_content, out, record);
3065                    if record {
3066                        let mut inline_boxes = rich_layout.inline_boxes;
3067                        inline_boxes.sort_by_key(|inline_box| inline_box.id);
3068                        for (child_id, inline_box) in flow_children.iter().zip(inline_boxes.iter())
3069                        {
3070                            self.layout_node_constraints(
3071                                *child_id,
3072                                BoxConstraints::tight(LayoutSize::new(
3073                                    inline_box.width,
3074                                    inline_box.height,
3075                                )),
3076                                LayoutPoint::new(origin.x + inline_box.x, origin.y + inline_box.y),
3077                                out,
3078                                constraints_out,
3079                                measure_cache,
3080                                scroll_source,
3081                                record,
3082                                depth + 1,
3083                            )?;
3084                        }
3085                    }
3086                    if !record {
3087                        measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3088                    }
3089                    return Ok(result);
3090                }
3091                if node.children_ids.is_empty() {
3092                    let result =
3093                        self.record_geometry(node_id, origin, measured, text_content, out, record);
3094                    if !record {
3095                        measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3096                    }
3097                    return Ok(result);
3098                }
3099                content_size.width = content_size.width.max(text_content.width);
3100                content_size.height = content_size.height.max(text_content.height);
3101            }
3102        }
3103
3104        let result = self.record_geometry(node_id, origin, size, content_size, out, record);
3105        if !record {
3106            measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3107        }
3108        Ok(result)
3109    }
3110
3111    fn record_geometry(
3112        &self,
3113        node_id: WidgetId,
3114        origin: LayoutPoint,
3115        size: LayoutSize,
3116        content_size: LayoutSize,
3117        out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
3118        record: bool,
3119    ) -> LayoutSize {
3120        let mut rect_origin = origin;
3121        let mut rect_size = size;
3122        let mut rect_content = content_size;
3123        let mut had_non_finite = false;
3124
3125        if !rect_origin.x.is_finite() {
3126            rect_origin.x = 0.0;
3127            had_non_finite = true;
3128        }
3129        if !rect_origin.y.is_finite() {
3130            rect_origin.y = 0.0;
3131            had_non_finite = true;
3132        }
3133        if !rect_size.width.is_finite() {
3134            rect_size.width = 0.0;
3135            had_non_finite = true;
3136        }
3137        if !rect_size.height.is_finite() {
3138            rect_size.height = 0.0;
3139            had_non_finite = true;
3140        }
3141        if !rect_content.width.is_finite() {
3142            rect_content.width = 0.0;
3143            had_non_finite = true;
3144        }
3145        if !rect_content.height.is_finite() {
3146            rect_content.height = 0.0;
3147            had_non_finite = true;
3148        }
3149
3150        if had_non_finite {
3151            diag::emit(
3152                diag::DiagCategory::Invariants,
3153                diag::DiagLevel::Error,
3154                diag::DiagEventKind::InvariantViolation {
3155                    kind: "non_finite_layout".into(),
3156                    node: Some(node_id.as_u128()),
3157                    details: format!(
3158                        "origin=({:.2},{:.2}) size=({:.2},{:.2}) content=({:.2},{:.2})",
3159                        origin.x,
3160                        origin.y,
3161                        size.width,
3162                        size.height,
3163                        content_size.width,
3164                        content_size.height
3165                    ),
3166                    dump_ref: None,
3167                },
3168            );
3169        }
3170
3171        if record {
3172            let rect = LayoutRect::new(
3173                rect_origin.x,
3174                rect_origin.y,
3175                rect_size.width,
3176                rect_size.height,
3177            );
3178            out.insert(
3179                node_id,
3180                LayoutNodeGeometry {
3181                    rect,
3182                    content_size: rect_content,
3183                },
3184            );
3185        }
3186        rect_size
3187    }
3188}