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::{
598        LayoutEngine, LayoutGraphState, LayoutInputNode, TextMeasurer,
599        DEFAULT_RICH_TEXT_HIT_TEST_FONT_SIZE,
600    };
601    use fission_ir::op::{Color, FontStyle, TextRun, TextStyle};
602    use fission_ir::{LayoutOp, WidgetId};
603    use std::sync::atomic::{AtomicU32, Ordering};
604
605    fn box_node(
606        id: WidgetId,
607        parent_id: Option<WidgetId>,
608        children_ids: Vec<WidgetId>,
609    ) -> LayoutInputNode {
610        LayoutInputNode {
611            id,
612            parent_id,
613            op: LayoutOp::Box {
614                width: Some(40.0),
615                height: Some(20.0),
616                min_width: None,
617                max_width: None,
618                min_height: None,
619                max_height: None,
620                padding: [0.0; 4],
621                flex_grow: 0.0,
622                flex_shrink: 0.0,
623                aspect_ratio: None,
624            },
625            children_ids,
626            debug_name: format!("node-{}", id.as_u128()),
627            width: Some(40.0),
628            height: Some(20.0),
629            flex_grow: 0.0,
630            flex_shrink: 0.0,
631            rich_text: None,
632        }
633    }
634
635    struct RecordingMeasurer {
636        last_font_size_bits: AtomicU32,
637    }
638
639    impl RecordingMeasurer {
640        fn new() -> Self {
641            Self {
642                last_font_size_bits: AtomicU32::new(f32::NAN.to_bits()),
643            }
644        }
645
646        fn last_font_size(&self) -> f32 {
647            f32::from_bits(self.last_font_size_bits.load(Ordering::SeqCst))
648        }
649    }
650
651    impl TextMeasurer for RecordingMeasurer {
652        fn measure(
653            &self,
654            _text: &str,
655            _font_size: f32,
656            _available_width: Option<f32>,
657        ) -> (f32, f32) {
658            (0.0, 0.0)
659        }
660
661        fn hit_test(
662            &self,
663            _text: &str,
664            font_size: f32,
665            _available_width: Option<f32>,
666            _x: f32,
667            _y: f32,
668        ) -> usize {
669            self.last_font_size_bits
670                .store(font_size.to_bits(), Ordering::SeqCst);
671            0
672        }
673    }
674
675    #[test]
676    fn matches_input_nodes_rejects_reordered_flattened_inputs() {
677        let root = WidgetId::from_u128(1);
678        let first = WidgetId::from_u128(2);
679        let second = WidgetId::from_u128(3);
680        let canonical = vec![
681            box_node(root, None, vec![first, second]),
682            box_node(first, Some(root), vec![]),
683            box_node(second, Some(root), vec![]),
684        ];
685        let reordered = vec![
686            box_node(root, None, vec![first, second]),
687            box_node(second, Some(root), vec![]),
688            box_node(first, Some(root), vec![]),
689        ];
690
691        let state = LayoutGraphState::from_input_nodes(&canonical, 1);
692        assert!(!state.matches_input_nodes(&reordered));
693    }
694
695    #[test]
696    fn update_refreshes_node_order_for_reordered_flattened_inputs() {
697        let root = WidgetId::from_u128(10);
698        let first = WidgetId::from_u128(11);
699        let second = WidgetId::from_u128(12);
700        let canonical = vec![
701            box_node(root, None, vec![first, second]),
702            box_node(first, Some(root), vec![]),
703            box_node(second, Some(root), vec![]),
704        ];
705        let reordered = vec![
706            box_node(root, None, vec![first, second]),
707            box_node(second, Some(root), vec![]),
708            box_node(first, Some(root), vec![]),
709        ];
710
711        let mut engine = LayoutEngine::new();
712        engine.update(&canonical);
713        engine.update(&reordered);
714
715        let ordered = engine
716            .graph_state
717            .ordered_nodes()
718            .map(|node| node.id)
719            .collect::<Vec<_>>();
720        assert_eq!(ordered, vec![root, second, first]);
721    }
722
723    #[test]
724    fn rich_text_hit_test_uses_body_font_size_when_runs_are_empty() {
725        let measurer = RecordingMeasurer::new();
726
727        measurer.hit_test_rich(&[], None, 4.0, 2.0);
728
729        assert_eq!(
730            measurer.last_font_size(),
731            DEFAULT_RICH_TEXT_HIT_TEST_FONT_SIZE
732        );
733    }
734
735    #[test]
736    fn rich_text_hit_test_uses_first_run_font_size_when_present() {
737        let measurer = RecordingMeasurer::new();
738        let runs = vec![TextRun {
739            text: "Hello".to_string(),
740            style: TextStyle {
741                font_size: 18.0,
742                color: Color::BLACK,
743                underline: false,
744                font_family: None,
745                locale: None,
746                font_weight: 400,
747                font_style: FontStyle::Normal,
748                line_height: None,
749                letter_spacing: 0.0,
750                background_color: None,
751            },
752        }];
753
754        measurer.hit_test_rich(&runs, None, 4.0, 2.0);
755
756        assert_eq!(measurer.last_font_size(), 18.0);
757    }
758}
759
760fn layout_input_fingerprint(node: &LayoutInputNode) -> u64 {
761    let mut hasher = DefaultHasher::new();
762    format!("{node:?}").hash(&mut hasher);
763    hasher.finish()
764}
765
766/// An axis-aligned rectangle: an origin point plus a size.
767///
768/// `LayoutRect` is the final output for every node after layout: it says exactly
769/// where the node sits on screen and how large it is.
770///
771/// # Example
772///
773/// ```rust
774/// use fission_layout::{LayoutRect, LayoutPoint};
775///
776/// let rect = LayoutRect::new(10.0, 20.0, 300.0, 200.0);
777/// assert_eq!(rect.right(), 310.0);
778/// assert!(rect.contains(LayoutPoint::new(15.0, 25.0)));
779/// ```
780#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
781pub struct LayoutRect {
782    /// The top-left corner of the rectangle.
783    pub origin: LayoutPoint,
784    /// The width and height of the rectangle.
785    pub size: LayoutSize,
786}
787
788impl LayoutRect {
789    /// Creates a rectangle from x, y, width, and height.
790    pub fn new(x: LayoutUnit, y: LayoutUnit, width: LayoutUnit, height: LayoutUnit) -> Self {
791        Self {
792            origin: LayoutPoint { x, y },
793            size: LayoutSize { width, height },
794        }
795    }
796
797    /// The x coordinate of the left edge.
798    pub fn x(&self) -> LayoutUnit {
799        self.origin.x
800    }
801    /// The y coordinate of the top edge.
802    pub fn y(&self) -> LayoutUnit {
803        self.origin.y
804    }
805    /// The width of the rectangle.
806    pub fn width(&self) -> LayoutUnit {
807        self.size.width
808    }
809    /// The height of the rectangle.
810    pub fn height(&self) -> LayoutUnit {
811        self.size.height
812    }
813
814    /// The x coordinate of the right edge (`x + width`).
815    pub fn right(&self) -> LayoutUnit {
816        self.origin.x + self.size.width
817    }
818    /// The y coordinate of the bottom edge (`y + height`).
819    pub fn bottom(&self) -> LayoutUnit {
820        self.origin.y + self.size.height
821    }
822
823    /// Returns `true` if the point `p` lies within this rectangle (inclusive on
824    /// the left/top edges, exclusive on the right/bottom edges).
825    pub fn contains(&self, p: LayoutPoint) -> bool {
826        p.x >= self.x() && p.x < self.right() && p.y >= self.y() && p.y < self.bottom()
827    }
828}
829
830/// The computed geometry of a single layout node.
831///
832/// After layout, every node has a bounding rectangle (its position and size on
833/// screen) and a content size (how large its content actually is, which may exceed
834/// the rect for scroll containers).
835#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
836pub struct LayoutNodeGeometry {
837    /// The bounding rectangle of this node in absolute (screen) coordinates.
838    pub rect: LayoutRect,
839    /// The natural size of the node's content before clipping. For scroll containers,
840    /// this may be larger than `rect.size`, indicating scrollable overflow.
841    pub content_size: LayoutSize,
842}
843
844/// The complete output of a layout pass.
845///
846/// `LayoutSnapshot` maps every node to its computed geometry and records the
847/// viewport size that was used. It is the primary interface between the layout
848/// engine and downstream consumers (the renderer, hit testing, accessibility).
849///
850/// # Example
851///
852/// ```rust,no_run
853/// use fission_layout::{LayoutSnapshot, LayoutSize};
854/// use fission_ir::WidgetId;
855///
856/// let snapshot = LayoutSnapshot::new(LayoutSize::new(800.0, 600.0));
857/// assert_eq!(snapshot.viewport_size.width, 800.0);
858/// ```
859#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
860pub struct LayoutSnapshot {
861    /// Computed geometry for every node, keyed by [`WidgetId`].
862    pub nodes: HashMap<WidgetId, LayoutNodeGeometry>,
863    /// The constraints that were passed to each node during layout. Useful for
864    /// debugging. Skipped during serialization.
865    #[serde(skip)]
866    pub constraints: HashMap<WidgetId, BoxConstraints>,
867    /// The viewport size used for this layout pass.
868    pub viewport_size: LayoutSize,
869}
870
871impl LayoutSnapshot {
872    /// Creates an empty snapshot for the given viewport size.
873    pub fn new(viewport_size: LayoutSize) -> Self {
874        Self {
875            nodes: HashMap::new(),
876            constraints: HashMap::new(),
877            viewport_size,
878        }
879    }
880
881    /// Returns the full geometry (rect + content size) for a node, or `None` if
882    /// the node was not part of this layout pass.
883    pub fn get_node_geometry(&self, node_id: WidgetId) -> Option<&LayoutNodeGeometry> {
884        self.nodes.get(&node_id)
885    }
886
887    /// Returns just the bounding rectangle for a node, or `None` if not found.
888    pub fn get_node_rect(&self, node_id: WidgetId) -> Option<LayoutRect> {
889        self.nodes.get(&node_id).map(|g| g.rect)
890    }
891
892    /// Returns the constraints that were passed to a node during layout, or `None`
893    /// if not found. Useful for debugging layout issues.
894    pub fn get_node_constraints(&self, node_id: WidgetId) -> Option<BoxConstraints> {
895        self.constraints.get(&node_id).copied()
896    }
897}
898
899/// A flattened representation of a layout node, ready for the layout engine.
900///
901/// The widget compiler produces a list of `LayoutInputNode`s from the IR. Each node
902/// carries its layout operation, parent/child relationships, flex participation
903/// parameters, and optional rich text content for text measurement.
904///
905/// The layout engine operates on `&[LayoutInputNode]` rather than traversing the
906/// IR directly, which keeps the engine decoupled from the IR's internal structure.
907#[derive(Debug, Clone)]
908pub struct LayoutInputNode {
909    /// The unique identity of this node.
910    pub id: WidgetId,
911    /// The parent node's ID, or `None` for the root.
912    pub parent_id: Option<WidgetId>,
913    /// The layout operation this node performs.
914    pub op: LayoutOp,
915    /// Ordered list of child node IDs.
916    pub children_ids: Vec<WidgetId>,
917    /// A human-readable name for debugging and diagnostics.
918    pub debug_name: String,
919    /// Explicit width override, or `None` to derive from constraints.
920    pub width: Option<LayoutUnit>,
921    /// Explicit height override, or `None` to derive from constraints.
922    pub height: Option<LayoutUnit>,
923    /// How much extra main-axis space this node claims from its flex parent.
924    pub flex_grow: LayoutUnit,
925    /// How much this node shrinks when its flex parent overflows.
926    pub flex_shrink: LayoutUnit,
927    /// Optional rich text content. When present, the layout engine uses the
928    /// [`TextMeasurer`] to determine the node's intrinsic size from the text.
929    pub rich_text: Option<Vec<TextRun>>,
930}
931
932/// Per-line metrics returned by text measurement.
933///
934/// When the layout engine or hit-testing code needs to know about individual lines
935/// of text (e.g., for cursor positioning in a multi-line text field), it calls
936/// [`TextMeasurer::get_line_metrics`] and receives a `Vec<LineMetric>`.
937pub struct LineMetric {
938    /// Byte index where this line starts in the source string.
939    pub start_index: usize,
940    /// Byte index where this line ends in the source string (exclusive).
941    pub end_index: usize,
942    /// Distance from the top of the line to its alphabetic baseline, in logical pixels.
943    pub baseline: f32,
944    /// Total height of the line (ascent + descent + leading), in logical pixels.
945    pub height: f32,
946    /// Measured width of the line's content, in logical pixels.
947    pub width: f32,
948}
949
950#[derive(Debug, Clone, Copy, PartialEq)]
951pub struct RichTextInlineBox {
952    pub id: u64,
953    pub x: f32,
954    pub y: f32,
955    pub width: f32,
956    pub height: f32,
957}
958
959#[derive(Debug, Clone, PartialEq)]
960pub struct RichTextLayoutInfo {
961    pub width: f32,
962    pub height: f32,
963    pub inline_boxes: Vec<RichTextInlineBox>,
964}
965
966/// A platform-provided text measurement backend.
967///
968/// The layout engine does not shape or measure text itself. Instead, platform
969/// backends implement `TextMeasurer` to wrap their native text engine (CoreText
970/// on macOS, DirectWrite on Windows, HarfBuzz + FreeType on Linux, etc.).
971///
972/// All methods have default implementations that return zero-sized results, so
973/// you only need to override the methods your backend supports.
974///
975/// # Required
976///
977/// * [`measure`](TextMeasurer::measure) -- must be implemented to get correct text layout.
978///
979/// # Optional
980///
981/// * [`hit_test`](TextMeasurer::hit_test) -- needed for click-to-cursor in text fields.
982/// * [`get_line_metrics`](TextMeasurer::get_line_metrics) -- needed for multi-line cursor navigation.
983/// * [`get_caret_position`](TextMeasurer::get_caret_position) -- needed for drawing the text cursor.
984/// * [`measure_rich_text`](TextMeasurer::measure_rich_text) -- needed for mixed-style text.
985const DEFAULT_RICH_TEXT_HIT_TEST_FONT_SIZE: f32 = 14.0;
986
987pub trait TextMeasurer: Send + Sync {
988    /// Measures single-style text and returns `(width, height)` in logical pixels.
989    ///
990    /// If `available_width` is `Some`, the text should be wrapped at that width.
991    /// If `None`, the text is measured as a single unwrapped line.
992    fn measure(&self, text: &str, font_size: f32, available_width: Option<f32>) -> (f32, f32);
993
994    /// Returns the byte index of the character closest to the point `(x, y)`,
995    /// relative to the text's origin. Used for click-to-cursor in text fields.
996    ///
997    /// The default implementation returns `0`.
998    fn hit_test(
999        &self,
1000        _text: &str,
1001        _font_size: f32,
1002        _available_width: Option<f32>,
1003        _x: f32,
1004        _y: f32,
1005    ) -> usize {
1006        0
1007    }
1008
1009    /// Returns per-line metrics for the given text. Used for multi-line text fields
1010    /// and line-based cursor navigation.
1011    ///
1012    /// The default implementation returns an empty vec.
1013    fn get_line_metrics(
1014        &self,
1015        _text: &str,
1016        _font_size: f32,
1017        _available_width: Option<f32>,
1018    ) -> Vec<LineMetric> {
1019        vec![]
1020    }
1021
1022    /// Returns the `(x, y)` position of the text cursor at `caret_index` (byte offset),
1023    /// relative to the text's origin.
1024    ///
1025    /// The default implementation returns `(0.0, 0.0)`.
1026    fn get_caret_position(
1027        &self,
1028        _text: &str,
1029        _font_size: f32,
1030        _available_width: Option<f32>,
1031        _caret_index: usize,
1032    ) -> (f32, f32) {
1033        (0.0, 0.0)
1034    }
1035
1036    /// Measures multi-style (rich) text and returns `(width, height)` in logical pixels.
1037    ///
1038    /// The default implementation returns `(0.0, 0.0)`.
1039    fn measure_rich_text(&self, _runs: &[TextRun], _available_width: Option<f32>) -> (f32, f32) {
1040        (0.0, 0.0)
1041    }
1042
1043    /// Measures rich text and returns positioned inline-widget boxes, if any.
1044    ///
1045    /// Backends that understand inline rich-text widget markers should override
1046    /// this so layout can place the child widgets at the same coordinates used
1047    /// by text shaping.
1048    fn layout_rich_text(
1049        &self,
1050        runs: &[TextRun],
1051        available_width: Option<f32>,
1052    ) -> RichTextLayoutInfo {
1053        let (width, height) = if runs.len() == 1 {
1054            let run = &runs[0];
1055            self.measure(&run.text, run.style.font_size, available_width)
1056        } else {
1057            self.measure_rich_text(runs, available_width)
1058        };
1059        RichTextLayoutInfo {
1060            width,
1061            height,
1062            inline_boxes: Vec::new(),
1063        }
1064    }
1065
1066    /// Hit-test rich text (styled runs) at the given (x, y) position.
1067    /// Returns the byte offset into the concatenated text of all runs.
1068    /// Default falls back to plain hit_test using the first run's font size.
1069    fn hit_test_rich(
1070        &self,
1071        runs: &[TextRun],
1072        _available_width: Option<f32>,
1073        x: f32,
1074        y: f32,
1075    ) -> usize {
1076        // Preserve the normal body-text fallback when no run is available, so
1077        // fallback hit testing never asks a backend to shape zero-sized text.
1078        let text: String = runs.iter().map(|r| r.text.as_str()).collect();
1079        let font_size = runs
1080            .first()
1081            .map(|r| r.style.font_size)
1082            .unwrap_or(DEFAULT_RICH_TEXT_HIT_TEST_FONT_SIZE);
1083        self.hit_test(&text, font_size, None, x, y)
1084    }
1085
1086    /// Resolves the rich-text annotation at the given point, if any.
1087    ///
1088    /// This is used for interactive rich-text spans that need hit testing
1089    /// against shaped rich text rather than box nodes.
1090    fn resolve_rich_text_annotation_at_point(
1091        &self,
1092        _runs: &[TextRun],
1093        _available_width: Option<f32>,
1094        _x: f32,
1095        _y: f32,
1096        _paragraph_style: TextParagraphStyle,
1097        _annotations: &[RichTextAnnotation],
1098    ) -> Option<RichTextAnnotation> {
1099        None
1100    }
1101}
1102
1103/// The constraint-based layout solver.
1104///
1105/// `LayoutEngine` walks the node tree top-down, passing [`BoxConstraints`] from
1106/// parent to child, and bottom-up, returning [`LayoutSize`] from child to parent.
1107/// The final result is a [`LayoutSnapshot`] that maps every node to its absolute
1108/// screen-space rectangle.
1109///
1110/// The engine optionally holds a [`TextMeasurer`] for sizing text nodes. Without
1111/// one, text nodes are treated as zero-sized.
1112///
1113/// # Example
1114///
1115/// ```rust,no_run
1116/// use fission_layout::*;
1117/// use fission_ir::WidgetId;
1118/// use std::sync::Arc;
1119///
1120/// let mut engine = LayoutEngine::new();
1121/// // engine = engine.with_measurer(my_text_measurer);
1122///
1123/// // let snapshot = engine.compute_layout(&nodes, root_id, viewport, &|_| 0.0).unwrap();
1124/// ```
1125pub struct LayoutEngine {
1126    measurer: Option<Arc<dyn TextMeasurer>>,
1127    graph_state: LayoutGraphState,
1128    next_graph_version: u64,
1129    incremental_reuse: Option<IncrementalLayoutReuseState>,
1130}
1131
1132impl LayoutEngine {
1133    const MAX_LAYOUT_RECURSION_DEPTH: usize = 100;
1134
1135    /// Creates a new layout engine with no text measurer.
1136    ///
1137    /// Text nodes will be treated as zero-sized until a measurer is provided
1138    /// via [`with_measurer`](LayoutEngine::with_measurer).
1139    pub fn new() -> Self {
1140        Self {
1141            measurer: None,
1142            graph_state: LayoutGraphState::default(),
1143            next_graph_version: 1,
1144            incremental_reuse: None,
1145        }
1146    }
1147
1148    /// Returns a new engine with the given text measurer attached.
1149    ///
1150    /// This is a builder-style method that consumes and returns `self`.
1151    pub fn with_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
1152        self.measurer = Some(measurer);
1153        self
1154    }
1155
1156    fn allocate_graph_version(&mut self) -> u64 {
1157        let version = self.next_graph_version;
1158        self.next_graph_version = self.next_graph_version.saturating_add(1);
1159        version
1160    }
1161
1162    fn refresh_graph_state(&mut self, input_nodes: &[LayoutInputNode]) {
1163        let version = self.allocate_graph_version();
1164        self.graph_state = LayoutGraphState::from_input_nodes(input_nodes, version);
1165    }
1166
1167    fn ensure_graph_state(&mut self, input_nodes: &[LayoutInputNode]) {
1168        if self.graph_state.is_empty() || !self.graph_state.matches_input_nodes(input_nodes) {
1169            self.refresh_graph_state(input_nodes);
1170        }
1171    }
1172
1173    fn validate_graph_state(&self, root: WidgetId) -> Result<()> {
1174        if let Some(err) = self.graph_state.validation.first_error() {
1175            return Err(err);
1176        }
1177        if !self.graph_state.nodes.contains_key(&root) {
1178            anyhow::bail!("[verify] missing node {:?}", root);
1179        }
1180        if !self.graph_state.roots.contains(&root)
1181            && self
1182                .graph_state
1183                .parents
1184                .get(&root)
1185                .copied()
1186                .flatten()
1187                .is_some()
1188        {
1189            anyhow::bail!("[verify] root {:?} is not a graph root", root);
1190        }
1191        if let Some(last_layout_version) = self.graph_state.last_layout_version {
1192            if last_layout_version > self.graph_state.graph_version {
1193                anyhow::bail!(
1194                    "[verify] cached layout version {} exceeds graph version {}",
1195                    last_layout_version,
1196                    self.graph_state.graph_version
1197                );
1198            }
1199        }
1200        Ok(())
1201    }
1202
1203    /// Refreshes the cached graph state after upstream layout edits.
1204    ///
1205    /// Unchanged nodes keep their cached graph entries while edited topology and
1206    /// fingerprints are synchronized to the latest flattened node list.
1207    pub fn update(&mut self, input_nodes: &[LayoutInputNode]) {
1208        if self.graph_state.is_empty() {
1209            self.refresh_graph_state(input_nodes);
1210            return;
1211        }
1212
1213        if self.graph_state.matches_input_nodes(input_nodes) {
1214            return;
1215        }
1216
1217        let version = self.allocate_graph_version();
1218        self.graph_state.graph_version = version;
1219        self.graph_state.update_nodes(input_nodes);
1220    }
1221
1222    /// Rebuilds internal data structures from the full node list.
1223    pub fn rebuild(&mut self, input_nodes: &[LayoutInputNode]) -> Result<()> {
1224        self.refresh_graph_state(input_nodes);
1225        if let Some(err) = self.graph_state.validation.first_error() {
1226            return Err(err);
1227        }
1228        Ok(())
1229    }
1230
1231    /// Verifies parent-child consistency and checks for cycles in the node graph.
1232    ///
1233    /// Call this during development/testing to catch malformed IR before it causes
1234    /// layout panics. Returns `Err` with a description of the first problem found.
1235    pub fn verify_post_update(
1236        &self,
1237        input_nodes: &[LayoutInputNode],
1238        root: WidgetId,
1239    ) -> Result<()> {
1240        if self.graph_state.matches_input_nodes(input_nodes) {
1241            return self.validate_graph_state(root);
1242        }
1243
1244        let node_map: HashMap<WidgetId, &LayoutInputNode> =
1245            input_nodes.iter().map(|n| (n.id, n)).collect();
1246        // Parent/child consistency
1247        for n in input_nodes {
1248            for child in &n.children_ids {
1249                let child_node = node_map
1250                    .get(child)
1251                    .ok_or_else(|| anyhow::anyhow!("[verify] child {:?} not found", child))?;
1252                if child_node.parent_id != Some(n.id) {
1253                    anyhow::bail!("[verify] parent/child mismatch parent={:?} child={:?} child.parent_id={:?}", n.id, child, child_node.parent_id);
1254                }
1255            }
1256        }
1257        // Cycle via DFS
1258        fn dfs(
1259            id: WidgetId,
1260            map: &HashMap<WidgetId, &LayoutInputNode>,
1261            visited: &mut HashSet<WidgetId>,
1262            stack: &mut HashSet<WidgetId>,
1263        ) -> Result<()> {
1264            if !visited.insert(id) {
1265                return Ok(());
1266            }
1267            stack.insert(id);
1268            let node = map
1269                .get(&id)
1270                .ok_or_else(|| anyhow::anyhow!("[verify] missing node {:?}", id))?;
1271            for child in &node.children_ids {
1272                if stack.contains(child) {
1273                    anyhow::bail!("[verify] cycle detected at {:?} -> {:?}", id, child);
1274                }
1275                dfs(*child, map, visited, stack)?;
1276            }
1277            stack.remove(&id);
1278            Ok(())
1279        }
1280        let mut visited = HashSet::new();
1281        let mut stack = HashSet::new();
1282        dfs(root, &node_map, &mut visited, &mut stack)?;
1283        Ok(())
1284    }
1285
1286    /// Computes layout for the entire node tree and returns a snapshot.
1287    ///
1288    /// This is the main entry point. It runs the constraint-based layout algorithm
1289    /// starting from `root_node_id`, using `viewport_size` as the root constraints,
1290    /// and querying `scroll_source` for scroll offsets. After layout, it emits scroll
1291    /// diagnostics for debugging.
1292    ///
1293    /// # Arguments
1294    ///
1295    /// * `input_nodes` -- The flat list of all layout nodes.
1296    /// * `root_node_id` -- Which node is the root of the tree.
1297    /// * `viewport_size` -- The size of the window/screen.
1298    /// * `scroll_source` -- Provides scroll offsets for scroll containers.
1299    ///
1300    /// # Errors
1301    ///
1302    /// Returns `Err` if a cycle is detected or a required node is missing.
1303    pub fn compute_layout(
1304        &mut self,
1305        input_nodes: &[LayoutInputNode],
1306        root_node_id: WidgetId,
1307        viewport_size: LayoutSize,
1308        scroll_source: &impl ScrollDataSource,
1309    ) -> Result<LayoutSnapshot> {
1310        self.ensure_graph_state(input_nodes);
1311        self.validate_graph_state(root_node_id)?;
1312        let snapshot = self.compute_layout_constraints(
1313            input_nodes,
1314            root_node_id,
1315            viewport_size,
1316            scroll_source,
1317        )?;
1318        self.emit_scroll_diagnostics(&snapshot);
1319        Ok(snapshot)
1320    }
1321
1322    /// InternalLower-level layout that skips scroll diagnostics.
1323    ///
1324    /// Same as [`compute_layout`](LayoutEngine::compute_layout) but does not emit
1325    /// diagnostic events. Useful when you need the snapshot but not the debug output.
1326    pub fn compute_layout_constraints(
1327        &mut self,
1328        input_nodes: &[LayoutInputNode],
1329        root_node_id: WidgetId,
1330        viewport_size: LayoutSize,
1331        scroll_source: &impl ScrollDataSource,
1332    ) -> Result<LayoutSnapshot> {
1333        self.ensure_graph_state(input_nodes);
1334        self.validate_graph_state(root_node_id)?;
1335
1336        // Root constraints should be tight to the viewport size if no explicit size is given
1337        let mut constraints = BoxConstraints::tight(viewport_size);
1338        if let Some(root) = self.graph_state.node(root_node_id) {
1339            // Only loosen if explicit dimensions are provided for the root node
1340            if root.width.is_some() || root.height.is_some() {
1341                constraints = BoxConstraints::loose(viewport_size.width, viewport_size.height)
1342                    .tighten(root.width, root.height);
1343            }
1344        }
1345
1346        let mut snapshot = LayoutSnapshot::new(viewport_size);
1347        let mut measure_cache = HashMap::new();
1348        self.layout_node_constraints(
1349            root_node_id,
1350            constraints,
1351            LayoutPoint::ZERO,
1352            &mut snapshot.nodes,
1353            &mut snapshot.constraints,
1354            &mut measure_cache,
1355            scroll_source,
1356            true,
1357            0,
1358        )?;
1359
1360        let visual_location = |node_id: WidgetId| -> Option<LayoutPoint> {
1361            let mut pos = snapshot.nodes.get(&node_id)?.rect.origin;
1362            let mut current = self.graph_state.parent_of(node_id);
1363            while let Some(parent_id) = current {
1364                if let Some(parent) = self.graph_state.node(parent_id) {
1365                    if let LayoutOp::Scroll { direction, .. } = &parent.op {
1366                        let offset = scroll_source.get_offset(parent_id);
1367                        match direction {
1368                            FlexDirection::Row => pos.x -= offset,
1369                            FlexDirection::Column => pos.y -= offset,
1370                        }
1371                    }
1372                    current = self.graph_state.parent_of(parent_id);
1373                } else {
1374                    break;
1375                }
1376            }
1377            Some(pos)
1378        };
1379
1380        let mut flyout_abs_overrides: HashMap<WidgetId, (f32, f32)> = HashMap::new();
1381        for node in self.graph_state.ordered_nodes() {
1382            if let LayoutOp::Flyout { anchor, content } = node.op {
1383                if let (Some(anchor_geom), Some(content_geom)) =
1384                    (snapshot.nodes.get(&anchor), snapshot.nodes.get(&content))
1385                {
1386                    if let Some(anchor_abs) = visual_location(anchor) {
1387                        let content_w = content_geom.rect.width();
1388                        let content_h = content_geom.rect.height();
1389                        let anchor_h = anchor_geom.rect.height();
1390                        let max_left = (snapshot.viewport_size.width - content_w).max(0.0);
1391                        let left_rel = anchor_abs.x.clamp(0.0, max_left);
1392
1393                        let below_top = anchor_abs.y + anchor_h;
1394                        let max_top = (snapshot.viewport_size.height - content_h).max(0.0);
1395                        let top_rel = if below_top + content_h <= snapshot.viewport_size.height {
1396                            below_top
1397                        } else {
1398                            let above_top = anchor_abs.y - content_h;
1399                            if above_top >= 0.0 {
1400                                above_top
1401                            } else {
1402                                below_top.clamp(0.0, max_top)
1403                            }
1404                        };
1405                        flyout_abs_overrides.insert(content, (left_rel, top_rel));
1406                    }
1407                }
1408            }
1409        }
1410
1411        if !flyout_abs_overrides.is_empty() {
1412            for (nid, (abs_x, abs_y)) in flyout_abs_overrides {
1413                if let Some(current) = snapshot.nodes.get(&nid) {
1414                    let dx = abs_x - current.rect.origin.x;
1415                    let dy = abs_y - current.rect.origin.y;
1416                    let mut stack = vec![(nid, 0usize)];
1417                    while let Some((current_id, depth)) = stack.pop() {
1418                        if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1419                            return Err(self.layout_depth_overflow(current_id, depth));
1420                        }
1421                        if let Some(geometry) = snapshot.nodes.get_mut(&current_id) {
1422                            geometry.rect.origin.x += dx;
1423                            geometry.rect.origin.y += dy;
1424                        }
1425                        for child_id in self.graph_state.children_of(current_id).iter().rev() {
1426                            stack.push((*child_id, depth + 1));
1427                        }
1428                    }
1429                }
1430            }
1431        }
1432
1433        self.graph_state.mark_layout_complete();
1434        self.incremental_reuse = None;
1435
1436        Ok(snapshot)
1437    }
1438
1439    pub fn compute_layout_incremental(
1440        &mut self,
1441        input_nodes: &[LayoutInputNode],
1442        root_node_id: WidgetId,
1443        viewport_size: LayoutSize,
1444        scroll_source: &impl ScrollDataSource,
1445        previous_snapshot: &LayoutSnapshot,
1446        dirty_nodes: &HashSet<WidgetId>,
1447    ) -> Result<LayoutSnapshot> {
1448        self.ensure_graph_state(input_nodes);
1449        self.validate_graph_state(root_node_id)?;
1450
1451        let mut dirty_ancestors = HashSet::new();
1452        for node_id in dirty_nodes {
1453            let mut current = Some(*node_id);
1454            while let Some(id) = current {
1455                if !dirty_ancestors.insert(id) {
1456                    break;
1457                }
1458                current = self.graph_state.parent_of(id);
1459            }
1460        }
1461        dirty_ancestors.insert(root_node_id);
1462
1463        self.incremental_reuse = Some(IncrementalLayoutReuseState {
1464            previous_snapshot: previous_snapshot.clone(),
1465            dirty_ancestors,
1466        });
1467        let result = self.compute_layout_constraints(
1468            input_nodes,
1469            root_node_id,
1470            viewport_size,
1471            scroll_source,
1472        );
1473        self.incremental_reuse = None;
1474        result
1475    }
1476
1477    fn emit_scroll_diagnostics(&self, snapshot: &LayoutSnapshot) {
1478        use fission_diagnostics::prelude as diag;
1479        let trace_scroll = std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
1480        for n in self.graph_state.ordered_nodes() {
1481            if let LayoutOp::Scroll { .. } = n.op {
1482                if let Some(g) = snapshot.nodes.get(&n.id) {
1483                    let note = if g.rect.height() <= 0.0 {
1484                        let parent_op = n
1485                            .parent_id
1486                            .and_then(|pid| self.graph_state.node(pid))
1487                            .map(|p| format!("{:?}", p.op));
1488                        let parent_constraints = n
1489                            .parent_id
1490                            .and_then(|pid| snapshot.constraints.get(&pid))
1491                            .copied();
1492                        snapshot
1493                            .constraints
1494                            .get(&n.id)
1495                            .map(|c| {
1496                                format!(
1497                                    "op={:?} parent={:?} parent_op={:?} parent_constraints={:?} constraints={:?}",
1498                                    n.op,
1499                                    n.parent_id,
1500                                    parent_op,
1501                                    parent_constraints,
1502                                    c
1503                                )
1504                            })
1505                    } else {
1506                        None
1507                    };
1508                    diag::emit(
1509                        diag::DiagCategory::Layout,
1510                        diag::DiagLevel::Debug,
1511                        diag::DiagEventKind::ScrollExtent {
1512                            node: n.id.as_u128(),
1513                            viewport_w: g.rect.width(),
1514                            viewport_h: g.rect.height(),
1515                            content_w: g.content_size.width,
1516                            content_h: g.content_size.height,
1517                            note,
1518                        },
1519                    );
1520                    if trace_scroll {
1521                        eprintln!(
1522                            "[scroll-trace] node={} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
1523                            n.id.as_u128(),
1524                            g.rect.width(),
1525                            g.rect.height(),
1526                            g.content_size.width,
1527                            g.content_size.height
1528                        );
1529                    }
1530                }
1531            }
1532        }
1533    }
1534
1535    fn layout_depth_overflow(&self, node_id: WidgetId, depth: usize) -> anyhow::Error {
1536        let details = format!(
1537            "layout recursion depth {} exceeded max {} at node {}",
1538            depth,
1539            Self::MAX_LAYOUT_RECURSION_DEPTH,
1540            node_id.as_u128()
1541        );
1542        diag::emit(
1543            diag::DiagCategory::Invariants,
1544            diag::DiagLevel::Error,
1545            diag::DiagEventKind::InvariantViolation {
1546                kind: "layout_recursion_depth".into(),
1547                node: Some(node_id.as_u128()),
1548                details: details.clone(),
1549                dump_ref: None,
1550            },
1551        );
1552        anyhow::anyhow!(details)
1553    }
1554
1555    fn copy_cached_subtree(
1556        &self,
1557        node_id: WidgetId,
1558        origin: LayoutPoint,
1559        current_constraints: BoxConstraints,
1560        out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
1561        constraints_out: &mut HashMap<WidgetId, BoxConstraints>,
1562    ) -> Result<Option<LayoutSize>> {
1563        let Some(reuse) = self.incremental_reuse.as_ref() else {
1564            return Ok(None);
1565        };
1566        if reuse.dirty_ancestors.contains(&node_id) {
1567            return Ok(None);
1568        }
1569
1570        let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(&node_id) else {
1571            return Ok(None);
1572        };
1573        let Some(previous_constraints) = reuse.previous_snapshot.constraints.get(&node_id).copied()
1574        else {
1575            return Ok(None);
1576        };
1577        if previous_constraints != current_constraints {
1578            return Ok(None);
1579        }
1580
1581        let dx = origin.x - previous_geometry.rect.origin.x;
1582        let dy = origin.y - previous_geometry.rect.origin.y;
1583        let mut stack = vec![(node_id, 0usize)];
1584        while let Some((current_id, depth)) = stack.pop() {
1585            if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1586                return Err(self.layout_depth_overflow(current_id, depth));
1587            }
1588            let Some(previous_geometry) = reuse.previous_snapshot.nodes.get(&current_id) else {
1589                return Ok(None);
1590            };
1591            let Some(previous_constraints) = reuse
1592                .previous_snapshot
1593                .constraints
1594                .get(&current_id)
1595                .copied()
1596            else {
1597                return Ok(None);
1598            };
1599
1600            let mut geometry = previous_geometry.clone();
1601            geometry.rect.origin.x += dx;
1602            geometry.rect.origin.y += dy;
1603            out.insert(current_id, geometry);
1604            constraints_out.insert(current_id, previous_constraints);
1605
1606            let children = self.graph_state.children_of(current_id);
1607            for child_id in children.iter().rev() {
1608                stack.push((*child_id, depth + 1));
1609            }
1610        }
1611
1612        Ok(Some(previous_geometry.content_size))
1613    }
1614
1615    fn layout_node_constraints(
1616        &self,
1617        node_id: WidgetId,
1618        constraints: BoxConstraints,
1619        origin: LayoutPoint,
1620        out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
1621        constraints_out: &mut HashMap<WidgetId, BoxConstraints>,
1622        measure_cache: &mut HashMap<MeasureCacheKey, LayoutSize>,
1623        scroll_source: &impl ScrollDataSource,
1624        record: bool,
1625        depth: usize,
1626    ) -> Result<LayoutSize> {
1627        if depth > Self::MAX_LAYOUT_RECURSION_DEPTH {
1628            return Err(self.layout_depth_overflow(node_id, depth));
1629        }
1630        if !record {
1631            let cache_key = MeasureCacheKey::new(node_id, constraints);
1632            if let Some(cached) = measure_cache.get(&cache_key).copied() {
1633                return Ok(cached);
1634            }
1635        }
1636        let node = match self.graph_state.node(node_id) {
1637            Some(node) => node,
1638            None => return Ok(LayoutSize::ZERO),
1639        };
1640
1641        if record {
1642            constraints_out.insert(node_id, constraints);
1643        }
1644
1645        if record {
1646            if let Some(reused) =
1647                self.copy_cached_subtree(node_id, origin, constraints, out, constraints_out)?
1648            {
1649                return Ok(reused);
1650            }
1651        }
1652
1653        let mut flow_children: Vec<WidgetId> = Vec::new();
1654        let mut abs_children: Vec<WidgetId> = Vec::new();
1655        for child_id in self.graph_state.children_of(node_id) {
1656            let is_absolute = matches!(
1657                self.graph_state.node(*child_id).map(|n| &n.op),
1658                Some(LayoutOp::AbsoluteFill) | Some(LayoutOp::Positioned { .. })
1659            );
1660            if is_absolute {
1661                abs_children.push(*child_id);
1662            } else {
1663                flow_children.push(*child_id);
1664            }
1665        }
1666        let rich_text_inline_children = node.rich_text.is_some() && !flow_children.is_empty();
1667
1668        let mut content_size;
1669        let size = match &node.op {
1670            LayoutOp::Box {
1671                width,
1672                height,
1673                min_width,
1674                max_width,
1675                min_height,
1676                max_height,
1677                padding,
1678                aspect_ratio,
1679                ..
1680            } => {
1681                let mut local =
1682                    constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
1683                local = local.tighten(*width, *height);
1684                if let Some(ratio) = aspect_ratio.filter(|r| *r > 0.0) {
1685                    let mut target_w = *width;
1686                    let mut target_h = *height;
1687
1688                    if target_w.is_some() && target_h.is_none() {
1689                        target_h = target_w.map(|w| w / ratio);
1690                    } else if target_h.is_some() && target_w.is_none() {
1691                        target_w = target_h.map(|h| h * ratio);
1692                    } else if target_w.is_none() && target_h.is_none() {
1693                        if local.is_width_bounded() || local.is_height_bounded() {
1694                            let (mut w, mut h) = if local.is_width_bounded() {
1695                                let w = local.max_w;
1696                                let h = w / ratio;
1697                                (w, h)
1698                            } else {
1699                                let h = local.max_h;
1700                                let w = h * ratio;
1701                                (w, h)
1702                            };
1703                            if local.is_width_bounded()
1704                                && local.is_height_bounded()
1705                                && h > local.max_h
1706                            {
1707                                h = local.max_h;
1708                                w = h * ratio;
1709                            }
1710                            target_w = Some(w);
1711                            target_h = Some(h);
1712                        }
1713                    }
1714
1715                    if target_w.is_some() || target_h.is_some() {
1716                        local = local.tighten(target_w, target_h);
1717                    }
1718                }
1719                let base_child_constraints = local.deflate(*padding);
1720                let mut max_child = LayoutSize::ZERO;
1721                let mut measured_children: Vec<(WidgetId, BoxConstraints, LayoutSize)> = Vec::new();
1722                if !rich_text_inline_children {
1723                    for child_id in &flow_children {
1724                        let (child_width, child_height, child_max_width, child_max_height) = self
1725                            .graph_state
1726                            .node(*child_id)
1727                            .map(|child| match &child.op {
1728                                LayoutOp::Box {
1729                                    width,
1730                                    height,
1731                                    max_width,
1732                                    max_height,
1733                                    ..
1734                                } => (*width, *height, *max_width, *max_height),
1735                                LayoutOp::Scroll {
1736                                    width,
1737                                    height,
1738                                    max_width,
1739                                    max_height,
1740                                    ..
1741                                } => (*width, *height, *max_width, *max_height),
1742                                LayoutOp::Embed { width, height, .. } => {
1743                                    (*width, *height, None, None)
1744                                }
1745                                _ => (None, None, None, None),
1746                            })
1747                            .unwrap_or((None, None, None, None));
1748                        let mut child_constraints = base_child_constraints;
1749                        let tight_width = child_constraints.min_w == child_constraints.max_w;
1750                        let stretch_width =
1751                            tight_width && child_width.is_none() && child_max_width.is_none();
1752                        if stretch_width {
1753                            child_constraints.min_w = child_constraints.max_w;
1754                        } else if tight_width
1755                            && (child_width.is_some() || child_max_width.is_some())
1756                        {
1757                            child_constraints.min_w = 0.0;
1758                        }
1759                        let tight_height = child_constraints.min_h == child_constraints.max_h;
1760                        let stretch_height =
1761                            tight_height && child_height.is_none() && child_max_height.is_none();
1762                        if stretch_height {
1763                            child_constraints.min_h = child_constraints.max_h;
1764                        } else if tight_height
1765                            && (child_height.is_some() || child_max_height.is_some())
1766                        {
1767                            child_constraints.min_h = 0.0;
1768                        }
1769                        let child_size = self.layout_node_constraints(
1770                            *child_id,
1771                            child_constraints,
1772                            LayoutPoint::ZERO,
1773                            out,
1774                            constraints_out,
1775                            measure_cache,
1776                            scroll_source,
1777                            false,
1778                            depth + 1,
1779                        )?;
1780                        max_child.width = max_child.width.max(child_size.width);
1781                        max_child.height = max_child.height.max(child_size.height);
1782                        measured_children.push((*child_id, child_constraints, child_size));
1783                    }
1784                }
1785                let padded = LayoutSize::new(
1786                    max_child.width + padding[0] + padding[1],
1787                    max_child.height + padding[2] + padding[3],
1788                );
1789                let size = local.constrain(padded);
1790                if record {
1791                    for (child_id, child_constraints, _child_size) in measured_children {
1792                        self.layout_node_constraints(
1793                            child_id,
1794                            child_constraints,
1795                            LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
1796                            out,
1797                            constraints_out,
1798                            measure_cache,
1799                            scroll_source,
1800                            record,
1801                            depth + 1,
1802                        )?;
1803                    }
1804                    if !abs_children.is_empty() {
1805                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
1806                        for child_id in abs_children {
1807                            self.layout_node_constraints(
1808                                child_id,
1809                                abs_constraints,
1810                                origin,
1811                                out,
1812                                constraints_out,
1813                                measure_cache,
1814                                scroll_source,
1815                                record,
1816                                depth + 1,
1817                            )?;
1818                        }
1819                    }
1820                }
1821                content_size = padded;
1822                size
1823            }
1824            LayoutOp::Flex {
1825                direction,
1826                wrap,
1827                padding,
1828                gap,
1829                align_items,
1830                justify_content,
1831                flex_grow,
1832                ..
1833            } => {
1834                let gap = gap.unwrap_or(0.0);
1835                let local = constraints.tighten(node.width, node.height);
1836                let inner = local.deflate(*padding);
1837                let is_row = matches!(direction, IrFlexDirection::Row);
1838
1839                let max_main = if is_row { inner.max_w } else { inner.max_h };
1840                let max_cross = if is_row { inner.max_h } else { inner.max_w };
1841                let min_main = if is_row { inner.min_w } else { inner.min_h };
1842                let min_cross = if is_row { inner.min_h } else { inner.min_w };
1843                let main_bounded = if is_row {
1844                    inner.is_width_bounded()
1845                } else {
1846                    inner.is_height_bounded()
1847                };
1848                let cross_bounded = if is_row {
1849                    inner.is_height_bounded()
1850                } else {
1851                    inner.is_width_bounded()
1852                };
1853
1854                if matches!(wrap, IrFlexWrap::Wrap | IrFlexWrap::WrapReverse) {
1855                    let mut lines: Vec<(Vec<(WidgetId, LayoutSize, BoxConstraints)>, f32, f32)> =
1856                        Vec::new();
1857                    let mut line_children: Vec<(WidgetId, LayoutSize, BoxConstraints)> = Vec::new();
1858                    let mut line_main = 0.0f32;
1859                    let mut line_cross = 0.0f32;
1860                    let mut max_line_main = 0.0f32;
1861
1862                    for child_id in &flow_children {
1863                        let child_constraints = if is_row {
1864                            BoxConstraints {
1865                                min_w: 0.0,
1866                                max_w: max_main,
1867                                min_h: 0.0,
1868                                max_h: max_cross,
1869                            }
1870                        } else {
1871                            BoxConstraints {
1872                                min_w: 0.0,
1873                                max_w: max_cross,
1874                                min_h: 0.0,
1875                                max_h: max_main,
1876                            }
1877                        };
1878                        let child_size = self.layout_node_constraints(
1879                            *child_id,
1880                            child_constraints,
1881                            LayoutPoint::ZERO,
1882                            out,
1883                            constraints_out,
1884                            measure_cache,
1885                            scroll_source,
1886                            false,
1887                            depth + 1,
1888                        )?;
1889                        let child_main = if is_row {
1890                            child_size.width
1891                        } else {
1892                            child_size.height
1893                        };
1894                        let child_cross = if is_row {
1895                            child_size.height
1896                        } else {
1897                            child_size.width
1898                        };
1899                        let next_main = if line_children.is_empty() {
1900                            child_main
1901                        } else {
1902                            line_main + gap + child_main
1903                        };
1904
1905                        if main_bounded && !line_children.is_empty() && next_main > max_main {
1906                            max_line_main = max_line_main.max(line_main);
1907                            lines.push((line_children, line_main, line_cross));
1908                            line_children = Vec::new();
1909                            line_main = 0.0;
1910                            line_cross = 0.0;
1911                        }
1912
1913                        if !line_children.is_empty() {
1914                            line_main += gap;
1915                        }
1916                        line_main += child_main;
1917                        line_cross = line_cross.max(child_cross);
1918                        line_children.push((*child_id, child_size, child_constraints));
1919                    }
1920
1921                    if !line_children.is_empty() {
1922                        max_line_main = max_line_main.max(line_main);
1923                        lines.push((line_children, line_main, line_cross));
1924                    }
1925
1926                    let mut container_main = if main_bounded && *flex_grow > 0.0 {
1927                        max_main
1928                    } else {
1929                        max_line_main
1930                    };
1931                    container_main = container_main.max(min_main);
1932                    let total_lines_cross: f32 =
1933                        lines.iter().map(|(_, _, cross)| *cross).sum::<f32>()
1934                            + gap * lines.len().saturating_sub(1) as f32;
1935                    let container_cross = total_lines_cross.max(min_cross);
1936                    let size = if is_row {
1937                        local.constrain(LayoutSize::new(
1938                            container_main + padding[0] + padding[1],
1939                            container_cross + padding[2] + padding[3],
1940                        ))
1941                    } else {
1942                        local.constrain(LayoutSize::new(
1943                            container_cross + padding[0] + padding[1],
1944                            container_main + padding[2] + padding[3],
1945                        ))
1946                    };
1947
1948                    let inner_main = if is_row {
1949                        size.width - padding[0] - padding[1]
1950                    } else {
1951                        size.height - padding[2] - padding[3]
1952                    };
1953                    let inner_cross = if is_row {
1954                        size.height - padding[2] - padding[3]
1955                    } else {
1956                        size.width - padding[0] - padding[1]
1957                    };
1958
1959                    let mut ordered_lines = lines;
1960                    if matches!(wrap, IrFlexWrap::WrapReverse) {
1961                        ordered_lines.reverse();
1962                    }
1963
1964                    let mut line_cursor = if matches!(wrap, IrFlexWrap::WrapReverse) {
1965                        (inner_cross - total_lines_cross).max(0.0)
1966                    } else {
1967                        0.0
1968                    };
1969
1970                    for (line_children, line_main, line_cross) in ordered_lines {
1971                        let remaining_space = (inner_main - line_main).max(0.0);
1972                        let mut extra_gap = 0.0;
1973                        let mut offset_main = 0.0;
1974                        match justify_content {
1975                            fission_ir::op::JustifyContent::Start => {}
1976                            fission_ir::op::JustifyContent::End => offset_main = remaining_space,
1977                            fission_ir::op::JustifyContent::Center => {
1978                                offset_main = remaining_space / 2.0
1979                            }
1980                            fission_ir::op::JustifyContent::SpaceBetween => {
1981                                if line_children.len() > 1 {
1982                                    extra_gap =
1983                                        remaining_space / (line_children.len() as f32 - 1.0);
1984                                }
1985                            }
1986                            fission_ir::op::JustifyContent::SpaceAround => {
1987                                if !line_children.is_empty() {
1988                                    extra_gap = remaining_space / line_children.len() as f32;
1989                                    offset_main = extra_gap / 2.0;
1990                                }
1991                            }
1992                            fission_ir::op::JustifyContent::SpaceEvenly => {
1993                                if !line_children.is_empty() {
1994                                    extra_gap =
1995                                        remaining_space / (line_children.len() as f32 + 1.0);
1996                                    offset_main = extra_gap;
1997                                }
1998                            }
1999                        }
2000
2001                        let mut cursor = offset_main;
2002                        for (child_id, child_size, mut child_constraints) in line_children {
2003                            let child_main = if is_row {
2004                                child_size.width
2005                            } else {
2006                                child_size.height
2007                            };
2008                            let child_cross = if is_row {
2009                                child_size.height
2010                            } else {
2011                                child_size.width
2012                            };
2013                            if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
2014                                if is_row {
2015                                    child_constraints.min_h = line_cross;
2016                                    child_constraints.max_h = line_cross;
2017                                } else {
2018                                    child_constraints.min_w = line_cross;
2019                                    child_constraints.max_w = line_cross;
2020                                }
2021                            }
2022                            let cross_offset = match align_items {
2023                                fission_ir::op::AlignItems::Start
2024                                | fission_ir::op::AlignItems::Stretch => 0.0,
2025                                fission_ir::op::AlignItems::End => {
2026                                    (line_cross - child_cross).max(0.0)
2027                                }
2028                                fission_ir::op::AlignItems::Center => {
2029                                    ((line_cross - child_cross) / 2.0).max(0.0)
2030                                }
2031                                fission_ir::op::AlignItems::Baseline => 0.0,
2032                            };
2033                            let child_origin = if is_row {
2034                                LayoutPoint::new(
2035                                    origin.x + padding[0] + cursor,
2036                                    origin.y + padding[2] + line_cursor + cross_offset,
2037                                )
2038                            } else {
2039                                LayoutPoint::new(
2040                                    origin.x + padding[0] + line_cursor + cross_offset,
2041                                    origin.y + padding[2] + cursor,
2042                                )
2043                            };
2044                            self.layout_node_constraints(
2045                                child_id,
2046                                child_constraints,
2047                                child_origin,
2048                                out,
2049                                constraints_out,
2050                                measure_cache,
2051                                scroll_source,
2052                                record,
2053                                depth + 1,
2054                            )?;
2055                            cursor += child_main + gap + extra_gap;
2056                        }
2057
2058                        line_cursor += line_cross + gap;
2059                    }
2060
2061                    if record && !abs_children.is_empty() {
2062                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
2063                        for child_id in abs_children {
2064                            self.layout_node_constraints(
2065                                child_id,
2066                                abs_constraints,
2067                                origin,
2068                                out,
2069                                constraints_out,
2070                                measure_cache,
2071                                scroll_source,
2072                                record,
2073                                depth + 1,
2074                            )?;
2075                        }
2076                    }
2077                    content_size = size;
2078                    size
2079                } else {
2080                    struct FlexChildEntry {
2081                        id: WidgetId,
2082                        flex: f32,
2083                        size: LayoutSize,
2084                        constraints: BoxConstraints,
2085                        is_flex: bool,
2086                    }
2087                    let mut measured: Vec<FlexChildEntry> = Vec::new();
2088                    let mut total_flex = 0.0f32;
2089                    let mut nonflex_main = 0.0f32;
2090                    let mut max_child_cross = 0.0f32;
2091                    let treat_flex_as_nonflex = !main_bounded;
2092
2093                    for child_id in &flow_children {
2094                        let child = match self.graph_state.node(*child_id) {
2095                            Some(child) => child,
2096                            None => continue,
2097                        };
2098                        let flex = child.flex_grow;
2099                        if flex > 0.0 && !treat_flex_as_nonflex {
2100                            total_flex += flex;
2101                            measured.push(FlexChildEntry {
2102                                id: *child_id,
2103                                flex,
2104                                size: LayoutSize::ZERO,
2105                                constraints: BoxConstraints::loose(0.0, 0.0),
2106                                is_flex: true,
2107                            });
2108                            continue;
2109                        }
2110                        let child_constraints = if is_row {
2111                            let cross =
2112                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2113                                    && cross_bounded
2114                                {
2115                                    BoxConstraints {
2116                                        min_w: 0.0,
2117                                        max_w: f32::INFINITY,
2118                                        min_h: max_cross,
2119                                        max_h: max_cross,
2120                                    }
2121                                } else {
2122                                    BoxConstraints {
2123                                        min_w: 0.0,
2124                                        max_w: f32::INFINITY,
2125                                        min_h: 0.0,
2126                                        max_h: max_cross,
2127                                    }
2128                                };
2129                            cross
2130                        } else {
2131                            let cross =
2132                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2133                                    && cross_bounded
2134                                {
2135                                    BoxConstraints {
2136                                        min_w: max_cross,
2137                                        max_w: max_cross,
2138                                        min_h: 0.0,
2139                                        max_h: f32::INFINITY,
2140                                    }
2141                                } else {
2142                                    BoxConstraints {
2143                                        min_w: 0.0,
2144                                        max_w: max_cross,
2145                                        min_h: 0.0,
2146                                        max_h: f32::INFINITY,
2147                                    }
2148                                };
2149                            cross
2150                        };
2151                        let child_size = self.layout_node_constraints(
2152                            *child_id,
2153                            child_constraints,
2154                            LayoutPoint::ZERO,
2155                            out,
2156                            constraints_out,
2157                            measure_cache,
2158                            scroll_source,
2159                            false,
2160                            depth + 1,
2161                        )?;
2162                        let child_main = if is_row {
2163                            child_size.width
2164                        } else {
2165                            child_size.height
2166                        };
2167                        let child_cross = if is_row {
2168                            child_size.height
2169                        } else {
2170                            child_size.width
2171                        };
2172                        nonflex_main += child_main;
2173                        max_child_cross = max_child_cross.max(child_cross);
2174                        measured.push(FlexChildEntry {
2175                            id: *child_id,
2176                            flex,
2177                            size: child_size,
2178                            constraints: child_constraints,
2179                            is_flex: false,
2180                        });
2181                    }
2182
2183                    let gap_total = gap * flow_children.len().saturating_sub(1) as f32;
2184                    let remaining = if main_bounded {
2185                        (max_main - nonflex_main - gap_total).max(0.0)
2186                    } else {
2187                        0.0
2188                    };
2189
2190                    for entry in measured.iter_mut().filter(|e| e.is_flex) {
2191                        let flex = entry.flex;
2192                        let allocated = if main_bounded && total_flex > 0.0 {
2193                            remaining * (flex / total_flex)
2194                        } else {
2195                            0.0
2196                        };
2197                        let child_constraints = if is_row {
2198                            let cross =
2199                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2200                                    && cross_bounded
2201                                {
2202                                    BoxConstraints {
2203                                        min_w: allocated,
2204                                        max_w: allocated,
2205                                        min_h: max_cross,
2206                                        max_h: max_cross,
2207                                    }
2208                                } else {
2209                                    BoxConstraints {
2210                                        min_w: allocated,
2211                                        max_w: allocated,
2212                                        min_h: 0.0,
2213                                        max_h: max_cross,
2214                                    }
2215                                };
2216                            cross
2217                        } else {
2218                            let cross =
2219                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
2220                                    && cross_bounded
2221                                {
2222                                    BoxConstraints {
2223                                        min_w: max_cross,
2224                                        max_w: max_cross,
2225                                        min_h: allocated,
2226                                        max_h: allocated,
2227                                    }
2228                                } else {
2229                                    BoxConstraints {
2230                                        min_w: 0.0,
2231                                        max_w: max_cross,
2232                                        min_h: allocated,
2233                                        max_h: allocated,
2234                                    }
2235                                };
2236                            cross
2237                        };
2238                        let child_size = self.layout_node_constraints(
2239                            entry.id,
2240                            child_constraints,
2241                            LayoutPoint::ZERO,
2242                            out,
2243                            constraints_out,
2244                            measure_cache,
2245                            scroll_source,
2246                            false,
2247                            depth + 1,
2248                        )?;
2249                        let child_cross = if is_row {
2250                            child_size.height
2251                        } else {
2252                            child_size.width
2253                        };
2254                        max_child_cross = max_child_cross.max(child_cross);
2255                        entry.size = child_size;
2256                        entry.constraints = child_constraints;
2257                    }
2258
2259                    let final_children_main: f32 = measured
2260                        .iter()
2261                        .map(|entry| {
2262                            if is_row {
2263                                entry.size.width
2264                            } else {
2265                                entry.size.height
2266                            }
2267                        })
2268                        .sum();
2269
2270                    let mut container_main = if main_bounded && *flex_grow > 0.0 {
2271                        max_main
2272                    } else {
2273                        final_children_main + gap_total
2274                    };
2275                    container_main = container_main.max(min_main);
2276
2277                    if main_bounded && final_children_main + gap_total > max_main {
2278                        // SHRINK logic
2279                        let mut total_shrink_scaled = 0.0f32;
2280                        for entry in &measured {
2281                            let Some(child) = self.graph_state.node(entry.id) else {
2282                                continue;
2283                            };
2284                            let main_size = if is_row {
2285                                entry.size.width
2286                            } else {
2287                                entry.size.height
2288                            };
2289                            total_shrink_scaled += main_size * child.flex_shrink;
2290                        }
2291
2292                        if total_shrink_scaled > 0.0 {
2293                            let overflow = (final_children_main + gap_total) - max_main;
2294                            for entry in &mut measured {
2295                                let Some(child) = self.graph_state.node(entry.id) else {
2296                                    continue;
2297                                };
2298                                let main_size = if is_row {
2299                                    entry.size.width
2300                                } else {
2301                                    entry.size.height
2302                                };
2303                                let shrink_amount = (main_size * child.flex_shrink
2304                                    / total_shrink_scaled)
2305                                    * overflow;
2306                                // Don't shrink below a reasonable minimum. Items with
2307                                // flex_shrink > 0 can shrink but not to zero - preserve at
2308                                // least a small fraction of their natural size.
2309                                let floor = if child.flex_shrink > 0.0 {
2310                                    // Check for explicit min/fixed dimension
2311                                    let explicit_min = match &child.op {
2312                                        LayoutOp::Box {
2313                                            min_width,
2314                                            min_height,
2315                                            height,
2316                                            width,
2317                                            ..
2318                                        } => {
2319                                            if is_row {
2320                                                min_width.or(*width).unwrap_or(0.0)
2321                                            } else {
2322                                                min_height.or(*height).unwrap_or(0.0)
2323                                            }
2324                                        }
2325                                        _ => 0.0,
2326                                    };
2327                                    explicit_min
2328                                } else {
2329                                    main_size // flex_shrink == 0 means don't shrink at all
2330                                };
2331                                let new_main = (main_size - shrink_amount).max(floor);
2332
2333                                let mut child_constraints = entry.constraints;
2334                                if is_row {
2335                                    child_constraints.min_w = new_main;
2336                                    child_constraints.max_w = new_main;
2337                                } else {
2338                                    child_constraints.min_h = new_main;
2339                                    child_constraints.max_h = new_main;
2340                                }
2341                                let new_size = self.layout_node_constraints(
2342                                    entry.id,
2343                                    child_constraints,
2344                                    LayoutPoint::ZERO,
2345                                    out,
2346                                    constraints_out,
2347                                    measure_cache,
2348                                    scroll_source,
2349                                    false,
2350                                    depth + 1,
2351                                )?;
2352                                entry.size = new_size;
2353                                entry.constraints = child_constraints;
2354                            }
2355                        }
2356                    }
2357
2358                    let container_cross = max_child_cross.max(min_cross);
2359                    let size = if is_row {
2360                        local.constrain(LayoutSize::new(
2361                            container_main + padding[0] + padding[1],
2362                            container_cross + padding[2] + padding[3],
2363                        ))
2364                    } else {
2365                        local.constrain(LayoutSize::new(
2366                            container_cross + padding[0] + padding[1],
2367                            container_main + padding[2] + padding[3],
2368                        ))
2369                    };
2370
2371                    let inner_main = if is_row {
2372                        size.width - padding[0] - padding[1]
2373                    } else {
2374                        size.height - padding[2] - padding[3]
2375                    };
2376                    let inner_cross = if is_row {
2377                        size.height - padding[2] - padding[3]
2378                    } else {
2379                        size.width - padding[0] - padding[1]
2380                    };
2381
2382                    let final_children_main: f32 = measured
2383                        .iter()
2384                        .map(|entry| {
2385                            if is_row {
2386                                entry.size.width
2387                            } else {
2388                                entry.size.height
2389                            }
2390                        })
2391                        .sum();
2392
2393                    let remaining_space = (inner_main - final_children_main - gap_total).max(0.0);
2394                    let mut extra_gap = 0.0;
2395                    let mut offset_main = 0.0;
2396                    match justify_content {
2397                        fission_ir::op::JustifyContent::Start => {}
2398                        fission_ir::op::JustifyContent::End => offset_main = remaining_space,
2399                        fission_ir::op::JustifyContent::Center => {
2400                            offset_main = remaining_space / 2.0
2401                        }
2402                        fission_ir::op::JustifyContent::SpaceBetween => {
2403                            if measured.len() > 1 {
2404                                extra_gap = remaining_space / (measured.len() as f32 - 1.0);
2405                            }
2406                        }
2407                        fission_ir::op::JustifyContent::SpaceAround => {
2408                            if !measured.is_empty() {
2409                                extra_gap = remaining_space / measured.len() as f32;
2410                                offset_main = extra_gap / 2.0;
2411                            }
2412                        }
2413                        fission_ir::op::JustifyContent::SpaceEvenly => {
2414                            if !measured.is_empty() {
2415                                extra_gap = remaining_space / (measured.len() as f32 + 1.0);
2416                                offset_main = extra_gap;
2417                            }
2418                        }
2419                    }
2420
2421                    let mut cursor = offset_main;
2422                    for entry in measured {
2423                        let child_main = if is_row {
2424                            entry.size.width
2425                        } else {
2426                            entry.size.height
2427                        };
2428                        let child_cross = if is_row {
2429                            entry.size.height
2430                        } else {
2431                            entry.size.width
2432                        };
2433                        let cross_offset = match align_items {
2434                            fission_ir::op::AlignItems::Start
2435                            | fission_ir::op::AlignItems::Stretch => 0.0,
2436                            fission_ir::op::AlignItems::End => (inner_cross - child_cross).max(0.0),
2437                            fission_ir::op::AlignItems::Center => {
2438                                ((inner_cross - child_cross) / 2.0).max(0.0)
2439                            }
2440                            fission_ir::op::AlignItems::Baseline => 0.0,
2441                        };
2442                        let child_origin = if is_row {
2443                            LayoutPoint::new(
2444                                origin.x + padding[0] + cursor,
2445                                origin.y + padding[2] + cross_offset,
2446                            )
2447                        } else {
2448                            LayoutPoint::new(
2449                                origin.x + padding[0] + cross_offset,
2450                                origin.y + padding[2] + cursor,
2451                            )
2452                        };
2453
2454                        let mut child_constraints = entry.constraints;
2455                        if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
2456                            // Only stretch children that don't have an explicit cross-axis size.
2457                            let child_node = self.graph_state.node(entry.id);
2458                            let has_explicit_cross = child_node
2459                                .map(|n| match &n.op {
2460                                    LayoutOp::Box { width, height, .. } => {
2461                                        if is_row {
2462                                            height.is_some()
2463                                        } else {
2464                                            width.is_some()
2465                                        }
2466                                    }
2467                                    _ => false,
2468                                })
2469                                .unwrap_or(false);
2470                            if !has_explicit_cross {
2471                                if is_row {
2472                                    child_constraints.min_h = inner_cross;
2473                                    child_constraints.max_h = inner_cross;
2474                                } else {
2475                                    child_constraints.min_w = inner_cross;
2476                                    child_constraints.max_w = inner_cross;
2477                                }
2478                            }
2479                        }
2480
2481                        self.layout_node_constraints(
2482                            entry.id,
2483                            child_constraints,
2484                            child_origin,
2485                            out,
2486                            constraints_out,
2487                            measure_cache,
2488                            scroll_source,
2489                            record,
2490                            depth + 1,
2491                        )?;
2492                        cursor += child_main + gap + extra_gap;
2493                    }
2494
2495                    if record && !abs_children.is_empty() {
2496                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
2497                        for child_id in abs_children {
2498                            self.layout_node_constraints(
2499                                child_id,
2500                                abs_constraints,
2501                                origin,
2502                                out,
2503                                constraints_out,
2504                                measure_cache,
2505                                scroll_source,
2506                                record,
2507                                depth + 1,
2508                            )?;
2509                        }
2510                    }
2511                    content_size = size;
2512                    size
2513                }
2514            }
2515            LayoutOp::Grid {
2516                columns,
2517                rows,
2518                column_gap,
2519                row_gap,
2520                padding,
2521            } => {
2522                let gap_x = column_gap.unwrap_or(0.0);
2523                let gap_y = row_gap.unwrap_or(0.0);
2524                let inner = constraints.deflate(*padding);
2525                let bounded_w = inner.is_width_bounded();
2526                let bounded_h = inner.is_height_bounded();
2527                let available_w = if bounded_w { inner.max_w } else { 0.0 };
2528                let available_h = if bounded_h { inner.max_h } else { 0.0 };
2529
2530                let col_count = columns.len().max(1);
2531                let mut col_widths = vec![0.0f32; col_count];
2532                let mut fr_total = 0.0f32;
2533                let mut fixed_total = 0.0f32;
2534                for (i, track) in columns.iter().enumerate() {
2535                    match track {
2536                        GridTrack::Points(p) => {
2537                            col_widths[i] = *p;
2538                            fixed_total += *p;
2539                        }
2540                        GridTrack::Percent(p) => {
2541                            let w = if bounded_w {
2542                                available_w * (*p / 100.0)
2543                            } else {
2544                                0.0
2545                            };
2546                            col_widths[i] = w;
2547                            fixed_total += w;
2548                        }
2549                        GridTrack::Fr(f) => fr_total += *f,
2550                        _ => {}
2551                    }
2552                }
2553                if fr_total > 0.0 && bounded_w {
2554                    let remaining =
2555                        (available_w - fixed_total - gap_x * (col_count.saturating_sub(1) as f32))
2556                            .max(0.0);
2557                    for (i, track) in columns.iter().enumerate() {
2558                        if let GridTrack::Fr(f) = track {
2559                            col_widths[i] = remaining * (*f / fr_total);
2560                        }
2561                    }
2562                }
2563
2564                let child_count = flow_children.len();
2565                let row_count = if rows.is_empty() {
2566                    (child_count + col_count - 1) / col_count
2567                } else {
2568                    rows.len()
2569                };
2570                let mut row_heights = vec![0.0f32; row_count.max(1)];
2571
2572                if !rows.is_empty() {
2573                    let mut row_fr_total = 0.0f32;
2574                    let mut row_fixed_total = 0.0f32;
2575                    for (i, track) in rows.iter().enumerate() {
2576                        if i >= row_heights.len() {
2577                            break;
2578                        }
2579                        match track {
2580                            GridTrack::Points(p) => {
2581                                row_heights[i] = *p;
2582                                row_fixed_total += *p;
2583                            }
2584                            GridTrack::Percent(p) => {
2585                                let h = if bounded_h {
2586                                    available_h * (*p / 100.0)
2587                                } else {
2588                                    0.0
2589                                };
2590                                row_heights[i] = h;
2591                                row_fixed_total += h;
2592                            }
2593                            GridTrack::Fr(f) => row_fr_total += *f,
2594                            _ => {}
2595                        }
2596                    }
2597                    if row_fr_total > 0.0 && bounded_h {
2598                        let remaining = (available_h
2599                            - row_fixed_total
2600                            - gap_y * (row_heights.len().saturating_sub(1) as f32))
2601                            .max(0.0);
2602                        for (i, track) in rows.iter().enumerate() {
2603                            if let GridTrack::Fr(f) = track {
2604                                row_heights[i] = remaining * (*f / row_fr_total);
2605                            }
2606                        }
2607                    }
2608                }
2609
2610                let mut cell_assignments = Vec::new();
2611                let mut auto_row = 0;
2612                let mut auto_col = 0;
2613
2614                for child_id in &flow_children {
2615                    let Some(child) = self.graph_state.node(*child_id) else {
2616                        continue;
2617                    };
2618                    let (row, col) = if let LayoutOp::GridItem {
2619                        row_start,
2620                        col_start,
2621                        ..
2622                    } = &child.op
2623                    {
2624                        let r = match row_start {
2625                            fission_ir::op::GridPlacement::Line(l) => {
2626                                (*l as usize).saturating_sub(1)
2627                            }
2628                            _ => auto_row,
2629                        };
2630                        let c = match col_start {
2631                            fission_ir::op::GridPlacement::Line(l) => {
2632                                (*l as usize).saturating_sub(1)
2633                            }
2634                            _ => auto_col,
2635                        };
2636                        (r, c)
2637                    } else {
2638                        let res = (auto_row, auto_col);
2639                        auto_col += 1;
2640                        if auto_col >= col_count {
2641                            auto_col = 0;
2642                            auto_row += 1;
2643                        }
2644                        res
2645                    };
2646                    cell_assignments.push((*child_id, row, col));
2647                }
2648
2649                for (child_id, row, col) in &cell_assignments {
2650                    if *row >= row_heights.len() || *col >= col_widths.len() {
2651                        continue;
2652                    }
2653                    let cell_w = col_widths[*col];
2654                    let cell_constraints = BoxConstraints {
2655                        min_w: cell_w,
2656                        max_w: cell_w,
2657                        min_h: 0.0,
2658                        max_h: if row_heights[*row] > 0.0 {
2659                            row_heights[*row]
2660                        } else {
2661                            f32::INFINITY
2662                        },
2663                    };
2664                    let child_size = self.layout_node_constraints(
2665                        *child_id,
2666                        cell_constraints,
2667                        LayoutPoint::ZERO,
2668                        out,
2669                        constraints_out,
2670                        measure_cache,
2671                        scroll_source,
2672                        false,
2673                        depth + 1,
2674                    )?;
2675                    if row_heights[*row] == 0.0 {
2676                        row_heights[*row] = child_size.height;
2677                    } else {
2678                        row_heights[*row] = row_heights[*row].max(child_size.height);
2679                    }
2680                }
2681
2682                let grid_w: f32 =
2683                    col_widths.iter().sum::<f32>() + gap_x * (col_count.saturating_sub(1) as f32);
2684                let grid_h: f32 = row_heights.iter().sum::<f32>()
2685                    + gap_y * (row_heights.len().saturating_sub(1) as f32);
2686                let size = constraints.constrain(LayoutSize::new(
2687                    grid_w + padding[0] + padding[1],
2688                    grid_h + padding[2] + padding[3],
2689                ));
2690
2691                if record {
2692                    let padding_origin_x = origin.x + padding[0];
2693                    let padding_origin_y = origin.y + padding[2];
2694                    for (child_id, row, col) in &cell_assignments {
2695                        if *row >= row_heights.len() || *col >= col_widths.len() {
2696                            continue;
2697                        }
2698                        let mut cell_x = padding_origin_x;
2699                        for i in 0..*col {
2700                            cell_x += col_widths[i] + gap_x;
2701                        }
2702                        let mut cell_y = padding_origin_y;
2703                        for i in 0..*row {
2704                            cell_y += row_heights[i] + gap_y;
2705                        }
2706                        let cell_w = col_widths[*col];
2707                        let cell_h = row_heights[*row];
2708                        let child_constraints = BoxConstraints {
2709                            min_w: cell_w,
2710                            max_w: cell_w,
2711                            min_h: cell_h,
2712                            max_h: cell_h,
2713                        };
2714                        self.layout_node_constraints(
2715                            *child_id,
2716                            child_constraints,
2717                            LayoutPoint::new(cell_x, cell_y),
2718                            out,
2719                            constraints_out,
2720                            measure_cache,
2721                            scroll_source,
2722                            record,
2723                            depth + 1,
2724                        )?;
2725                    }
2726                }
2727
2728                if record && !abs_children.is_empty() {
2729                    let abs_constraints = BoxConstraints::loose(size.width, size.height);
2730                    for child_id in abs_children {
2731                        self.layout_node_constraints(
2732                            child_id,
2733                            abs_constraints,
2734                            origin,
2735                            out,
2736                            constraints_out,
2737                            measure_cache,
2738                            scroll_source,
2739                            record,
2740                            depth + 1,
2741                        )?;
2742                    }
2743                }
2744                content_size = size;
2745                size
2746            }
2747            LayoutOp::GridItem { .. } => {
2748                let mut child_size = LayoutSize::ZERO;
2749                if let Some(child_id) = node.children_ids.first() {
2750                    child_size = self.layout_node_constraints(
2751                        *child_id,
2752                        constraints,
2753                        origin,
2754                        out,
2755                        constraints_out,
2756                        measure_cache,
2757                        scroll_source,
2758                        record,
2759                        depth + 1,
2760                    )?;
2761                }
2762                content_size = child_size;
2763                constraints.constrain(child_size)
2764            }
2765            LayoutOp::Scroll {
2766                direction,
2767                width,
2768                height,
2769                min_width,
2770                max_width,
2771                min_height,
2772                max_height,
2773                padding,
2774                ..
2775            } => {
2776                let mut local =
2777                    constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
2778                local = local.tighten(*width, *height);
2779                let is_horizontal = matches!(direction, FlexDirection::Row);
2780                let mut child_constraints = local.deflate(*padding);
2781                if is_horizontal {
2782                    child_constraints.min_w = 0.0;
2783                    child_constraints.max_w = f32::INFINITY;
2784                } else {
2785                    child_constraints.min_h = 0.0;
2786                    child_constraints.max_h = f32::INFINITY;
2787                }
2788                let mut child_size = LayoutSize::ZERO;
2789                if let Some(child_id) = flow_children.first() {
2790                    child_size = self.layout_node_constraints(
2791                        *child_id,
2792                        child_constraints,
2793                        LayoutPoint::ZERO,
2794                        out,
2795                        constraints_out,
2796                        measure_cache,
2797                        scroll_source,
2798                        false,
2799                        depth + 1,
2800                    )?;
2801                }
2802                let size = local.constrain(LayoutSize::new(
2803                    child_size.width + padding[0] + padding[1],
2804                    child_size.height + padding[2] + padding[3],
2805                ));
2806                if record {
2807                    if let Some(child_id) = flow_children.first() {
2808                        self.layout_node_constraints(
2809                            *child_id,
2810                            child_constraints,
2811                            LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
2812                            out,
2813                            constraints_out,
2814                            measure_cache,
2815                            scroll_source,
2816                            record,
2817                            depth + 1,
2818                        )?;
2819                    }
2820                    if !abs_children.is_empty() {
2821                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
2822                        for child_id in abs_children {
2823                            self.layout_node_constraints(
2824                                child_id,
2825                                abs_constraints,
2826                                origin,
2827                                out,
2828                                constraints_out,
2829                                measure_cache,
2830                                scroll_source,
2831                                record,
2832                                depth + 1,
2833                            )?;
2834                        }
2835                    }
2836                }
2837                content_size = child_size;
2838                size
2839            }
2840            LayoutOp::Align => {
2841                let child_constraints = BoxConstraints::loose(constraints.max_w, constraints.max_h);
2842                let mut child_size = LayoutSize::ZERO;
2843                if let Some(child_id) = flow_children.first() {
2844                    child_size = self.layout_node_constraints(
2845                        *child_id,
2846                        child_constraints,
2847                        LayoutPoint::ZERO,
2848                        out,
2849                        constraints_out,
2850                        measure_cache,
2851                        scroll_source,
2852                        false,
2853                        depth + 1,
2854                    )?;
2855                }
2856                let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
2857                    constraints.constrain(LayoutSize::new(
2858                        if constraints.is_width_bounded() {
2859                            constraints.max_w
2860                        } else {
2861                            child_size.width
2862                        },
2863                        if constraints.is_height_bounded() {
2864                            constraints.max_h
2865                        } else {
2866                            child_size.height
2867                        },
2868                    ))
2869                } else {
2870                    child_size
2871                };
2872                if let Some(child_id) = flow_children.first() {
2873                    let dx = ((size.width - child_size.width) / 2.0).max(0.0);
2874                    let dy = ((size.height - child_size.height) / 2.0).max(0.0);
2875                    self.layout_node_constraints(
2876                        *child_id,
2877                        child_constraints,
2878                        LayoutPoint::new(origin.x + dx, origin.y + dy),
2879                        out,
2880                        constraints_out,
2881                        measure_cache,
2882                        scroll_source,
2883                        record,
2884                        depth + 1,
2885                    )?;
2886                }
2887                if record && !abs_children.is_empty() {
2888                    let abs_constraints = BoxConstraints::loose(size.width, size.height);
2889                    for child_id in abs_children {
2890                        self.layout_node_constraints(
2891                            child_id,
2892                            abs_constraints,
2893                            origin,
2894                            out,
2895                            constraints_out,
2896                            measure_cache,
2897                            scroll_source,
2898                            record,
2899                            depth + 1,
2900                        )?;
2901                    }
2902                }
2903                content_size = child_size;
2904                size
2905            }
2906            LayoutOp::ZStack => {
2907                let mut max_child = LayoutSize::ZERO;
2908                for child_id in &flow_children {
2909                    let child_size = self.layout_node_constraints(
2910                        *child_id,
2911                        BoxConstraints::loose(constraints.max_w, constraints.max_h),
2912                        LayoutPoint::ZERO,
2913                        out,
2914                        constraints_out,
2915                        measure_cache,
2916                        scroll_source,
2917                        false,
2918                        depth + 1,
2919                    )?;
2920                    max_child.width = max_child.width.max(child_size.width);
2921                    max_child.height = max_child.height.max(child_size.height);
2922                }
2923                let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
2924                    constraints.constrain(LayoutSize::new(
2925                        if constraints.is_width_bounded() {
2926                            constraints.max_w
2927                        } else {
2928                            max_child.width
2929                        },
2930                        if constraints.is_height_bounded() {
2931                            constraints.max_h
2932                        } else {
2933                            max_child.height
2934                        },
2935                    ))
2936                } else {
2937                    max_child
2938                };
2939                for child_id in &flow_children {
2940                    let child_constraints = BoxConstraints::loose(size.width, size.height);
2941                    let child_origin = LayoutPoint::new(origin.x, origin.y);
2942                    self.layout_node_constraints(
2943                        *child_id,
2944                        child_constraints,
2945                        child_origin,
2946                        out,
2947                        constraints_out,
2948                        measure_cache,
2949                        scroll_source,
2950                        record,
2951                        depth + 1,
2952                    )?;
2953                }
2954                if record && !abs_children.is_empty() {
2955                    let abs_constraints = BoxConstraints::loose(size.width, size.height);
2956                    for child_id in abs_children {
2957                        self.layout_node_constraints(
2958                            child_id,
2959                            abs_constraints,
2960                            origin,
2961                            out,
2962                            constraints_out,
2963                            measure_cache,
2964                            scroll_source,
2965                            record,
2966                            depth + 1,
2967                        )?;
2968                    }
2969                }
2970                content_size = size;
2971                size
2972            }
2973            LayoutOp::Positioned {
2974                top,
2975                left,
2976                bottom,
2977                right,
2978                width,
2979                height,
2980            } => {
2981                let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
2982                let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
2983                let size = constraints.constrain(LayoutSize::new(target_w, target_h));
2984                let mut child_constraints = BoxConstraints::loose(size.width, size.height);
2985                if let (Some(l), Some(r)) = (left, right) {
2986                    let w = (size.width - l - r).max(0.0);
2987                    child_constraints = child_constraints.tighten(Some(w), None);
2988                }
2989                if let (Some(t), Some(b)) = (top, bottom) {
2990                    let h = (size.height - t - b).max(0.0);
2991                    child_constraints = child_constraints.tighten(None, Some(h));
2992                }
2993                child_constraints = child_constraints.tighten(*width, *height);
2994                if let Some(child_id) = node.children_ids.first() {
2995                    let child_size = self.layout_node_constraints(
2996                        *child_id,
2997                        child_constraints,
2998                        LayoutPoint::ZERO,
2999                        out,
3000                        constraints_out,
3001                        measure_cache,
3002                        scroll_source,
3003                        false,
3004                        depth + 1,
3005                    )?;
3006                    let x = left.unwrap_or_else(|| {
3007                        right
3008                            .map(|r| (size.width - r - child_size.width).max(0.0))
3009                            .unwrap_or(0.0)
3010                    });
3011                    let y = top.unwrap_or_else(|| {
3012                        bottom
3013                            .map(|b| (size.height - b - child_size.height).max(0.0))
3014                            .unwrap_or(0.0)
3015                    });
3016                    self.layout_node_constraints(
3017                        *child_id,
3018                        child_constraints,
3019                        LayoutPoint::new(origin.x + x, origin.y + y),
3020                        out,
3021                        constraints_out,
3022                        measure_cache,
3023                        scroll_source,
3024                        record,
3025                        depth + 1,
3026                    )?;
3027                }
3028                content_size = size;
3029                size
3030            }
3031            LayoutOp::Embed { width, height, .. } => {
3032                let local = constraints.tighten(*width, *height);
3033                let w = if local.is_width_bounded() {
3034                    local.max_w
3035                } else {
3036                    local.min_w
3037                };
3038                let h = if local.is_height_bounded() {
3039                    local.max_h
3040                } else {
3041                    local.min_h
3042                };
3043                let size = local.constrain(LayoutSize::new(w, h));
3044                content_size = size;
3045                size
3046            }
3047            LayoutOp::AbsoluteFill => {
3048                let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
3049                let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
3050                let size = constraints.constrain(LayoutSize::new(target_w, target_h));
3051                for child_id in self.graph_state.children_of(node_id) {
3052                    self.layout_node_constraints(
3053                        *child_id,
3054                        BoxConstraints::tight(size),
3055                        origin,
3056                        out,
3057                        constraints_out,
3058                        measure_cache,
3059                        scroll_source,
3060                        record,
3061                        depth + 1,
3062                    )?;
3063                }
3064                content_size = size;
3065                size
3066            }
3067            LayoutOp::Transform { .. } | LayoutOp::Clip { .. } => {
3068                let mut child_size = LayoutSize::ZERO;
3069                if let Some(child_id) = node.children_ids.first() {
3070                    child_size = self.layout_node_constraints(
3071                        *child_id,
3072                        constraints,
3073                        origin,
3074                        out,
3075                        constraints_out,
3076                        measure_cache,
3077                        scroll_source,
3078                        record,
3079                        depth + 1,
3080                    )?;
3081                }
3082                content_size = child_size;
3083                constraints.constrain(child_size)
3084            }
3085            LayoutOp::Flyout { anchor, content: _ } => {
3086                let loose = BoxConstraints::loose(
3087                    if constraints.is_width_bounded() {
3088                        constraints.max_w
3089                    } else {
3090                        f32::INFINITY
3091                    },
3092                    if constraints.is_height_bounded() {
3093                        constraints.max_h
3094                    } else {
3095                        f32::INFINITY
3096                    },
3097                );
3098                let mut child_size = LayoutSize::ZERO;
3099                for child_id in self.graph_state.children_of(node_id) {
3100                    child_size = self.layout_node_constraints(
3101                        *child_id,
3102                        loose,
3103                        origin,
3104                        out,
3105                        constraints_out,
3106                        measure_cache,
3107                        scroll_source,
3108                        false,
3109                        depth + 1,
3110                    )?;
3111                }
3112                if record {
3113                    let anchor_rect = out.get(anchor).map(|g| g.rect);
3114                    let place_x = anchor_rect.map(|r| r.x()).unwrap_or(origin.x);
3115                    let place_y = anchor_rect.map(|r| r.y() + r.height()).unwrap_or(origin.y);
3116                    for child_id in self.graph_state.children_of(node_id) {
3117                        self.layout_node_constraints(
3118                            *child_id,
3119                            loose,
3120                            LayoutPoint::new(place_x, place_y),
3121                            out,
3122                            constraints_out,
3123                            measure_cache,
3124                            scroll_source,
3125                            record,
3126                            depth + 1,
3127                        )?;
3128                    }
3129                }
3130                content_size = child_size;
3131                child_size
3132            }
3133        };
3134
3135        if let Some(runs) = &node.rich_text {
3136            if let Some(measurer) = &self.measurer {
3137                let node_max_w = match &node.op {
3138                    LayoutOp::Box { max_width, .. } => *max_width,
3139                    _ => None,
3140                };
3141                let avail_w = {
3142                    let from_constraints = if constraints.is_width_bounded() {
3143                        Some(constraints.max_w)
3144                    } else {
3145                        None
3146                    };
3147                    match (from_constraints, node_max_w) {
3148                        (Some(c), Some(m)) => Some(c.min(m)),
3149                        (Some(c), None) => Some(c),
3150                        (None, Some(m)) => Some(m),
3151                        (None, None) => None,
3152                    }
3153                };
3154                let rich_layout = measurer.layout_rich_text(runs, avail_w);
3155                let text_content = LayoutSize::new(rich_layout.width, rich_layout.height);
3156                let measured = constraints.constrain(text_content);
3157                if rich_text_inline_children
3158                    && rich_layout.inline_boxes.len() == flow_children.len()
3159                {
3160                    let result =
3161                        self.record_geometry(node_id, origin, measured, text_content, out, record);
3162                    if record {
3163                        let mut inline_boxes = rich_layout.inline_boxes;
3164                        inline_boxes.sort_by_key(|inline_box| inline_box.id);
3165                        for (child_id, inline_box) in flow_children.iter().zip(inline_boxes.iter())
3166                        {
3167                            self.layout_node_constraints(
3168                                *child_id,
3169                                BoxConstraints::tight(LayoutSize::new(
3170                                    inline_box.width,
3171                                    inline_box.height,
3172                                )),
3173                                LayoutPoint::new(origin.x + inline_box.x, origin.y + inline_box.y),
3174                                out,
3175                                constraints_out,
3176                                measure_cache,
3177                                scroll_source,
3178                                record,
3179                                depth + 1,
3180                            )?;
3181                        }
3182                    }
3183                    if !record {
3184                        measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3185                    }
3186                    return Ok(result);
3187                }
3188                if node.children_ids.is_empty() {
3189                    let result =
3190                        self.record_geometry(node_id, origin, measured, text_content, out, record);
3191                    if !record {
3192                        measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3193                    }
3194                    return Ok(result);
3195                }
3196                content_size.width = content_size.width.max(text_content.width);
3197                content_size.height = content_size.height.max(text_content.height);
3198            }
3199        }
3200
3201        let result = self.record_geometry(node_id, origin, size, content_size, out, record);
3202        if !record {
3203            measure_cache.insert(MeasureCacheKey::new(node_id, constraints), result);
3204        }
3205        Ok(result)
3206    }
3207
3208    fn record_geometry(
3209        &self,
3210        node_id: WidgetId,
3211        origin: LayoutPoint,
3212        size: LayoutSize,
3213        content_size: LayoutSize,
3214        out: &mut HashMap<WidgetId, LayoutNodeGeometry>,
3215        record: bool,
3216    ) -> LayoutSize {
3217        let mut rect_origin = origin;
3218        let mut rect_size = size;
3219        let mut rect_content = content_size;
3220        let mut had_non_finite = false;
3221
3222        if !rect_origin.x.is_finite() {
3223            rect_origin.x = 0.0;
3224            had_non_finite = true;
3225        }
3226        if !rect_origin.y.is_finite() {
3227            rect_origin.y = 0.0;
3228            had_non_finite = true;
3229        }
3230        if !rect_size.width.is_finite() {
3231            rect_size.width = 0.0;
3232            had_non_finite = true;
3233        }
3234        if !rect_size.height.is_finite() {
3235            rect_size.height = 0.0;
3236            had_non_finite = true;
3237        }
3238        if !rect_content.width.is_finite() {
3239            rect_content.width = 0.0;
3240            had_non_finite = true;
3241        }
3242        if !rect_content.height.is_finite() {
3243            rect_content.height = 0.0;
3244            had_non_finite = true;
3245        }
3246
3247        if had_non_finite {
3248            diag::emit(
3249                diag::DiagCategory::Invariants,
3250                diag::DiagLevel::Error,
3251                diag::DiagEventKind::InvariantViolation {
3252                    kind: "non_finite_layout".into(),
3253                    node: Some(node_id.as_u128()),
3254                    details: format!(
3255                        "origin=({:.2},{:.2}) size=({:.2},{:.2}) content=({:.2},{:.2})",
3256                        origin.x,
3257                        origin.y,
3258                        size.width,
3259                        size.height,
3260                        content_size.width,
3261                        content_size.height
3262                    ),
3263                    dump_ref: None,
3264                },
3265            );
3266        }
3267
3268        if record {
3269            let rect = LayoutRect::new(
3270                rect_origin.x,
3271                rect_origin.y,
3272                rect_size.width,
3273                rect_size.height,
3274            );
3275            out.insert(
3276                node_id,
3277                LayoutNodeGeometry {
3278                    rect,
3279                    content_size: rect_content,
3280                },
3281            );
3282        }
3283        rect_size
3284    }
3285}