Skip to main content

fission_layout/
lib.rs

1//! Constraint-based layout engine for the Fission UI framework.
2//!
3//! This crate takes a flat list of [`LayoutInputNode`]s (produced from the
4//! [`fission-ir`](fission_ir) intermediate representation) and computes the
5//! absolute position and size of every node on screen. It implements:
6//!
7//! * **Box layout** -- constrained containers with padding, min/max, and aspect ratio.
8//! * **Flexbox** -- single-axis distribution with grow, shrink, wrap, alignment, and justification.
9//! * **CSS Grid** -- two-dimensional track-based layout with `fr`, `%`, and fixed sizing.
10//! * **Scroll containers** -- clipped viewports with infinite content axes.
11//! * **Absolute positioning** -- `top`/`left`/`right`/`bottom` offsets.
12//! * **ZStack** -- overlapping children.
13//! * **Flyout anchoring** -- popups positioned relative to an anchor node.
14//!
15//! The engine is pure computation with no platform dependencies. Give it nodes and
16//! a viewport size, and it returns a [`LayoutSnapshot`] mapping every
17//! [`NodeId`](fission_ir::NodeId) to a [`LayoutRect`].
18//!
19//! # Example
20//!
21//! ```rust,no_run
22//! use fission_layout::*;
23//! use fission_ir::{NodeId, LayoutOp};
24//!
25//! let mut engine = LayoutEngine::new();
26//! let root_id = NodeId::explicit("root");
27//! // ... build LayoutInputNode list ...
28//! // let snapshot = engine.compute_layout(&nodes, root_id, viewport, &|_| 0.0).unwrap();
29//! ```
30
31use anyhow::Result;
32use fission_diagnostics::prelude as diag;
33use fission_ir::op::TextRun;
34use fission_ir::{FlexDirection as IrFlexDirection, FlexWrap as IrFlexWrap, NodeId};
35use serde::{Deserialize, Serialize};
36use std::collections::{HashMap, HashSet};
37use std::sync::Arc;
38
39pub use fission_ir::{FlexDirection, GridPlacement, GridTrack, LayoutOp};
40
41/// A source of scroll offsets for scroll containers.
42///
43/// The layout engine calls [`get_offset`](ScrollDataSource::get_offset) for each
44/// [`LayoutOp::Scroll`] node to learn how far the user has scrolled. Platform
45/// backends implement this trait (or pass a closure, which also implements it).
46///
47/// # Example
48///
49/// ```rust
50/// use fission_layout::ScrollDataSource;
51/// use fission_ir::NodeId;
52///
53/// // A closure works as a ScrollDataSource:
54/// let source = |_node: NodeId| -> f32 { 0.0 };
55/// assert_eq!(source.get_offset(NodeId::explicit("scroll")), 0.0);
56/// ```
57pub trait ScrollDataSource {
58    /// Returns the current scroll offset for the given scroll container node.
59    fn get_offset(&self, node_id: NodeId) -> f32;
60}
61
62impl<F> ScrollDataSource for F
63where
64    F: Fn(NodeId) -> f32,
65{
66    fn get_offset(&self, node_id: NodeId) -> f32 {
67        self(node_id)
68    }
69}
70
71/// The scalar type used for all layout measurements.
72///
73/// Currently `f32`. Matches [`fission_ir::op::LayoutUnit`].
74pub type LayoutUnit = f32;
75
76/// Returns `value` if it is finite, otherwise `fallback`.
77fn finite_or(value: LayoutUnit, fallback: LayoutUnit) -> LayoutUnit {
78    if value.is_finite() {
79        value
80    } else {
81        fallback
82    }
83}
84
85/// A 2D point in layout coordinate space.
86///
87/// Represents an (x, y) position in logical pixels. Used for node origins and
88/// coordinate calculations throughout the layout engine.
89#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
90pub struct LayoutPoint {
91    /// Horizontal position in logical pixels.
92    pub x: LayoutUnit,
93    /// Vertical position in logical pixels.
94    pub y: LayoutUnit,
95}
96
97impl LayoutPoint {
98    /// The origin point: `(0.0, 0.0)`.
99    pub const ZERO: Self = Self { x: 0.0, y: 0.0 };
100
101    /// Creates a new point from x and y coordinates.
102    pub fn new(x: LayoutUnit, y: LayoutUnit) -> Self {
103        Self { x, y }
104    }
105}
106
107/// A 2D size in layout coordinate space.
108///
109/// Represents a width and height in logical pixels. Used as the output of layout
110/// measurement and as input to constraints.
111#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Default)]
112pub struct LayoutSize {
113    /// Width in logical pixels.
114    pub width: LayoutUnit,
115    /// Height in logical pixels.
116    pub height: LayoutUnit,
117}
118
119impl LayoutSize {
120    /// A zero-sized size: `(0.0, 0.0)`.
121    pub const ZERO: Self = Self {
122        width: 0.0,
123        height: 0.0,
124    };
125
126    /// Creates a new size from width and height values.
127    pub fn new(width: LayoutUnit, height: LayoutUnit) -> Self {
128        Self { width, height }
129    }
130}
131
132/// Minimum and maximum width/height bounds passed from parent to child during layout.
133///
134/// `BoxConstraints` is the fundamental mechanism for top-down size negotiation. A
135/// parent creates constraints describing the space available to a child, and the
136/// child returns a [`LayoutSize`] that satisfies those constraints.
137///
138/// There are two common patterns:
139///
140/// * **Tight constraints** -- `min == max`, forcing the child to a specific size.
141///   Created with [`BoxConstraints::tight`].
142/// * **Loose constraints** -- `min == 0`, giving the child freedom to be smaller
143///   than the max. Created with [`BoxConstraints::loose`].
144///
145/// # Example
146///
147/// ```rust
148/// use fission_layout::{BoxConstraints, LayoutSize};
149///
150/// let constraints = BoxConstraints::loose(800.0, 600.0);
151/// assert_eq!(constraints.min_w, 0.0);
152///
153/// let child_wants = LayoutSize::new(300.0, 200.0);
154/// let actual = constraints.constrain(child_wants);
155/// assert_eq!(actual, child_wants); // fits within the constraints
156/// ```
157#[derive(Debug, Clone, Copy, PartialEq)]
158pub struct BoxConstraints {
159    /// Minimum width the child must occupy.
160    pub min_w: LayoutUnit,
161    /// Maximum width the child may occupy. Can be `f32::INFINITY` for unbounded.
162    pub max_w: LayoutUnit,
163    /// Minimum height the child must occupy.
164    pub min_h: LayoutUnit,
165    /// Maximum height the child may occupy. Can be `f32::INFINITY` for unbounded.
166    pub max_h: LayoutUnit,
167}
168
169impl BoxConstraints {
170    /// Creates tight constraints that force a child to exactly `size`.
171    ///
172    /// Both min and max are set to the given width/height.
173    pub fn tight(size: LayoutSize) -> Self {
174        Self {
175            min_w: size.width,
176            max_w: size.width,
177            min_h: size.height,
178            max_h: size.height,
179        }
180    }
181
182    /// Creates loose constraints: min is zero, max is the given values.
183    ///
184    /// The child can be anywhere from zero to `max_w` x `max_h`.
185    pub fn loose(max_w: LayoutUnit, max_h: LayoutUnit) -> Self {
186        Self {
187            min_w: 0.0,
188            max_w,
189            min_h: 0.0,
190            max_h,
191        }
192    }
193
194    /// Returns `true` if the maximum width is finite (not `f32::INFINITY`).
195    pub fn is_width_bounded(&self) -> bool {
196        self.max_w.is_finite()
197    }
198
199    /// Returns `true` if the maximum height is finite (not `f32::INFINITY`).
200    pub fn is_height_bounded(&self) -> bool {
201        self.max_h.is_finite()
202    }
203
204    /// Clamps `size` so it falls within these constraints.
205    ///
206    /// The returned width is `max(min_w, min(size.width, max_w))`, and likewise
207    /// for height.
208    pub fn constrain(&self, size: LayoutSize) -> LayoutSize {
209        LayoutSize {
210            width: size.width.max(self.min_w).min(self.max_w),
211            height: size.height.max(self.min_h).min(self.max_h),
212        }
213    }
214
215    /// Returns the smallest size that satisfies these constraints: `(min_w, min_h)`.
216    pub fn smallest(&self) -> LayoutSize {
217        LayoutSize::new(self.min_w, self.min_h)
218    }
219
220    /// Returns new constraints shrunk inward by `padding`.
221    ///
222    /// Padding is `[left, right, top, bottom]`. Horizontal padding reduces the
223    /// width bounds; vertical padding reduces the height bounds. Bounds are
224    /// clamped to zero.
225    pub fn deflate(&self, padding: [LayoutUnit; 4]) -> Self {
226        let horiz = padding[0] + padding[1];
227        let vert = padding[2] + padding[3];
228        let max_w = (self.max_w - horiz).max(0.0);
229        let max_h = (self.max_h - vert).max(0.0);
230        let min_w = (self.min_w - horiz).max(0.0).min(max_w);
231        let min_h = (self.min_h - vert).max(0.0).min(max_h);
232        Self {
233            min_w,
234            max_w,
235            min_h,
236            max_h,
237        }
238    }
239
240    /// Makes the constraints tighter by fixing the width and/or height.
241    ///
242    /// If `width` is `Some`, both `min_w` and `max_w` are set to that value
243    /// (clamped to the current bounds). Same for `height`.
244    pub fn tighten(&self, width: Option<LayoutUnit>, height: Option<LayoutUnit>) -> Self {
245        let mut out = *self;
246        if let Some(w) = width {
247            let clamped = w.min(out.max_w).max(out.min_w);
248            out.min_w = clamped;
249            out.max_w = clamped;
250        }
251        if let Some(h) = height {
252            let clamped = h.min(out.max_h).max(out.min_h);
253            out.min_h = clamped;
254            out.max_h = clamped;
255        }
256        if out.max_w < out.min_w {
257            out.max_w = out.min_w;
258        }
259        if out.max_h < out.min_h {
260            out.max_h = out.min_h;
261        }
262        out
263    }
264
265    /// Applies additional min/max constraints on top of the current ones.
266    ///
267    /// Each `Some` value further restricts the corresponding bound. `None` values
268    /// leave the bound unchanged. After adjustment, max is clamped to be at least
269    /// min.
270    pub fn apply_min_max(
271        &self,
272        min_w: Option<LayoutUnit>,
273        max_w: Option<LayoutUnit>,
274        min_h: Option<LayoutUnit>,
275        max_h: Option<LayoutUnit>,
276    ) -> Self {
277        let mut out = *self;
278        if let Some(w) = min_w {
279            out.min_w = out.min_w.max(w);
280        }
281        if let Some(h) = min_h {
282            out.min_h = out.min_h.max(h);
283        }
284        if let Some(w) = max_w {
285            out.max_w = out.max_w.min(w);
286        }
287        if let Some(h) = max_h {
288            out.max_h = out.max_h.min(h);
289        }
290        if out.max_w < out.min_w {
291            out.max_w = out.min_w;
292        }
293        if out.max_h < out.min_h {
294            out.max_h = out.min_h;
295        }
296        out
297    }
298
299    /// Returns loose constraints with the same maximums but zeroed minimums.
300    ///
301    /// Useful when a parent wants to let a child be as small as it likes while
302    /// still capping its maximum size.
303    pub fn loosen(&self) -> Self {
304        Self {
305            min_w: 0.0,
306            max_w: self.max_w,
307            min_h: 0.0,
308            max_h: self.max_h,
309        }
310    }
311}
312
313/// An axis-aligned rectangle: an origin point plus a size.
314///
315/// `LayoutRect` is the final output for every node after layout: it says exactly
316/// where the node sits on screen and how large it is.
317///
318/// # Example
319///
320/// ```rust
321/// use fission_layout::{LayoutRect, LayoutPoint};
322///
323/// let rect = LayoutRect::new(10.0, 20.0, 300.0, 200.0);
324/// assert_eq!(rect.right(), 310.0);
325/// assert!(rect.contains(LayoutPoint::new(15.0, 25.0)));
326/// ```
327#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
328pub struct LayoutRect {
329    /// The top-left corner of the rectangle.
330    pub origin: LayoutPoint,
331    /// The width and height of the rectangle.
332    pub size: LayoutSize,
333}
334
335impl LayoutRect {
336    /// Creates a rectangle from x, y, width, and height.
337    pub fn new(x: LayoutUnit, y: LayoutUnit, width: LayoutUnit, height: LayoutUnit) -> Self {
338        Self {
339            origin: LayoutPoint { x, y },
340            size: LayoutSize { width, height },
341        }
342    }
343
344    /// The x coordinate of the left edge.
345    pub fn x(&self) -> LayoutUnit {
346        self.origin.x
347    }
348    /// The y coordinate of the top edge.
349    pub fn y(&self) -> LayoutUnit {
350        self.origin.y
351    }
352    /// The width of the rectangle.
353    pub fn width(&self) -> LayoutUnit {
354        self.size.width
355    }
356    /// The height of the rectangle.
357    pub fn height(&self) -> LayoutUnit {
358        self.size.height
359    }
360
361    /// The x coordinate of the right edge (`x + width`).
362    pub fn right(&self) -> LayoutUnit {
363        self.origin.x + self.size.width
364    }
365    /// The y coordinate of the bottom edge (`y + height`).
366    pub fn bottom(&self) -> LayoutUnit {
367        self.origin.y + self.size.height
368    }
369
370    /// Returns `true` if the point `p` lies within this rectangle (inclusive on
371    /// the left/top edges, exclusive on the right/bottom edges).
372    pub fn contains(&self, p: LayoutPoint) -> bool {
373        p.x >= self.x() && p.x < self.right() && p.y >= self.y() && p.y < self.bottom()
374    }
375}
376
377/// The computed geometry of a single layout node.
378///
379/// After layout, every node has a bounding rectangle (its position and size on
380/// screen) and a content size (how large its content actually is, which may exceed
381/// the rect for scroll containers).
382#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
383pub struct LayoutNodeGeometry {
384    /// The bounding rectangle of this node in absolute (screen) coordinates.
385    pub rect: LayoutRect,
386    /// The natural size of the node's content before clipping. For scroll containers,
387    /// this may be larger than `rect.size`, indicating scrollable overflow.
388    pub content_size: LayoutSize,
389}
390
391/// The complete output of a layout pass.
392///
393/// `LayoutSnapshot` maps every node to its computed geometry and records the
394/// viewport size that was used. It is the primary interface between the layout
395/// engine and downstream consumers (the renderer, hit testing, accessibility).
396///
397/// # Example
398///
399/// ```rust,no_run
400/// use fission_layout::{LayoutSnapshot, LayoutSize};
401/// use fission_ir::NodeId;
402///
403/// let snapshot = LayoutSnapshot::new(LayoutSize::new(800.0, 600.0));
404/// assert_eq!(snapshot.viewport_size.width, 800.0);
405/// ```
406#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
407pub struct LayoutSnapshot {
408    /// Computed geometry for every node, keyed by [`NodeId`].
409    pub nodes: HashMap<NodeId, LayoutNodeGeometry>,
410    /// The constraints that were passed to each node during layout. Useful for
411    /// debugging. Skipped during serialization.
412    #[serde(skip)]
413    pub constraints: HashMap<NodeId, BoxConstraints>,
414    /// The viewport size used for this layout pass.
415    pub viewport_size: LayoutSize,
416}
417
418impl LayoutSnapshot {
419    /// Creates an empty snapshot for the given viewport size.
420    pub fn new(viewport_size: LayoutSize) -> Self {
421        Self {
422            nodes: HashMap::new(),
423            constraints: HashMap::new(),
424            viewport_size,
425        }
426    }
427
428    /// Returns the full geometry (rect + content size) for a node, or `None` if
429    /// the node was not part of this layout pass.
430    pub fn get_node_geometry(&self, node_id: NodeId) -> Option<&LayoutNodeGeometry> {
431        self.nodes.get(&node_id)
432    }
433
434    /// Returns just the bounding rectangle for a node, or `None` if not found.
435    pub fn get_node_rect(&self, node_id: NodeId) -> Option<LayoutRect> {
436        self.nodes.get(&node_id).map(|g| g.rect)
437    }
438
439    /// Returns the constraints that were passed to a node during layout, or `None`
440    /// if not found. Useful for debugging layout issues.
441    pub fn get_node_constraints(&self, node_id: NodeId) -> Option<BoxConstraints> {
442        self.constraints.get(&node_id).copied()
443    }
444}
445
446/// A flattened representation of a layout node, ready for the layout engine.
447///
448/// The widget compiler produces a list of `LayoutInputNode`s from the IR. Each node
449/// carries its layout operation, parent/child relationships, flex participation
450/// parameters, and optional rich text content for text measurement.
451///
452/// The layout engine operates on `&[LayoutInputNode]` rather than traversing the
453/// IR directly, which keeps the engine decoupled from the IR's internal structure.
454#[derive(Debug, Clone)]
455pub struct LayoutInputNode {
456    /// The unique identity of this node.
457    pub id: NodeId,
458    /// The parent node's ID, or `None` for the root.
459    pub parent_id: Option<NodeId>,
460    /// The layout operation this node performs.
461    pub op: LayoutOp,
462    /// Ordered list of child node IDs.
463    pub children_ids: Vec<NodeId>,
464    /// A human-readable name for debugging and diagnostics.
465    pub debug_name: String,
466    /// Explicit width override, or `None` to derive from constraints.
467    pub width: Option<LayoutUnit>,
468    /// Explicit height override, or `None` to derive from constraints.
469    pub height: Option<LayoutUnit>,
470    /// How much extra main-axis space this node claims from its flex parent.
471    pub flex_grow: LayoutUnit,
472    /// How much this node shrinks when its flex parent overflows.
473    pub flex_shrink: LayoutUnit,
474    /// Optional rich text content. When present, the layout engine uses the
475    /// [`TextMeasurer`] to determine the node's intrinsic size from the text.
476    pub rich_text: Option<Vec<TextRun>>,
477}
478
479/// Per-line metrics returned by text measurement.
480///
481/// When the layout engine or hit-testing code needs to know about individual lines
482/// of text (e.g., for cursor positioning in a multi-line text field), it calls
483/// [`TextMeasurer::get_line_metrics`] and receives a `Vec<LineMetric>`.
484pub struct LineMetric {
485    /// Byte index where this line starts in the source string.
486    pub start_index: usize,
487    /// Byte index where this line ends in the source string (exclusive).
488    pub end_index: usize,
489    /// Distance from the top of the line to its alphabetic baseline, in logical pixels.
490    pub baseline: f32,
491    /// Total height of the line (ascent + descent + leading), in logical pixels.
492    pub height: f32,
493    /// Measured width of the line's content, in logical pixels.
494    pub width: f32,
495}
496
497/// A platform-provided text measurement backend.
498///
499/// The layout engine does not shape or measure text itself. Instead, platform
500/// backends implement `TextMeasurer` to wrap their native text engine (CoreText
501/// on macOS, DirectWrite on Windows, HarfBuzz + FreeType on Linux, etc.).
502///
503/// All methods have default implementations that return zero-sized results, so
504/// you only need to override the methods your backend supports.
505///
506/// # Required
507///
508/// * [`measure`](TextMeasurer::measure) -- must be implemented to get correct text layout.
509///
510/// # Optional
511///
512/// * [`hit_test`](TextMeasurer::hit_test) -- needed for click-to-cursor in text fields.
513/// * [`get_line_metrics`](TextMeasurer::get_line_metrics) -- needed for multi-line cursor navigation.
514/// * [`get_caret_position`](TextMeasurer::get_caret_position) -- needed for drawing the text cursor.
515/// * [`measure_rich_text`](TextMeasurer::measure_rich_text) -- needed for mixed-style text.
516pub trait TextMeasurer: Send + Sync {
517    /// Measures single-style text and returns `(width, height)` in logical pixels.
518    ///
519    /// If `available_width` is `Some`, the text should be wrapped at that width.
520    /// If `None`, the text is measured as a single unwrapped line.
521    fn measure(&self, text: &str, font_size: f32, available_width: Option<f32>) -> (f32, f32);
522
523    /// Returns the byte index of the character closest to the point `(x, y)`,
524    /// relative to the text's origin. Used for click-to-cursor in text fields.
525    ///
526    /// The default implementation returns `0`.
527    fn hit_test(
528        &self,
529        _text: &str,
530        _font_size: f32,
531        _available_width: Option<f32>,
532        _x: f32,
533        _y: f32,
534    ) -> usize {
535        0
536    }
537
538    /// Returns per-line metrics for the given text. Used for multi-line text fields
539    /// and line-based cursor navigation.
540    ///
541    /// The default implementation returns an empty vec.
542    fn get_line_metrics(
543        &self,
544        text: &str,
545        font_size: f32,
546        available_width: Option<f32>,
547    ) -> Vec<LineMetric> {
548        vec![]
549    }
550
551    /// Returns the `(x, y)` position of the text cursor at `caret_index` (byte offset),
552    /// relative to the text's origin.
553    ///
554    /// The default implementation returns `(0.0, 0.0)`.
555    fn get_caret_position(
556        &self,
557        _text: &str,
558        _font_size: f32,
559        _available_width: Option<f32>,
560        _caret_index: usize,
561    ) -> (f32, f32) {
562        (0.0, 0.0)
563    }
564
565    /// Measures multi-style (rich) text and returns `(width, height)` in logical pixels.
566    ///
567    /// The default implementation returns `(0.0, 0.0)`.
568    fn measure_rich_text(&self, _runs: &[TextRun], _available_width: Option<f32>) -> (f32, f32) {
569        (0.0, 0.0)
570    }
571}
572
573/// The constraint-based layout solver.
574///
575/// `LayoutEngine` walks the node tree top-down, passing [`BoxConstraints`] from
576/// parent to child, and bottom-up, returning [`LayoutSize`] from child to parent.
577/// The final result is a [`LayoutSnapshot`] that maps every node to its absolute
578/// screen-space rectangle.
579///
580/// The engine optionally holds a [`TextMeasurer`] for sizing text nodes. Without
581/// one, text nodes are treated as zero-sized.
582///
583/// # Example
584///
585/// ```rust,no_run
586/// use fission_layout::*;
587/// use fission_ir::NodeId;
588/// use std::sync::Arc;
589///
590/// let mut engine = LayoutEngine::new();
591/// // engine = engine.with_measurer(my_text_measurer);
592///
593/// // let snapshot = engine.compute_layout(&nodes, root_id, viewport, &|_| 0.0).unwrap();
594/// ```
595pub struct LayoutEngine {
596    measurer: Option<Arc<dyn TextMeasurer>>,
597}
598
599impl LayoutEngine {
600    /// Creates a new layout engine with no text measurer.
601    ///
602    /// Text nodes will be treated as zero-sized until a measurer is provided
603    /// via [`with_measurer`](LayoutEngine::with_measurer).
604    pub fn new() -> Self {
605        Self { measurer: None }
606    }
607
608    /// Returns a new engine with the given text measurer attached.
609    ///
610    /// This is a builder-style method that consumes and returns `self`.
611    pub fn with_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
612        self.measurer = Some(measurer);
613        self
614    }
615
616    /// Incrementally updates layout for the given dirty nodes.
617    ///
618    /// Currently a no-op placeholder for future incremental layout support.
619    pub fn update(&mut self, input_nodes: &[LayoutInputNode], _dirty_set: &HashSet<NodeId>) {
620        let _ = input_nodes;
621    }
622
623    /// Rebuilds internal data structures from the full node list.
624    ///
625    /// Currently a no-op placeholder for future optimization.
626    pub fn rebuild(&mut self, input_nodes: &[LayoutInputNode]) -> Result<()> {
627        let _ = input_nodes;
628        Ok(())
629    }
630
631    /// Verifies parent-child consistency and checks for cycles in the node graph.
632    ///
633    /// Call this during development/testing to catch malformed IR before it causes
634    /// layout panics. Returns `Err` with a description of the first problem found.
635    pub fn verify_post_update(&self, input_nodes: &[LayoutInputNode], root: NodeId) -> Result<()> {
636        let node_map: HashMap<NodeId, &LayoutInputNode> =
637            input_nodes.iter().map(|n| (n.id, n)).collect();
638        // Parent/child consistency
639        for n in input_nodes {
640            for child in &n.children_ids {
641                let child_node = node_map
642                    .get(child)
643                    .ok_or_else(|| anyhow::anyhow!("[verify] child {:?} not found", child))?;
644                if child_node.parent_id != Some(n.id) {
645                    anyhow::bail!("[verify] parent/child mismatch parent={:?} child={:?} child.parent_id={:?}", n.id, child, child_node.parent_id);
646                }
647            }
648        }
649        // Cycle via DFS
650        fn dfs(
651            id: NodeId,
652            map: &HashMap<NodeId, &LayoutInputNode>,
653            visited: &mut HashSet<NodeId>,
654            stack: &mut HashSet<NodeId>,
655        ) -> Result<()> {
656            if !visited.insert(id) {
657                return Ok(());
658            }
659            stack.insert(id);
660            let node = map
661                .get(&id)
662                .ok_or_else(|| anyhow::anyhow!("[verify] missing node {:?}", id))?;
663            for child in &node.children_ids {
664                if stack.contains(child) {
665                    anyhow::bail!("[verify] cycle detected at {:?} -> {:?}", id, child);
666                }
667                dfs(*child, map, visited, stack)?;
668            }
669            stack.remove(&id);
670            Ok(())
671        }
672        let mut visited = HashSet::new();
673        let mut stack = HashSet::new();
674        dfs(root, &node_map, &mut visited, &mut stack)?;
675        Ok(())
676    }
677
678    /// Computes layout for the entire node tree and returns a snapshot.
679    ///
680    /// This is the main entry point. It runs the constraint-based layout algorithm
681    /// starting from `root_node_id`, using `viewport_size` as the root constraints,
682    /// and querying `scroll_source` for scroll offsets. After layout, it emits scroll
683    /// diagnostics for debugging.
684    ///
685    /// # Arguments
686    ///
687    /// * `input_nodes` -- The flat list of all layout nodes.
688    /// * `root_node_id` -- Which node is the root of the tree.
689    /// * `viewport_size` -- The size of the window/screen.
690    /// * `scroll_source` -- Provides scroll offsets for scroll containers.
691    ///
692    /// # Errors
693    ///
694    /// Returns `Err` if a cycle is detected or a required node is missing.
695    pub fn compute_layout(
696        &mut self,
697        input_nodes: &[LayoutInputNode],
698        root_node_id: NodeId,
699        viewport_size: LayoutSize,
700        scroll_source: &impl ScrollDataSource,
701    ) -> Result<LayoutSnapshot> {
702        let snapshot = self.compute_layout_constraints(
703            input_nodes,
704            root_node_id,
705            viewport_size,
706            scroll_source,
707        )?;
708        self.emit_scroll_diagnostics(input_nodes, &snapshot);
709        Ok(snapshot)
710    }
711
712    /// Lower-level layout that skips scroll diagnostics.
713    ///
714    /// Same as [`compute_layout`](LayoutEngine::compute_layout) but does not emit
715    /// diagnostic events. Useful when you need the snapshot but not the debug output.
716    pub fn compute_layout_constraints(
717        &self,
718        input_nodes: &[LayoutInputNode],
719        root_node_id: NodeId,
720        viewport_size: LayoutSize,
721        scroll_source: &impl ScrollDataSource,
722    ) -> Result<LayoutSnapshot> {
723        let node_map: HashMap<NodeId, &LayoutInputNode> =
724            input_nodes.iter().map(|n| (n.id, n)).collect();
725
726        // Root constraints should be tight to the viewport size if no explicit size is given
727        let mut constraints = BoxConstraints::tight(viewport_size);
728        if let Some(root) = node_map.get(&root_node_id) {
729            // Only loosen if explicit dimensions are provided for the root node
730            if root.width.is_some() || root.height.is_some() {
731                constraints = BoxConstraints::loose(viewport_size.width, viewport_size.height).tighten(root.width, root.height);
732            }
733        }
734
735        let mut snapshot = LayoutSnapshot::new(viewport_size);
736        self.layout_node_constraints(
737            root_node_id,
738            constraints,
739            LayoutPoint::ZERO,
740            &node_map,
741            &mut snapshot.nodes,
742            &mut snapshot.constraints,
743            scroll_source,
744            true,
745            0,
746        );
747
748        let visual_location = |node_id: NodeId| -> Option<LayoutPoint> {
749            let mut pos = snapshot.nodes.get(&node_id)?.rect.origin;
750            let mut current = node_map.get(&node_id).and_then(|n| n.parent_id);
751            while let Some(parent_id) = current {
752                if let Some(parent) = node_map.get(&parent_id) {
753                    if let LayoutOp::Scroll { direction, .. } = &parent.op {
754                        let offset = scroll_source.get_offset(parent_id);
755                        match direction {
756                            FlexDirection::Row => pos.x -= offset,
757                            FlexDirection::Column => pos.y -= offset,
758                        }
759                    }
760                    current = parent.parent_id;
761                } else {
762                    break;
763                }
764            }
765            Some(pos)
766        };
767
768        let mut flyout_abs_overrides: HashMap<NodeId, (f32, f32)> = HashMap::new();
769        for node in input_nodes {
770            if let LayoutOp::Flyout { anchor, content } = node.op {
771                if let (Some(anchor_geom), Some(_content_geom)) =
772                    (snapshot.nodes.get(&anchor), snapshot.nodes.get(&content))
773                {
774                    if let Some(anchor_abs) = visual_location(anchor) {
775                        let anchor_w = anchor_geom.rect.width();
776                        let anchor_h = anchor_geom.rect.height();
777                        let left_rel = anchor_abs.x;
778                        let top_rel = anchor_abs.y + anchor_h;
779                        flyout_abs_overrides.insert(content, (left_rel, top_rel));
780                    }
781                }
782            }
783        }
784
785        if !flyout_abs_overrides.is_empty() {
786            fn apply_offset_recursive(
787                id: NodeId,
788                dx: f32,
789                dy: f32,
790                node_map: &HashMap<NodeId, &LayoutInputNode>,
791                geometries: &mut HashMap<NodeId, LayoutNodeGeometry>,
792            ) {
793                if let Some(g) = geometries.get_mut(&id) {
794                    g.rect.origin.x += dx;
795                    g.rect.origin.y += dy;
796                }
797                if let Some(n) = node_map.get(&id) {
798                    for child in &n.children_ids {
799                        apply_offset_recursive(*child, dx, dy, node_map, geometries);
800                    }
801                }
802            }
803
804            for (nid, (abs_x, abs_y)) in flyout_abs_overrides {
805                if let Some(current) = snapshot.nodes.get(&nid) {
806                    let dx = abs_x - current.rect.origin.x;
807                    let dy = abs_y - current.rect.origin.y;
808                    apply_offset_recursive(nid, dx, dy, &node_map, &mut snapshot.nodes);
809                }
810            }
811        }
812
813        Ok(snapshot)
814    }
815
816    fn emit_scroll_diagnostics(&self, input_nodes: &[LayoutInputNode], snapshot: &LayoutSnapshot) {
817        use fission_diagnostics::prelude as diag;
818        let trace_scroll = std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
819        let node_map: HashMap<NodeId, &LayoutInputNode> =
820            input_nodes.iter().map(|n| (n.id, n)).collect();
821        for n in input_nodes {
822            if let LayoutOp::Scroll { .. } = n.op {
823                if let Some(g) = snapshot.nodes.get(&n.id) {
824                    let note = if g.rect.height() <= 0.0 {
825                        let parent_op = n
826                            .parent_id
827                            .and_then(|pid| node_map.get(&pid))
828                            .map(|p| format!("{:?}", p.op));
829                        let parent_constraints = n
830                            .parent_id
831                            .and_then(|pid| snapshot.constraints.get(&pid))
832                            .copied();
833                        snapshot
834                            .constraints
835                            .get(&n.id)
836                            .map(|c| {
837                                format!(
838                                    "op={:?} parent={:?} parent_op={:?} parent_constraints={:?} constraints={:?}",
839                                    n.op,
840                                    n.parent_id,
841                                    parent_op,
842                                    parent_constraints,
843                                    c
844                                )
845                            })
846                    } else {
847                        None
848                    };
849                    diag::emit(
850                        diag::DiagCategory::Layout,
851                        diag::DiagLevel::Debug,
852                        diag::DiagEventKind::ScrollExtent {
853                            node: n.id.as_u128(),
854                            viewport_w: g.rect.width(),
855                            viewport_h: g.rect.height(),
856                            content_w: g.content_size.width,
857                            content_h: g.content_size.height,
858                            note,
859                        },
860                    );
861                    if trace_scroll {
862                        eprintln!(
863                            "[scroll-trace] node={} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
864                            n.id.as_u128(),
865                            g.rect.width(),
866                            g.rect.height(),
867                            g.content_size.width,
868                            g.content_size.height
869                        );
870                    }
871                }
872            }
873        }
874    }
875
876    fn layout_node_constraints(
877        &self,
878        node_id: NodeId,
879        constraints: BoxConstraints,
880        origin: LayoutPoint,
881        node_map: &HashMap<NodeId, &LayoutInputNode>,
882        out: &mut HashMap<NodeId, LayoutNodeGeometry>,
883        constraints_out: &mut HashMap<NodeId, BoxConstraints>,
884        scroll_source: &impl ScrollDataSource,
885        record: bool,
886        depth: usize,
887    ) -> LayoutSize {
888        if depth > 100 {
889            panic!("Stack overflow safeguard: depth > 100 at node {:?}", node_id);
890        }
891        let node = match node_map.get(&node_id) {
892            Some(n) => *n,
893            None => return LayoutSize::ZERO,
894        };
895
896        if record {
897            constraints_out.insert(node_id, constraints);
898        }
899
900        let mut flow_children: Vec<NodeId> = Vec::new();
901        let mut abs_children: Vec<NodeId> = Vec::new();
902        for child_id in &node.children_ids {
903            let is_absolute = matches!(
904                node_map.get(child_id).map(|n| &n.op),
905                Some(LayoutOp::AbsoluteFill) | Some(LayoutOp::Positioned { .. })
906            );
907            if is_absolute {
908                abs_children.push(*child_id);
909            } else {
910                flow_children.push(*child_id);
911            }
912        }
913
914        let mut content_size = LayoutSize::ZERO;
915        let size = match &node.op {
916            LayoutOp::Box {
917                width,
918                height,
919                min_width,
920                max_width,
921                min_height,
922                max_height,
923                padding,
924                aspect_ratio,
925                ..
926            } => {
927                let mut local =
928                    constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
929                local = local.tighten(*width, *height);
930                if let Some(ratio) = aspect_ratio.filter(|r| *r > 0.0) {
931                    let mut target_w = *width;
932                    let mut target_h = *height;
933
934                    if target_w.is_some() && target_h.is_none() {
935                        target_h = Some(target_w.unwrap() / ratio);
936                    } else if target_h.is_some() && target_w.is_none() {
937                        target_w = Some(target_h.unwrap() * ratio);
938                    } else if target_w.is_none() && target_h.is_none() {
939                        if local.is_width_bounded() || local.is_height_bounded() {
940                            let (mut w, mut h) = if local.is_width_bounded() {
941                                let w = local.max_w;
942                                let h = w / ratio;
943                                (w, h)
944                            } else {
945                                let h = local.max_h;
946                                let w = h * ratio;
947                                (w, h)
948                            };
949                            if local.is_width_bounded()
950                                && local.is_height_bounded()
951                                && h > local.max_h
952                            {
953                                h = local.max_h;
954                                w = h * ratio;
955                            }
956                            target_w = Some(w);
957                            target_h = Some(h);
958                        }
959                    }
960
961                    if target_w.is_some() || target_h.is_some() {
962                        local = local.tighten(target_w, target_h);
963                    }
964                }
965                let base_child_constraints = local.deflate(*padding);
966                let mut max_child = LayoutSize::ZERO;
967                let mut measured_children: Vec<(NodeId, BoxConstraints, LayoutSize)> = Vec::new();
968                for child_id in &flow_children {
969                    let (child_width, child_height, child_max_width, child_max_height) = node_map
970                        .get(child_id)
971                        .map(|child| match &child.op {
972                            LayoutOp::Box {
973                                width,
974                                height,
975                                max_width,
976                                max_height,
977                                ..
978                            } => (*width, *height, *max_width, *max_height),
979                            LayoutOp::Scroll {
980                                width,
981                                height,
982                                max_width,
983                                max_height,
984                                ..
985                            } => (*width, *height, *max_width, *max_height),
986                            LayoutOp::Embed { width, height, .. } => (*width, *height, None, None),
987                            _ => (None, None, None, None),
988                        })
989                        .unwrap_or((None, None, None, None));
990                    let mut child_constraints = base_child_constraints;
991                    let stretch_width = child_constraints.min_w == child_constraints.max_w
992                        && child_width.is_none()
993                        && child_max_width.is_none();
994                    if stretch_width {
995                        child_constraints.min_w = child_constraints.max_w;
996                    } else {
997                        child_constraints.min_w = 0.0;
998                    }
999                    let stretch_height = child_constraints.min_h == child_constraints.max_h
1000                        && child_height.is_none()
1001                        && child_max_height.is_none();
1002                    if stretch_height {
1003                        child_constraints.min_h = child_constraints.max_h;
1004                    } else {
1005                        child_constraints.min_h = 0.0;
1006                    }
1007                    let child_size = self.layout_node_constraints(
1008                        *child_id,
1009                        child_constraints,
1010                        LayoutPoint::ZERO,
1011                        node_map,
1012                        out,
1013                        constraints_out,
1014                        scroll_source,
1015                        false,
1016                        depth + 1,
1017                    );
1018                    max_child.width = max_child.width.max(child_size.width);
1019                    max_child.height = max_child.height.max(child_size.height);
1020                    measured_children.push((*child_id, child_constraints, child_size));
1021                }
1022                let padded = LayoutSize::new(
1023                    max_child.width + padding[0] + padding[1],
1024                    max_child.height + padding[2] + padding[3],
1025                );
1026                let size = local.constrain(padded);
1027                if record {
1028                    for (child_id, child_constraints, _child_size) in measured_children {
1029                        self.layout_node_constraints(
1030                            child_id,
1031                            child_constraints,
1032                            LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]),
1033                            node_map,
1034                            out,
1035                            constraints_out,
1036                            scroll_source,
1037                            record,
1038                            depth + 1,
1039                        );
1040                    }
1041                    if !abs_children.is_empty() {
1042                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
1043                        for child_id in abs_children {
1044                            self.layout_node_constraints(
1045                                child_id,
1046                                abs_constraints,
1047                                origin,
1048                                node_map,
1049                                out,
1050                                constraints_out,
1051                                scroll_source,
1052                                record,
1053                                depth + 1,
1054                            );
1055                        }
1056                    }
1057                }
1058                content_size = padded;
1059                size
1060            }
1061            LayoutOp::Flex {
1062                direction,
1063                wrap,
1064                padding,
1065                gap,
1066                align_items,
1067                justify_content,
1068                flex_grow,
1069                ..
1070            } => {
1071                let gap = gap.unwrap_or(0.0);
1072                let mut local = constraints.tighten(node.width, node.height);
1073                let inner = local.deflate(*padding);
1074                let is_row = matches!(direction, IrFlexDirection::Row);
1075
1076                let max_main = if is_row { inner.max_w } else { inner.max_h };
1077                let max_cross = if is_row { inner.max_h } else { inner.max_w };
1078                let min_main = if is_row { inner.min_w } else { inner.min_h };
1079                let min_cross = if is_row { inner.min_h } else { inner.min_w };
1080                let main_bounded = if is_row {
1081                    inner.is_width_bounded()
1082                } else {
1083                    inner.is_height_bounded()
1084                };
1085                let cross_bounded = if is_row {
1086                    inner.is_height_bounded()
1087                } else {
1088                    inner.is_width_bounded()
1089                };
1090
1091                if matches!(wrap, IrFlexWrap::Wrap | IrFlexWrap::WrapReverse) {
1092                    let mut lines: Vec<(Vec<(NodeId, LayoutSize, BoxConstraints)>, f32, f32)> =
1093                        Vec::new();
1094                    let mut line_children: Vec<(NodeId, LayoutSize, BoxConstraints)> = Vec::new();
1095                    let mut line_main = 0.0f32;
1096                    let mut line_cross = 0.0f32;
1097                    let mut max_line_main = 0.0f32;
1098
1099                    for child_id in &flow_children {
1100                        let child_constraints = if is_row {
1101                            BoxConstraints {
1102                                min_w: 0.0,
1103                                max_w: max_main,
1104                                min_h: 0.0,
1105                                max_h: max_cross,
1106                            }
1107                        } else {
1108                            BoxConstraints {
1109                                min_w: 0.0,
1110                                max_w: max_cross,
1111                                min_h: 0.0,
1112                                max_h: max_main,
1113                            }
1114                        };
1115                        let child_size = self.layout_node_constraints(
1116                            *child_id,
1117                            child_constraints,
1118                            LayoutPoint::ZERO,
1119                            node_map,
1120                            out,
1121                            constraints_out,
1122                            scroll_source,
1123                            false,
1124                            depth + 1,
1125                        );
1126                        let child_main = if is_row {
1127                            child_size.width
1128                        } else {
1129                            child_size.height
1130                        };
1131                        let child_cross = if is_row {
1132                            child_size.height
1133                        } else {
1134                            child_size.width
1135                        };
1136                        let next_main = if line_children.is_empty() {
1137                            child_main
1138                        } else {
1139                            line_main + gap + child_main
1140                        };
1141
1142                        if main_bounded && !line_children.is_empty() && next_main > max_main {
1143                            max_line_main = max_line_main.max(line_main);
1144                            lines.push((line_children, line_main, line_cross));
1145                            line_children = Vec::new();
1146                            line_main = 0.0;
1147                            line_cross = 0.0;
1148                        }
1149
1150                        if !line_children.is_empty() {
1151                            line_main += gap;
1152                        }
1153                        line_main += child_main;
1154                        line_cross = line_cross.max(child_cross);
1155                        line_children.push((*child_id, child_size, child_constraints));
1156                    }
1157
1158                    if !line_children.is_empty() {
1159                        max_line_main = max_line_main.max(line_main);
1160                        lines.push((line_children, line_main, line_cross));
1161                    }
1162
1163                    let mut container_main = if main_bounded && *flex_grow > 0.0 {
1164                        max_main
1165                    } else {
1166                        max_line_main
1167                    };
1168                    container_main = container_main.max(min_main);
1169                    let total_lines_cross: f32 =
1170                        lines.iter().map(|(_, _, cross)| *cross).sum::<f32>()
1171                            + gap * lines.len().saturating_sub(1) as f32;
1172                    let mut container_cross = total_lines_cross.max(min_cross);
1173                    let size = if is_row {
1174                        local.constrain(LayoutSize::new(
1175                            container_main + padding[0] + padding[1],
1176                            container_cross + padding[2] + padding[3],
1177                        ))
1178                    } else {
1179                        local.constrain(LayoutSize::new(
1180                            container_cross + padding[0] + padding[1],
1181                            container_main + padding[2] + padding[3],
1182                        ))
1183                    };
1184
1185                    let inner_main = if is_row {
1186                        size.width - padding[0] - padding[1]
1187                    } else {
1188                        size.height - padding[2] - padding[3]
1189                    };
1190                    let inner_cross = if is_row {
1191                        size.height - padding[2] - padding[3]
1192                    } else {
1193                        size.width - padding[0] - padding[1]
1194                    };
1195
1196                    let mut ordered_lines = lines;
1197                    if matches!(wrap, IrFlexWrap::WrapReverse) {
1198                        ordered_lines.reverse();
1199                    }
1200
1201                    let mut line_cursor = if matches!(wrap, IrFlexWrap::WrapReverse) {
1202                        (inner_cross - total_lines_cross).max(0.0)
1203                    } else {
1204                        0.0
1205                    };
1206
1207                    for (line_children, line_main, line_cross) in ordered_lines {
1208                        let mut remaining_space = (inner_main - line_main).max(0.0);
1209                        let mut extra_gap = 0.0;
1210                        let mut offset_main = 0.0;
1211                        match justify_content {
1212                            fission_ir::op::JustifyContent::Start => {}
1213                            fission_ir::op::JustifyContent::End => offset_main = remaining_space,
1214                            fission_ir::op::JustifyContent::Center => {
1215                                offset_main = remaining_space / 2.0
1216                            }
1217                            fission_ir::op::JustifyContent::SpaceBetween => {
1218                                if line_children.len() > 1 {
1219                                    extra_gap =
1220                                        remaining_space / (line_children.len() as f32 - 1.0);
1221                                }
1222                            }
1223                            fission_ir::op::JustifyContent::SpaceAround => {
1224                                if !line_children.is_empty() {
1225                                    extra_gap = remaining_space / line_children.len() as f32;
1226                                    offset_main = extra_gap / 2.0;
1227                                }
1228                            }
1229                            fission_ir::op::JustifyContent::SpaceEvenly => {
1230                                if !line_children.is_empty() {
1231                                    extra_gap =
1232                                        remaining_space / (line_children.len() as f32 + 1.0);
1233                                    offset_main = extra_gap;
1234                                }
1235                            }
1236                        }
1237
1238                        let mut cursor = offset_main;
1239                        for (child_id, child_size, mut child_constraints) in line_children {
1240                            let child_main = if is_row {
1241                                child_size.width
1242                            } else {
1243                                child_size.height
1244                            };
1245                            let child_cross = if is_row {
1246                                child_size.height
1247                            } else {
1248                                child_size.width
1249                            };
1250                            if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
1251                                if is_row {
1252                                    child_constraints.min_h = line_cross;
1253                                    child_constraints.max_h = line_cross;
1254                                } else {
1255                                    child_constraints.min_w = line_cross;
1256                                    child_constraints.max_w = line_cross;
1257                                }
1258                            }
1259                            let cross_offset = match align_items {
1260                                fission_ir::op::AlignItems::Start
1261                                | fission_ir::op::AlignItems::Stretch => 0.0,
1262                                fission_ir::op::AlignItems::End => {
1263                                    (line_cross - child_cross).max(0.0)
1264                                }
1265                                fission_ir::op::AlignItems::Center => {
1266                                    ((line_cross - child_cross) / 2.0).max(0.0)
1267                                }
1268                                fission_ir::op::AlignItems::Baseline => 0.0,
1269                            };
1270                            let child_origin = if is_row {
1271                                LayoutPoint::new(
1272                                    origin.x + padding[0] + cursor,
1273                                    origin.y + padding[2] + line_cursor + cross_offset,
1274                                )
1275                            } else {
1276                                LayoutPoint::new(
1277                                    origin.x + padding[0] + line_cursor + cross_offset,
1278                                    origin.y + padding[2] + cursor,
1279                                )
1280                            };
1281                            self.layout_node_constraints(
1282                                child_id,
1283                                child_constraints,
1284                                child_origin,
1285                                node_map,
1286                                out,
1287                                constraints_out,
1288                                scroll_source,
1289                                record,
1290                                depth + 1,
1291                            );
1292                            cursor += child_main + gap + extra_gap;
1293                        }
1294
1295                        line_cursor += line_cross + gap;
1296                    }
1297
1298                    if record && !abs_children.is_empty() {
1299                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
1300                        for child_id in abs_children {
1301                            self.layout_node_constraints(
1302                                child_id,
1303                                abs_constraints,
1304                                origin,
1305                                node_map,
1306                                out,
1307                                constraints_out,
1308                                scroll_source,
1309                                record,
1310                                depth + 1,
1311                            );
1312                        }
1313                    }
1314                    content_size = size;
1315                    size
1316                } else {
1317                    struct FlexChildEntry {
1318                        id: NodeId,
1319                        flex: f32,
1320                        size: LayoutSize,
1321                        constraints: BoxConstraints,
1322                        is_flex: bool,
1323                    }
1324                    let mut measured: Vec<FlexChildEntry> = Vec::new();
1325                    let mut total_flex = 0.0f32;
1326                    let mut nonflex_main = 0.0f32;
1327                    let mut max_child_cross = 0.0f32;
1328                    let treat_flex_as_nonflex = !main_bounded;
1329
1330                    for child_id in &flow_children {
1331                        let child = match node_map.get(child_id) {
1332                            Some(c) => *c,
1333                            None => continue,
1334                        };
1335                        let flex = child.flex_grow;
1336                        if flex > 0.0 && !treat_flex_as_nonflex {
1337                            total_flex += flex;
1338                            measured.push(FlexChildEntry {
1339                                id: *child_id,
1340                                flex,
1341                                size: LayoutSize::ZERO,
1342                                constraints: BoxConstraints::loose(0.0, 0.0),
1343                                is_flex: true,
1344                            });
1345                            continue;
1346                        }
1347                        let child_constraints = if is_row {
1348                            let cross =
1349                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
1350                                    && cross_bounded
1351                                {
1352                                    BoxConstraints {
1353                                        min_w: 0.0,
1354                                        max_w: f32::INFINITY,
1355                                        min_h: max_cross,
1356                                        max_h: max_cross,
1357                                    }
1358                                } else {
1359                                    BoxConstraints {
1360                                        min_w: 0.0,
1361                                        max_w: f32::INFINITY,
1362                                        min_h: 0.0,
1363                                        max_h: max_cross,
1364                                    }
1365                                };
1366                            cross
1367                        } else {
1368                            let cross =
1369                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
1370                                    && cross_bounded
1371                                {
1372                                    BoxConstraints {
1373                                        min_w: max_cross,
1374                                        max_w: max_cross,
1375                                        min_h: 0.0,
1376                                        max_h: f32::INFINITY,
1377                                    }
1378                                } else {
1379                                    BoxConstraints {
1380                                        min_w: 0.0,
1381                                        max_w: max_cross,
1382                                        min_h: 0.0,
1383                                        max_h: f32::INFINITY,
1384                                    }
1385                                };
1386                            cross
1387                        };
1388                        let child_size = self.layout_node_constraints(
1389                            *child_id,
1390                            child_constraints,
1391                            LayoutPoint::ZERO,
1392                            node_map,
1393                            out,
1394                            constraints_out,
1395                            scroll_source,
1396                            false,
1397                            depth + 1,
1398                        );
1399                        let child_main = if is_row {
1400                            child_size.width
1401                        } else {
1402                            child_size.height
1403                        };
1404                        let child_cross = if is_row {
1405                            child_size.height
1406                        } else {
1407                            child_size.width
1408                        };
1409                        nonflex_main += child_main;
1410                        max_child_cross = max_child_cross.max(child_cross);
1411                        measured.push(FlexChildEntry {
1412                            id: *child_id,
1413                            flex,
1414                            size: child_size,
1415                            constraints: child_constraints,
1416                            is_flex: false,
1417                        });
1418                    }
1419
1420                    let gap_total = gap * flow_children.len().saturating_sub(1) as f32;
1421                    let remaining = if main_bounded {
1422                        (max_main - nonflex_main - gap_total).max(0.0)
1423                    } else {
1424                        0.0
1425                    };
1426
1427                    for entry in measured.iter_mut().filter(|e| e.is_flex) {
1428                        let flex = entry.flex;
1429                        let allocated = if main_bounded && total_flex > 0.0 {
1430                            remaining * (flex / total_flex)
1431                        } else {
1432                            0.0
1433                        };
1434                        let child_constraints = if is_row {
1435                            let cross =
1436                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
1437                                    && cross_bounded
1438                                {
1439                                    BoxConstraints {
1440                                        min_w: allocated,
1441                                        max_w: allocated,
1442                                        min_h: max_cross,
1443                                        max_h: max_cross,
1444                                    }
1445                                } else {
1446                                    BoxConstraints {
1447                                        min_w: allocated,
1448                                        max_w: allocated,
1449                                        min_h: 0.0,
1450                                        max_h: max_cross,
1451                                    }
1452                                };
1453                            cross
1454                        } else {
1455                            let cross =
1456                                if matches!(align_items, fission_ir::op::AlignItems::Stretch)
1457                                    && cross_bounded
1458                                {
1459                                    BoxConstraints {
1460                                        min_w: max_cross,
1461                                        max_w: max_cross,
1462                                        min_h: allocated,
1463                                        max_h: allocated,
1464                                    }
1465                                } else {
1466                                    BoxConstraints {
1467                                        min_w: 0.0,
1468                                        max_w: max_cross,
1469                                        min_h: allocated,
1470                                        max_h: allocated,
1471                                    }
1472                                };
1473                            cross
1474                        };
1475                        let child_size = self.layout_node_constraints(
1476                            entry.id,
1477                            child_constraints,
1478                            LayoutPoint::ZERO,
1479                            node_map,
1480                            out,
1481                            constraints_out,
1482                            scroll_source,
1483                            false,
1484                            depth + 1,
1485                        );
1486                        let child_cross = if is_row {
1487                            child_size.height
1488                        } else {
1489                            child_size.width
1490                        };
1491                        max_child_cross = max_child_cross.max(child_cross);
1492                        entry.size = child_size;
1493                        entry.constraints = child_constraints;
1494                    }
1495
1496                    let final_children_main: f32 = measured
1497                        .iter()
1498                        .map(|entry| {
1499                            if is_row {
1500                                entry.size.width
1501                            } else {
1502                                entry.size.height
1503                            }
1504                        })
1505                        .sum();
1506                    
1507                    let mut container_main = if main_bounded && *flex_grow > 0.0 {
1508                        max_main
1509                    } else {
1510                        final_children_main + gap_total
1511                    };
1512                    container_main = container_main.max(min_main);
1513                    
1514                    if main_bounded && final_children_main + gap_total > max_main {
1515                        // SHRINK logic
1516                        let mut total_shrink_scaled = 0.0f32;
1517                        for entry in &measured {
1518                            let child = node_map.get(&entry.id).unwrap();
1519                            let main_size = if is_row { entry.size.width } else { entry.size.height };
1520                            total_shrink_scaled += main_size * child.flex_shrink;
1521                        }
1522
1523                        if total_shrink_scaled > 0.0 {
1524                            let overflow = (final_children_main + gap_total) - max_main;
1525                            for entry in &mut measured {
1526                                let child = node_map.get(&entry.id).unwrap();
1527                                let main_size = if is_row { entry.size.width } else { entry.size.height };
1528                                let shrink_amount = (main_size * child.flex_shrink / total_shrink_scaled) * overflow;
1529                                // Don't shrink below a reasonable minimum. Items with
1530                                // flex_shrink > 0 can shrink but not to zero - preserve at
1531                                // least a small fraction of their natural size.
1532                                let floor = if child.flex_shrink > 0.0 {
1533                                    // Check for explicit min/fixed dimension
1534                                    let explicit_min = match &child.op {
1535                                        LayoutOp::Box { min_width, min_height, height, width, .. } => {
1536                                            if is_row {
1537                                                min_width.or(*width).unwrap_or(0.0)
1538                                            } else {
1539                                                min_height.or(*height).unwrap_or(0.0)
1540                                            }
1541                                        }
1542                                        _ => 0.0,
1543                                    };
1544                                    explicit_min
1545                                } else {
1546                                    main_size // flex_shrink == 0 means don't shrink at all
1547                                };
1548                                let new_main = (main_size - shrink_amount).max(floor);
1549                                
1550                                let mut child_constraints = entry.constraints;
1551                                if is_row {
1552                                    child_constraints.min_w = new_main;
1553                                    child_constraints.max_w = new_main;
1554                                } else {
1555                                    child_constraints.min_h = new_main;
1556                                    child_constraints.max_h = new_main;
1557                                }
1558                                let new_size = self.layout_node_constraints(
1559                                    entry.id,
1560                                    child_constraints,
1561                                    LayoutPoint::ZERO,
1562                                    node_map,
1563                                    out,
1564                                    constraints_out,
1565                                    scroll_source,
1566                                    false,
1567                                    depth + 1,
1568                                );
1569                                entry.size = new_size;
1570                                entry.constraints = child_constraints;
1571                            }
1572                        }
1573                    }
1574
1575                    let mut container_cross = max_child_cross.max(min_cross);
1576                    let size = if is_row {
1577                        local.constrain(LayoutSize::new(
1578                            container_main + padding[0] + padding[1],
1579                            container_cross + padding[2] + padding[3],
1580                        ))
1581                    } else {
1582                        local.constrain(LayoutSize::new(
1583                            container_cross + padding[0] + padding[1],
1584                            container_main + padding[2] + padding[3],
1585                        ))
1586                    };
1587
1588                    let inner_main = if is_row {
1589                        size.width - padding[0] - padding[1]
1590                    } else {
1591                        size.height - padding[2] - padding[3]
1592                    };
1593                    let inner_cross = if is_row {
1594                        size.height - padding[2] - padding[3]
1595                    } else {
1596                        size.width - padding[0] - padding[1]
1597                    };
1598                    
1599                    let final_children_main: f32 = measured
1600                        .iter()
1601                        .map(|entry| {
1602                            if is_row {
1603                                entry.size.width
1604                            } else {
1605                                entry.size.height
1606                            }
1607                        })
1608                        .sum();
1609
1610                    let mut remaining_space =
1611                        (inner_main - final_children_main - gap_total).max(0.0);
1612                    let mut extra_gap = 0.0;
1613                    let mut offset_main = 0.0;
1614                    match justify_content {
1615                        fission_ir::op::JustifyContent::Start => {}
1616                        fission_ir::op::JustifyContent::End => offset_main = remaining_space,
1617                        fission_ir::op::JustifyContent::Center => {
1618                            offset_main = remaining_space / 2.0
1619                        }
1620                        fission_ir::op::JustifyContent::SpaceBetween => {
1621                            if measured.len() > 1 {
1622                                extra_gap = remaining_space / (measured.len() as f32 - 1.0);
1623                            }
1624                        }
1625                        fission_ir::op::JustifyContent::SpaceAround => {
1626                            if !measured.is_empty() {
1627                                extra_gap = remaining_space / measured.len() as f32;
1628                                offset_main = extra_gap / 2.0;
1629                            }
1630                        }
1631                        fission_ir::op::JustifyContent::SpaceEvenly => {
1632                            if !measured.is_empty() {
1633                                extra_gap = remaining_space / (measured.len() as f32 + 1.0);
1634                                offset_main = extra_gap;
1635                            }
1636                        }
1637                    }
1638
1639                    let mut cursor = offset_main;
1640                    for entry in measured {
1641                        let child_main = if is_row {
1642                            entry.size.width
1643                        } else {
1644                            entry.size.height
1645                        };
1646                        let child_cross = if is_row {
1647                            entry.size.height
1648                        } else {
1649                            entry.size.width
1650                        };
1651                        let cross_offset = match align_items {
1652                            fission_ir::op::AlignItems::Start
1653                            | fission_ir::op::AlignItems::Stretch => 0.0,
1654                            fission_ir::op::AlignItems::End => (inner_cross - child_cross).max(0.0),
1655                            fission_ir::op::AlignItems::Center => {
1656                                ((inner_cross - child_cross) / 2.0).max(0.0)
1657                            }
1658                            fission_ir::op::AlignItems::Baseline => 0.0,
1659                        };
1660                        let child_origin = if is_row {
1661                            LayoutPoint::new(
1662                                origin.x + padding[0] + cursor,
1663                                origin.y + padding[2] + cross_offset,
1664                            )
1665                        } else {
1666                            LayoutPoint::new(
1667                                origin.x + padding[0] + cross_offset,
1668                                origin.y + padding[2] + cursor,
1669                            )
1670                        };
1671                        
1672                        let mut child_constraints = entry.constraints;
1673                        if matches!(align_items, fission_ir::op::AlignItems::Stretch) {
1674                            // Only stretch children that don't have an explicit cross-axis size.
1675                            let child_node = node_map.get(&entry.id);
1676                            let has_explicit_cross = child_node.map(|n| match &n.op {
1677                                LayoutOp::Box { width, height, .. } => {
1678                                    if is_row { height.is_some() } else { width.is_some() }
1679                                }
1680                                _ => false,
1681                            }).unwrap_or(false);
1682                            if !has_explicit_cross {
1683                                if is_row {
1684                                    child_constraints.min_h = inner_cross;
1685                                    child_constraints.max_h = inner_cross;
1686                                } else {
1687                                    child_constraints.min_w = inner_cross;
1688                                    child_constraints.max_w = inner_cross;
1689                                }
1690                            }
1691                        }
1692
1693                        self.layout_node_constraints(
1694                            entry.id,
1695                            child_constraints,
1696                            child_origin,
1697                            node_map,
1698                            out,
1699                            constraints_out,
1700                            scroll_source,
1701                            record,
1702                            depth + 1,
1703                        );
1704                        cursor += child_main + gap + extra_gap;
1705                    }
1706
1707                    if record && !abs_children.is_empty() {
1708                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
1709                        for child_id in abs_children {
1710                            self.layout_node_constraints(
1711                                child_id,
1712                                abs_constraints,
1713                                origin,
1714                                node_map,
1715                                out,
1716                                constraints_out,
1717                                scroll_source,
1718                                record,
1719                                depth + 1,
1720                            );
1721                        }
1722                    }
1723                    content_size = size;
1724                    size
1725                }
1726            }
1727            LayoutOp::Grid {
1728                columns,
1729                rows,
1730                column_gap,
1731                row_gap,
1732                padding,
1733            } => {
1734                let gap_x = column_gap.unwrap_or(0.0);
1735                let gap_y = row_gap.unwrap_or(0.0);
1736                let inner = constraints.deflate(*padding);
1737                let bounded_w = inner.is_width_bounded();
1738                let bounded_h = inner.is_height_bounded();
1739                let available_w = if bounded_w { inner.max_w } else { 0.0 };
1740                let available_h = if bounded_h { inner.max_h } else { 0.0 };
1741
1742                let col_count = columns.len().max(1);
1743                let mut col_widths = vec![0.0f32; col_count];
1744                let mut fr_total = 0.0f32;
1745                let mut fixed_total = 0.0f32;
1746                for (i, track) in columns.iter().enumerate() {
1747                    match track {
1748                        GridTrack::Points(p) => {
1749                            col_widths[i] = *p;
1750                            fixed_total += *p;
1751                        }
1752                        GridTrack::Percent(p) => {
1753                            let w = if bounded_w {
1754                                available_w * (*p / 100.0)
1755                            } else {
1756                                0.0
1757                            };
1758                            col_widths[i] = w;
1759                            fixed_total += w;
1760                        }
1761                        GridTrack::Fr(f) => fr_total += *f,
1762                        _ => {}
1763                    }
1764                }
1765                if fr_total > 0.0 && bounded_w {
1766                    let remaining = (available_w - fixed_total - gap_x * (col_count.saturating_sub(1) as f32)).max(0.0);
1767                    for (i, track) in columns.iter().enumerate() {
1768                        if let GridTrack::Fr(f) = track {
1769                            col_widths[i] = remaining * (*f / fr_total);
1770                        }
1771                    }
1772                }
1773
1774                let child_count = flow_children.len();
1775                let row_count = if rows.is_empty() {
1776                    (child_count + col_count - 1) / col_count
1777                } else {
1778                    rows.len()
1779                };
1780                let mut row_heights = vec![0.0f32; row_count.max(1)];
1781
1782                if !rows.is_empty() {
1783                    let mut row_fr_total = 0.0f32;
1784                    let mut row_fixed_total = 0.0f32;
1785                    for (i, track) in rows.iter().enumerate() {
1786                        if i >= row_heights.len() { break; }
1787                        match track {
1788                            GridTrack::Points(p) => {
1789                                row_heights[i] = *p;
1790                                row_fixed_total += *p;
1791                            }
1792                            GridTrack::Percent(p) => {
1793                                let h = if bounded_h { available_h * (*p / 100.0) } else { 0.0 };
1794                                row_heights[i] = h;
1795                                row_fixed_total += h;
1796                            }
1797                            GridTrack::Fr(f) => row_fr_total += *f,
1798                            _ => {}
1799                        }
1800                    }
1801                    if row_fr_total > 0.0 && bounded_h {
1802                        let remaining = (available_h - row_fixed_total - gap_y * (row_heights.len().saturating_sub(1) as f32)).max(0.0);
1803                        for (i, track) in rows.iter().enumerate() {
1804                            if let GridTrack::Fr(f) = track {
1805                                row_heights[i] = remaining * (*f / row_fr_total);
1806                            }
1807                        }
1808                    }
1809                }
1810
1811                let mut cell_assignments = Vec::new();
1812                let mut auto_row = 0;
1813                let mut auto_col = 0;
1814
1815                for child_id in &flow_children {
1816                    let child = node_map.get(child_id).unwrap();
1817                    let (row, col) = if let LayoutOp::GridItem { row_start, col_start, .. } = &child.op {
1818                        let r = match row_start {
1819                            fission_ir::op::GridPlacement::Line(l) => (*l as usize).saturating_sub(1),
1820                            _ => auto_row,
1821                        };
1822                        let c = match col_start {
1823                            fission_ir::op::GridPlacement::Line(l) => (*l as usize).saturating_sub(1),
1824                            _ => auto_col,
1825                        };
1826                        (r, c)
1827                    } else {
1828                        let res = (auto_row, auto_col);
1829                        auto_col += 1;
1830                        if auto_col >= col_count {
1831                            auto_col = 0;
1832                            auto_row += 1;
1833                        }
1834                        res
1835                    };
1836                    cell_assignments.push((*child_id, row, col));
1837                }
1838
1839                for (child_id, row, col) in &cell_assignments {
1840                    if *row >= row_heights.len() || *col >= col_widths.len() { continue; }
1841                    let cell_w = col_widths[*col];
1842                    let cell_constraints = BoxConstraints {
1843                        min_w: cell_w,
1844                        max_w: cell_w,
1845                        min_h: 0.0,
1846                        max_h: if row_heights[*row] > 0.0 { row_heights[*row] } else { f32::INFINITY },
1847                    };
1848                    let child_size = self.layout_node_constraints(*child_id, cell_constraints, LayoutPoint::ZERO, node_map, out, constraints_out, scroll_source, false, depth + 1);
1849                    if row_heights[*row] == 0.0 {
1850                        row_heights[*row] = child_size.height;
1851                    } else {
1852                        row_heights[*row] = row_heights[*row].max(child_size.height);
1853                    }
1854                }
1855
1856                let grid_w: f32 = col_widths.iter().sum::<f32>() + gap_x * (col_count.saturating_sub(1) as f32);
1857                let grid_h: f32 = row_heights.iter().sum::<f32>() + gap_y * (row_heights.len().saturating_sub(1) as f32);
1858                let size = constraints.constrain(LayoutSize::new(grid_w + padding[0] + padding[1], grid_h + padding[2] + padding[3]));
1859
1860                if record {
1861                    let padding_origin_x = origin.x + padding[0];
1862                    let padding_origin_y = origin.y + padding[2];
1863                    for (child_id, row, col) in &cell_assignments {
1864                        if *row >= row_heights.len() || *col >= col_widths.len() { continue; }
1865                        let mut cell_x = padding_origin_x;
1866                        for i in 0..*col { cell_x += col_widths[i] + gap_x; }
1867                        let mut cell_y = padding_origin_y;
1868                        for i in 0..*row { cell_y += row_heights[i] + gap_y; }
1869                        let cell_w = col_widths[*col];
1870                        let cell_h = row_heights[*row];
1871                        let child_constraints = BoxConstraints { min_w: cell_w, max_w: cell_w, min_h: cell_h, max_h: cell_h };
1872                        self.layout_node_constraints(*child_id, child_constraints, LayoutPoint::new(cell_x, cell_y), node_map, out, constraints_out, scroll_source, record, depth + 1);
1873                    }
1874                }
1875
1876                if record && !abs_children.is_empty() {
1877                    let abs_constraints = BoxConstraints::loose(size.width, size.height);
1878                    for child_id in abs_children {
1879                        self.layout_node_constraints(child_id, abs_constraints, origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
1880                    }
1881                }
1882                content_size = size;
1883                size
1884            }
1885            LayoutOp::GridItem { .. } => {
1886                let mut child_size = LayoutSize::ZERO;
1887                if let Some(child_id) = node.children_ids.first() {
1888                    child_size = self.layout_node_constraints(*child_id, constraints, origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
1889                }
1890                content_size = child_size;
1891                constraints.constrain(child_size)
1892            }
1893            LayoutOp::Scroll { direction, width, height, min_width, max_width, min_height, max_height, padding, .. } => {
1894                let mut local = constraints.apply_min_max(*min_width, *max_width, *min_height, *max_height);
1895                local = local.tighten(*width, *height);
1896                let is_horizontal = matches!(direction, FlexDirection::Row);
1897                let mut child_constraints = local.deflate(*padding).loosen();
1898                if is_horizontal { child_constraints.max_w = f32::INFINITY; } else { child_constraints.max_h = f32::INFINITY; }
1899                let mut child_size = LayoutSize::ZERO;
1900                if let Some(child_id) = flow_children.first() {
1901                    child_size = self.layout_node_constraints(*child_id, child_constraints, LayoutPoint::ZERO, node_map, out, constraints_out, scroll_source, false, depth + 1);
1902                }
1903                let size = local.constrain(LayoutSize::new(child_size.width + padding[0] + padding[1], child_size.height + padding[2] + padding[3]));
1904                if record {
1905                    if let Some(child_id) = flow_children.first() {
1906                        self.layout_node_constraints(*child_id, child_constraints, LayoutPoint::new(origin.x + padding[0], origin.y + padding[2]), node_map, out, constraints_out, scroll_source, record, depth + 1);
1907                    }
1908                    if !abs_children.is_empty() {
1909                        let abs_constraints = BoxConstraints::loose(size.width, size.height);
1910                        for child_id in abs_children {
1911                            self.layout_node_constraints(child_id, abs_constraints, origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
1912                        }
1913                    }
1914                }
1915                content_size = child_size;
1916                size
1917            }
1918            LayoutOp::Align => {
1919                let child_constraints = BoxConstraints::loose(constraints.max_w, constraints.max_h);
1920                let mut child_size = LayoutSize::ZERO;
1921                if let Some(child_id) = flow_children.first() {
1922                    child_size = self.layout_node_constraints(*child_id, child_constraints, LayoutPoint::ZERO, node_map, out, constraints_out, scroll_source, false, depth + 1);
1923                }
1924                let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
1925                    constraints.constrain(LayoutSize::new(if constraints.is_width_bounded() { constraints.max_w } else { child_size.width }, if constraints.is_height_bounded() { constraints.max_h } else { child_size.height }))
1926                } else { child_size };
1927                if let Some(child_id) = flow_children.first() {
1928                    let dx = ((size.width - child_size.width) / 2.0).max(0.0);
1929                    let dy = ((size.height - child_size.height) / 2.0).max(0.0);
1930                    self.layout_node_constraints(*child_id, child_constraints, LayoutPoint::new(origin.x + dx, origin.y + dy), node_map, out, constraints_out, scroll_source, record, depth + 1);
1931                }
1932                if record && !abs_children.is_empty() {
1933                    let abs_constraints = BoxConstraints::loose(size.width, size.height);
1934                    for child_id in abs_children {
1935                        self.layout_node_constraints(child_id, abs_constraints, origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
1936                    }
1937                }
1938                content_size = child_size;
1939                size
1940            }
1941            LayoutOp::ZStack => {
1942                let mut max_child = LayoutSize::ZERO;
1943                for child_id in &flow_children {
1944                    let child_size = self.layout_node_constraints(*child_id, BoxConstraints::loose(constraints.max_w, constraints.max_h), LayoutPoint::ZERO, node_map, out, constraints_out, scroll_source, false, depth + 1);
1945                    max_child.width = max_child.width.max(child_size.width);
1946                    max_child.height = max_child.height.max(child_size.height);
1947                }
1948                let size = if constraints.is_width_bounded() || constraints.is_height_bounded() {
1949                    constraints.constrain(LayoutSize::new(if constraints.is_width_bounded() { constraints.max_w } else { max_child.width }, if constraints.is_height_bounded() { constraints.max_h } else { max_child.height }))
1950                } else { max_child };
1951                for child_id in &flow_children {
1952                    let child_constraints = BoxConstraints::loose(size.width, size.height);
1953                    let child_origin = LayoutPoint::new(origin.x, origin.y);
1954                    self.layout_node_constraints(*child_id, child_constraints, child_origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
1955                }
1956                if record && !abs_children.is_empty() {
1957                    let abs_constraints = BoxConstraints::loose(size.width, size.height);
1958                    for child_id in abs_children {
1959                        self.layout_node_constraints(child_id, abs_constraints, origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
1960                    }
1961                }
1962                content_size = size;
1963                size
1964            }
1965            LayoutOp::Positioned { top, left, bottom, right, width, height } => {
1966                let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
1967                let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
1968                let size = constraints.constrain(LayoutSize::new(target_w, target_h));
1969                let mut child_constraints = BoxConstraints::loose(size.width, size.height);
1970                if let (Some(l), Some(r)) = (left, right) {
1971                    let w = (size.width - l - r).max(0.0);
1972                    child_constraints = child_constraints.tighten(Some(w), None);
1973                }
1974                if let (Some(t), Some(b)) = (top, bottom) {
1975                    let h = (size.height - t - b).max(0.0);
1976                    child_constraints = child_constraints.tighten(None, Some(h));
1977                }
1978                child_constraints = child_constraints.tighten(*width, *height);
1979                if let Some(child_id) = node.children_ids.first() {
1980                    let child_size = self.layout_node_constraints(*child_id, child_constraints, LayoutPoint::ZERO, node_map, out, constraints_out, scroll_source, false, depth + 1);
1981                    let x = left.unwrap_or_else(|| { right.map(|r| (size.width - r - child_size.width).max(0.0)).unwrap_or(0.0) });
1982                    let y = top.unwrap_or_else(|| { bottom.map(|b| (size.height - b - child_size.height).max(0.0)).unwrap_or(0.0) });
1983                    self.layout_node_constraints(*child_id, child_constraints, LayoutPoint::new(origin.x + x, origin.y + y), node_map, out, constraints_out, scroll_source, record, depth + 1);
1984                }
1985                content_size = size;
1986                size
1987            }
1988            LayoutOp::Embed { width, height, .. } => {
1989                let local = constraints.tighten(*width, *height);
1990                let w = if local.is_width_bounded() { local.max_w } else { local.min_w };
1991                let h = if local.is_height_bounded() { local.max_h } else { local.min_h };
1992                let size = local.constrain(LayoutSize::new(w, h));
1993                content_size = size;
1994                size
1995            }
1996            LayoutOp::AbsoluteFill => {
1997                let target_w = finite_or(constraints.max_w, finite_or(constraints.min_w, 0.0));
1998                let target_h = finite_or(constraints.max_h, finite_or(constraints.min_h, 0.0));
1999                let size = constraints.constrain(LayoutSize::new(target_w, target_h));
2000                for child_id in &node.children_ids {
2001                    self.layout_node_constraints(*child_id, BoxConstraints::tight(size), origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
2002                }
2003                content_size = size;
2004                size
2005            }
2006            LayoutOp::Transform { .. } | LayoutOp::Clip { .. } => {
2007                let mut child_size = LayoutSize::ZERO;
2008                if let Some(child_id) = node.children_ids.first() {
2009                    child_size = self.layout_node_constraints(*child_id, constraints, origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
2010                }
2011                content_size = child_size;
2012                constraints.constrain(child_size)
2013            }
2014            LayoutOp::Flyout { anchor, content } => {
2015                let loose = BoxConstraints::loose(
2016                    if constraints.is_width_bounded() { constraints.max_w } else { f32::INFINITY },
2017                    if constraints.is_height_bounded() { constraints.max_h } else { f32::INFINITY },
2018                );
2019                let mut child_size = LayoutSize::ZERO;
2020                for child_id in &node.children_ids {
2021                    child_size = self.layout_node_constraints(*child_id, loose, origin, node_map, out, constraints_out, scroll_source, false, depth + 1);
2022                }
2023                if record {
2024                    let anchor_rect = out.get(anchor).map(|g| g.rect);
2025                    let place_x = anchor_rect.map(|r| r.x()).unwrap_or(origin.x);
2026                    let place_y = anchor_rect.map(|r| r.y() + r.height()).unwrap_or(origin.y);
2027                    for child_id in &node.children_ids {
2028                        self.layout_node_constraints(*child_id, loose, LayoutPoint::new(place_x, place_y), node_map, out, constraints_out, scroll_source, record, depth + 1);
2029                    }
2030                }
2031                content_size = child_size;
2032                child_size
2033            }
2034            _ => {
2035                let mut child_size = LayoutSize::ZERO;
2036                if !node.children_ids.is_empty() {
2037                    for child_id in &node.children_ids {
2038                        child_size = self.layout_node_constraints(*child_id, constraints, origin, node_map, out, constraints_out, scroll_source, record, depth + 1);
2039                    }
2040                }
2041                content_size = child_size;
2042                constraints.constrain(child_size)
2043            }
2044        };
2045
2046        if let Some(runs) = &node.rich_text {
2047            if let Some(measurer) = &self.measurer {
2048                let node_max_w = match &node.op {
2049                    LayoutOp::Box { max_width, .. } => *max_width,
2050                    _ => None,
2051                };
2052                let avail_w = {
2053                    let from_constraints = if constraints.is_width_bounded() {
2054                        Some(constraints.max_w)
2055                    } else {
2056                        None
2057                    };
2058                    match (from_constraints, node_max_w) {
2059                        (Some(c), Some(m)) => Some(c.min(m)),
2060                        (Some(c), None) => Some(c),
2061                        (None, Some(m)) => Some(m),
2062                        (None, None) => None,
2063                    }
2064                };
2065                let (mw, mh) = if runs.len() == 1 {
2066                    let run = &runs[0];
2067                    measurer.measure(&run.text, run.style.font_size, avail_w)
2068                } else {
2069                    measurer.measure_rich_text(runs, avail_w)
2070                };
2071                let text_content = LayoutSize::new(mw, mh);
2072                let measured = constraints.constrain(text_content);
2073                if node.children_ids.is_empty() {
2074                    content_size = text_content;
2075                    return self.record_geometry(node_id, origin, measured, text_content, out, record);
2076                }
2077                content_size.width = content_size.width.max(text_content.width);
2078                content_size.height = content_size.height.max(text_content.height);
2079            }
2080        }
2081
2082        self.record_geometry(node_id, origin, size, content_size, out, record)
2083    }
2084
2085    fn record_geometry(
2086        &self,
2087        node_id: NodeId,
2088        origin: LayoutPoint,
2089        size: LayoutSize,
2090        content_size: LayoutSize,
2091        out: &mut HashMap<NodeId, LayoutNodeGeometry>,
2092        record: bool,
2093    ) -> LayoutSize {
2094        let mut rect_origin = origin;
2095        let mut rect_size = size;
2096        let mut rect_content = content_size;
2097        let mut had_non_finite = false;
2098
2099        if !rect_origin.x.is_finite() { rect_origin.x = 0.0; had_non_finite = true; }
2100        if !rect_origin.y.is_finite() { rect_origin.y = 0.0; had_non_finite = true; }
2101        if !rect_size.width.is_finite() { rect_size.width = 0.0; had_non_finite = true; }
2102        if !rect_size.height.is_finite() { rect_size.height = 0.0; had_non_finite = true; }
2103        if !rect_content.width.is_finite() { rect_content.width = 0.0; had_non_finite = true; }
2104        if !rect_content.height.is_finite() { rect_content.height = 0.0; had_non_finite = true; }
2105
2106        if had_non_finite {
2107            diag::emit(diag::DiagCategory::Invariants, diag::DiagLevel::Error, diag::DiagEventKind::InvariantViolation {
2108                kind: "non_finite_layout".into(),
2109                node: Some(node_id.as_u128()),
2110                details: format!("origin=({:.2},{:.2}) size=({:.2},{:.2}) content=({:.2},{:.2})", origin.x, origin.y, size.width, size.height, content_size.width, content_size.height),
2111                dump_ref: None,
2112            });
2113        }
2114
2115        if record {
2116            let rect = LayoutRect::new(rect_origin.x, rect_origin.y, rect_size.width, rect_size.height);
2117            out.insert(node_id, LayoutNodeGeometry { rect, content_size: rect_content });
2118        }
2119        rect_size
2120    }
2121}