Skip to main content

azul_layout/solver3/
sizing.rs

1//! Intrinsic and used size calculations for layout nodes
2
3use crate::debug_log;
4use std::{
5    collections::BTreeSet,
6    sync::Arc,
7};
8
9use azul_core::{
10    dom::{FormattingContext, NodeId, NodeType},
11    geom::LogicalSize,
12    resources::RendererResources,
13    styled_dom::{StyledDom, StyledNodeState},
14};
15use azul_css::{
16    css::CssPropertyValue,
17    props::{
18        basic::PixelValue,
19        layout::{LayoutDisplay, LayoutFlexDirection, LayoutFlexWrap, LayoutFloat, LayoutHeight, LayoutPosition, LayoutWidth, LayoutWritingMode},
20        property::{CssProperty, CssPropertyType},
21    },
22    LayoutDebugMessage,
23};
24use rust_fontconfig::FcFontCache;
25
26#[cfg(feature = "text_layout")]
27use crate::text3;
28use crate::{
29    font::parsed::ParsedFont,
30    font_traits::{
31        AvailableSpace, FontLoaderTrait, FontManager, ImageSource, InlineContent, InlineImage,
32        InlineShape, LayoutCache, LayoutFragment, ObjectFit, ParsedFontTrait, ShapeDefinition,
33        StyleProperties, UnifiedConstraints,
34    },
35    solver3::{
36        fc::split_text_for_whitespace,
37        geometry::{BoxProps, IntrinsicSizes, WritingModeContext},
38        getters::{
39            get_css_box_sizing, get_css_height, get_css_width, get_display_property,
40            get_direction_property, get_element_font_size, get_flex_direction, get_float,
41            get_style_properties, get_text_orientation_property, get_writing_mode, MultiValue,
42        },
43        layout_tree::{LayoutNodeHot, LayoutTree, get_display_type},
44        positioning::get_position_type,
45        LayoutContext, LayoutError, Result,
46    },
47};
48
49const FALLBACK_MIN_CONTENT_WIDTH: f32 = 100.0;
50const FALLBACK_MAX_CONTENT_WIDTH: f32 = 300.0;
51const FALLBACK_MIN_CONTENT_HEIGHT: f32 = 20.0;
52const FALLBACK_MAX_CONTENT_HEIGHT: f32 = 20.0;
53
54/// Resolves a min/max sizing `PixelValue`, falling back to percentage-against-
55/// containing-block resolution (with box-model adjustment) when the value is a
56/// percentage rather than an absolute length.
57///
58/// `is_horizontal` selects which axis of `box_props` (left/right vs top/bottom)
59/// is subtracted during percentage resolution.
60fn resolve_px_with_box_model(
61    px: &PixelValue,
62    containing: f32,
63    box_props: &BoxProps,
64    is_horizontal: bool,
65    em: f32,
66    rem: f32,
67) -> Option<f32> {
68    if let Some(v) = super::calc::resolve_pixel_value_no_percent(px, em, rem) {
69        return Some(v);
70    }
71
72    let percent = px.to_percent()?;
73    let (margin, border, padding) = if is_horizontal {
74        (
75            (box_props.margin.left, box_props.margin.right),
76            (box_props.border.left, box_props.border.right),
77            (box_props.padding.left, box_props.padding.right),
78        )
79    } else {
80        (
81            (box_props.margin.top, box_props.margin.bottom),
82            (box_props.border.top, box_props.border.bottom),
83            (box_props.padding.top, box_props.padding.bottom),
84        )
85    };
86    Some(resolve_percentage_with_box_model(
87        containing,
88        percent.get(),
89        margin,
90        border,
91        padding,
92    ))
93}
94
95/// Resolves a percentage value against the containing block dimension.
96///
97/// Per CSS 2.1 Section 10.2, percentages resolve directly against the containing
98/// block's width or height. The margin/border/padding parameters are accepted for
99/// call-site convenience but are intentionally unused — percentage resolution does
100/// not subtract box-model extras in content-box sizing.
101///
102/// Returns `(containing_block_dimension * percentage).max(0.0)`.
103// +spec:containing-block:43c719 - percentages resolved against containing block width/height
104// +spec:containing-block:723eee - Percentages specify sizing with respect to the containing block
105// +spec:containing-block:8ad6f4 - Percentage resolution against containing block (editorial note: transferred percentages)
106// +spec:containing-block:257f3b - Block-axis percentages resolve against containing block size
107// +spec:containing-block:f1344e - percentage min/max-width resolved against containing block width; negative CB width yields zero
108#[must_use] pub fn resolve_percentage_with_box_model(
109    containing_block_dimension: f32,
110    percentage: f32,
111    _margins: (f32, f32),
112    _borders: (f32, f32),
113    _paddings: (f32, f32),
114) -> f32 {
115    // +spec:containing-block:b3388b - percentage resolved against containing block size without re-resolution (css-sizing-3 §5.2.1)
116    // CSS 2.1 Section 10.2: percentages resolve against containing block,
117    // not available space after margins/borders/padding
118    (containing_block_dimension * percentage).max(0.0)
119}
120
121/// Returns true if the DOM subtree rooted at `dom_id` contains any `NodeType::Text`.
122///
123/// Used when deciding whether a `FormattingContext::Inline` node should measure
124/// its inline content (it acts as an IFC root when nested inlines eventually
125/// hold text) versus returning zero (pure inline wrapper with no text reaches).
126fn subtree_contains_text(styled_dom: &StyledDom, dom_id: NodeId) -> bool {
127    let node_hierarchy = styled_dom.node_hierarchy.as_container();
128    let node_data = styled_dom.node_data.as_container();
129    if matches!(node_data[dom_id].get_node_type(), NodeType::Text(_)) {
130        return true;
131    }
132    dom_id
133        .az_children(&node_hierarchy)
134        .any(|child| subtree_contains_text(styled_dom, child))
135}
136
137/// Phase 2a: Calculate intrinsic sizes (bottom-up pass)
138/// // +spec:display-contents:f12d4e - intrinsic sizing: size determined by contents, not context
139// [g71 TEST] #[inline(never)] — RELIABLE bisection (g70, markers in free band) showed new_tree drops
140// 2→0 RIGHT BEFORE this call. It's currently INLINED into layout_document (absent from the lift log),
141// so its frame/entry isn't a separate SP-wrapped @sub_ call. Forcing it OUT makes the call a wrapped
142// @sub_ → enforce_sp_preservation save/restores SP around it. If new_tree survives (sizingEntry=2),
143// the inlined entry/frame-setup was mis-lifting SP. (g60's inline(always) was a no-op — already inlined.)
144#[inline(never)]
145#[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
146/// # Errors
147///
148/// Returns a `LayoutError` if intrinsic sizing fails.
149pub fn calculate_intrinsic_sizes<T: ParsedFontTrait>(
150    ctx: &mut LayoutContext<'_, T>,
151    tree: &mut LayoutTree,
152    text_cache: &mut LayoutCache,
153    dirty_nodes: &BTreeSet<usize>,
154) -> Result<()> {
155    // [az-diag g59 REVERT] RELIABLE field-access bracket (pointer CASTS mis-lift to 0 — g58 proved
156    // it; use tree.nodes.len() which is reliable). 0x407B0 = nodes.len at ENTRY. If 2 here but
157    // 0x40734 (line ~142, after compute_dirty_ancestor_closure + calculator) reads 0, the corruption
158    // is in 121-142. compute_dirty_ancestor_closure RETURNS a HashSet by sret — prime suspect:
159    // sret-slot overlapping new_tree, or the hashbrown empty-map bug. 0x407B4 (post-compute_dirty)
160    // isolates compute_dirty vs calculator-creation.
161    unsafe { crate::az_mark(0x607B0_u32, (tree.nodes.len() as u32)); }
162    if dirty_nodes.is_empty() {
163        return Ok(());
164    }
165
166    debug_log!(ctx, "Starting intrinsic size calculation");
167    // Pre-compute the "ancestor closure" of dirty_nodes: every dirty
168    // node AND each of its ancestors up to root. A node not in this
169    // set (and whose `intrinsic_sizes` is already populated) can
170    // reuse its cached intrinsic — we skip its entire subtree walk.
171    // Before this, `calculate_intrinsic_recursive` walked the full
172    // tree from root regardless, costing ~2 ms per warm render on
173    // excel.html even when only 3 nodes were actually dirty.
174    let dirty_closure = compute_dirty_ancestor_closure(tree, dirty_nodes);
175    // [az-diag g59 REVERT] 0x407B4 = nodes.len AFTER compute_dirty_ancestor_closure (its HashSet sret).
176    unsafe { crate::az_mark(0x607B4_u32, (tree.nodes.len() as u32)); }
177
178    let mut calculator = IntrinsicSizeCalculator::new(ctx, text_cache);
179    calculator.dirty_closure = Some(dirty_closure);
180    // Fix C (re-enabled §58 Win #3): skip intrinsic computation for subtrees
181    // whose values will never be consumed. `tree.subtree_needs_intrinsic` is a
182    // static-DOM bitmap precomputed at tree-build time — true if this node or
183    // any descendant establishes a shrink-to-fit context. When both the
184    // caller and the subtree are non-STF, no one reads the intrinsic, so the
185    // whole descent is pure waste.
186    //
187    // The previous attempt (7667d13e, reverted in bd9ad36d) wrote default
188    // (zero) intrinsics and broke auto-height rendering because
189    // calculate_used_size_for_node read intrinsic.max_content_height as the
190    // height:auto fallback. 97c3d3db refactored that dependency away: for
191    // block-level auto-height, used_size.height is 0 pre-layout and
192    // apply_content_based_height fills it from the laid-out content size.
193    // With that gone, skipping intrinsic is safe.
194    // [az-diag g53 REVERT] DECISIVE: is the lifted LayoutTree itself empty/broken? If
195    // tree.get(root)=None (0x40738=0) or nodes.len()=0 (0x40734), the InvalidTree@229 is
196    // because RECONCILE produced a broken tree — root cause is reconcile, not sizing.
197    unsafe {
198        crate::az_mark(0x60730_u32, (tree.root as u32));
199        crate::az_mark(0x60734_u32, (tree.nodes.len() as u32));
200        crate::az_mark(0x60738_u32, u32::from(tree.get(tree.root).is_some()));
201        // [az-diag g55] 0x4075C = the `tree` ptr the CALLEE sees. Compare with 0x40748
202        // (caller's &new_tree). Same → nodes-field-offset mis-lift; differ → &mut arg mis-passed.
203        crate::az_mark(0x6075C_u32, ((std::ptr::from_ref::<LayoutTree>(tree) as usize) as u32));
204    }
205    calculator.calculate_intrinsic_recursive(tree, tree.root, false)?;
206    debug_log!(ctx, "Finished intrinsic size calculation");
207    Ok(())
208}
209
210fn compute_dirty_ancestor_closure(
211    tree: &LayoutTree,
212    dirty_nodes: &BTreeSet<usize>,
213) -> std::collections::HashSet<usize> {
214    let mut closure: std::collections::HashSet<usize> = std::collections::HashSet::new();
215    for &dirty in dirty_nodes {
216        let mut cur = Some(dirty);
217        while let Some(idx) = cur {
218            if !closure.insert(idx) {
219                break;
220            }
221            cur = tree.get(idx).and_then(|n| n.parent);
222        }
223    }
224    closure
225}
226
227struct IntrinsicSizeCalculator<'a, 'b, 'c, T: ParsedFontTrait> {
228    ctx: &'a mut LayoutContext<'b, T>,
229    /// Shared text shaping cache, threaded through from the caller so
230    /// stages 1–3 of the inline layout pipeline (logical / `BiDi` / shaping)
231    /// are cache-hits across the sizing pass's min/max-content measurements
232    /// AND the subsequent real layout pass. Previously each pass held its
233    /// own `LayoutCache`, so identical text was shaped three times per
234    /// `root_layout_pass` — once per min-content measurement, once per
235    /// max-content measurement, once at final layout.
236    text_cache: &'c mut LayoutCache,
237    /// If `Some`, only nodes in this set (the ancestor-closure of
238    /// dirty nodes) need recomputation. A clean node whose
239    /// `warm.intrinsic_sizes` is already populated reuses the
240    /// cached value and skips its entire subtree descent.
241    dirty_closure: Option<std::collections::HashSet<usize>>,
242}
243
244impl<'a, 'b, 'c, T: ParsedFontTrait> IntrinsicSizeCalculator<'a, 'b, 'c, T> {
245    const fn new(ctx: &'a mut LayoutContext<'b, T>, text_cache: &'c mut LayoutCache) -> Self {
246        Self {
247            ctx,
248            text_cache,
249            dirty_closure: None,
250        }
251    }
252
253    #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
254    fn calculate_intrinsic_recursive(
255        &mut self,
256        tree: &mut LayoutTree,
257        node_index: usize,
258        ancestor_is_stf: bool,
259    ) -> Result<IntrinsicSizes> {
260        // [az-diag g52 REVERT] 0x40720 = node_index entering calculate_intrinsic_recursive
261        // (last value after the run = the node that InvalidTree'd or the stray child).
262        unsafe { crate::az_mark(0x60720_u32, (node_index as u32)); }
263        // Fast path: if this subtree has no dirty nodes AND we
264        // already have a cached intrinsic, return the cached value
265        // and skip the whole descent. Caller is the ancestor-closure
266        // computation in `calculate_intrinsic_sizes` — anything not
267        // in that set is guaranteed clean through every descendant.
268        if let Some(closure) = self.dirty_closure.as_ref() {
269            if !closure.contains(&node_index) {
270                if let Some(cached) = tree
271                    .warm(node_index)
272                    .and_then(|w| w.intrinsic_sizes)
273                {
274                    return Ok(cached);
275                }
276            }
277        }
278
279        // Fix C static-DOM short-circuit: if no ancestor needs this intrinsic
280        // (none are STF) AND no descendant in this subtree is STF, nobody
281        // will ever read the value. Write a default and skip the recursion.
282        // `subtree_needs_intrinsic` is precomputed at tree-build time from
283        // the DOM's display/position/float properties, so this is a constant
284        // lookup with no per-pass work.
285        if !ancestor_is_stf
286            && tree
287                .subtree_needs_intrinsic
288                .get(node_index)
289                .copied()
290                .is_some_and(|v| !v)
291        {
292            let default = IntrinsicSizes::default();
293            if let Some(n) = tree.warm_mut(node_index) {
294                n.intrinsic_sizes = Some(default);
295            }
296            return Ok(default);
297        }
298
299        // Previously cloned the full LayoutNode to sidestep borrow conflicts
300        // with the `&mut tree` recursive calls below, but we only need the
301        // DOM id here — a `Copy` scalar. The clone was allocating a
302        // Vec<usize> for children and a TaffyCache on every recursion
303        // (~300x on excel.html).
304        let dom_node_id = tree
305            .get(node_index)
306            .ok_or(LayoutError::InvalidTree)?
307            .dom_node_id;
308
309        // Out-of-flow (absolute/fixed) elements must NOT contribute to their
310        // parent's intrinsic size — but they still need their OWN intrinsic size
311        // computed, because an abs-pos box with `width:auto` is sized shrink-to-fit
312        // (§10.3.7), which reads this node's max-content width. So we compute and
313        // store the real intrinsic size below, then return zero to the caller so
314        // the parent ignores it. (Previously this early-returned zero AND stored
315        // zero on the node, collapsing every auto-width abs-pos box to width 0.)
316        let is_out_of_flow = matches!(
317            get_position_type(self.ctx.styled_dom, dom_node_id),
318            LayoutPosition::Absolute | LayoutPosition::Fixed
319        );
320
321        // Copy child indices before recursive calls (which need &mut tree).
322        // Stack buffer for the common case (≤32 children); heap only for huge nodes.
323        let children_slice = tree.children(node_index);
324        let n = children_slice.len();
325        let mut stack_buf = [0usize; 32];
326        let heap_buf: Vec<usize>;
327        let children: &[usize] = if n <= 32 {
328            stack_buf[..n].copy_from_slice(children_slice);
329            &stack_buf[..n]
330        } else {
331            heap_buf = children_slice.to_vec();
332            &heap_buf
333        };
334        // Propagate STF flag: children inherit `ancestor_is_stf=true` if any
335        // ancestor up to and including self is STF.
336        let self_is_stf = tree
337            .get(node_index)
338            .is_some_and(|n| {
339                crate::solver3::layout_tree::is_shrink_to_fit_context(
340                    self.ctx.styled_dom,
341                    n.dom_node_id,
342                    n.formatting_context,
343                )
344            });
345        let child_ancestor_is_stf = ancestor_is_stf || self_is_stf;
346
347        let mut child_intrinsics = Vec::with_capacity(n);
348        for &child_index in children {
349            // [az-diag g52 REVERT] 0x40728 = child_index about to recurse (last = the stray).
350            unsafe { crate::az_mark(0x60728_u32, (child_index as u32)); }
351            // [g52 FIX] Defensive: reconcile can mis-list a stray/out-of-range child_index
352            // (a Text node mis-listed as a layout child, or a lift artifact in the children
353            // array). The unguarded recursion would hit `tree.get(child_index).ok_or(InvalidTree)`
354            // at line ~226 and abort the WHOLE intrinsic-sizing pass. Skip gracefully so
355            // measurement continues — mirrors process_layout_children's guard (line ~1079).
356            // REAL fix = reconcile not listing the stray child.
357            if tree.get(child_index).is_none() {
358                continue;
359            }
360            let child_intrinsic =
361                self.calculate_intrinsic_recursive(tree, child_index, child_ancestor_is_stf)?;
362            child_intrinsics.push((child_index, child_intrinsic));
363        }
364
365        // Then calculate this node's intrinsic size based on its children
366        let mut intrinsic = self.calculate_node_intrinsic_sizes(tree, node_index, &child_intrinsics)?;
367
368        // +spec:min-max-sizing:970fef - if min-width/min-height is a <length>, use as floor for intrinsic sizes
369        if let Some(dom_id) = tree.get(node_index).and_then(|n| n.dom_node_id) {
370            use crate::solver3::getters::{get_css_min_width, get_css_min_height, MultiValue};
371
372            let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
373
374            // Resolve em against the element's OWN font-size and rem against the root.
375            let em = get_element_font_size(self.ctx.styled_dom, dom_id, node_state);
376            let rem = super::getters::get_root_font_size(self.ctx.styled_dom, node_state);
377
378            if let MultiValue::Exact(mw) = get_css_min_width(self.ctx.styled_dom, dom_id, node_state) {
379                if let Some(min_w) = super::calc::resolve_pixel_value_no_percent(&mw.inner, em, rem) {
380                    intrinsic.min_content_width = intrinsic.min_content_width.max(min_w);
381                    intrinsic.max_content_width = intrinsic.max_content_width.max(min_w);
382                }
383            }
384
385            if let MultiValue::Exact(mh) = get_css_min_height(self.ctx.styled_dom, dom_id, node_state) {
386                if let Some(min_h) = super::calc::resolve_pixel_value_no_percent(&mh.inner, em, rem) {
387                    intrinsic.min_content_height = intrinsic.min_content_height.max(min_h);
388                    intrinsic.max_content_height = intrinsic.max_content_height.max(min_h);
389                }
390            }
391        }
392
393        if let Some(n) = tree.warm_mut(node_index) {
394            n.intrinsic_sizes = Some(intrinsic);
395        }
396
397        // An out-of-flow box's own intrinsic size is stored above (for its
398        // shrink-to-fit auto width), but it does not contribute to its parent's
399        // intrinsic size — return zero to the caller.
400        if is_out_of_flow {
401            Ok(IntrinsicSizes::default())
402        } else {
403            Ok(intrinsic)
404        }
405    }
406
407    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
408    fn calculate_node_intrinsic_sizes(
409        &mut self,
410        tree: &LayoutTree,
411        node_index: usize,
412        child_intrinsics: &[(usize, IntrinsicSizes)],
413    ) -> Result<IntrinsicSizes> {
414        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
415
416        // +spec:block-formatting-context:30def2 - replaced elements use physical 300x150 default, not re-oriented by writing-mode
417        // +spec:display-property:015c41 - replaced elements default to 300x150 intrinsic size per css-sizing-3 §5.1
418        // +spec:display-property:2c6af3 - replaced elements with auto width/height use max-content size
419        // +spec:replaced-elements:6d6030 - Intrinsic sizes for replaced elements (images, virtual views)
420        // VirtualViews are replaced elements with a default intrinsic size of 300x150px
421        // (same as virtualized view elements)
422        if let Some(dom_id) = node.dom_node_id {
423            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
424            if node_data.is_virtual_view_node() {
425                return Ok(IntrinsicSizes {
426                    min_content_width: 300.0,
427                    max_content_width: 300.0,
428                    preferred_width: None, // Will be determined by CSS or flex-grow
429                    min_content_height: 150.0,
430                    max_content_height: 150.0,
431                    preferred_height: None, // Will be determined by CSS or flex-grow
432                    preferred_aspect_ratio: None,
433                });
434            }
435            
436            // +spec:containing-block:bb5a12 - replaced element intrinsic sizes using initial containing block
437            // +spec:display-property:7127f9 - intrinsic sizes of replaced elements without natural sizes (300x150 fallback, aspect ratio)
438            // +spec:display-property:f9cede - replaced elements derive intrinsic size from natural dimensions
439            // +spec:writing-modes:b18121 - stretch fit inline size from available space, calculate block size via aspect ratio
440            if let NodeType::Image(image_ref) = node_data.get_node_type() {
441                let size = image_ref.get_size();
442                // +spec:containing-block:1da6dc - use initial CB inline size for replaced elements with aspect ratio but no intrinsic size
443                // Per css-sizing-3 §5.1: "use an inline size matching the corresponding dimension
444                // of the initial containing block and calculate the other dimension using the aspect ratio"
445                let has_intrinsic = size.width > 0.0 || size.height > 0.0;
446                let (width, height) = if size.width > 0.0 && size.height > 0.0 {
447                    (size.width, size.height)
448                } else if size.width > 0.0 {
449                    (size.width, size.width / 2.0)
450                } else if size.height > 0.0 {
451                    // Has intrinsic height but no width — use initial CB inline dimension
452                    (self.ctx.viewport_size.width, size.height)
453                } else {
454                    // +spec:replaced-elements:43376b - 300px fallback with 2:1 ratio for replaced elements
455                    // No intrinsic dimensions — cap at 300x150 per CSS 2.2 §10.3.2
456                    // +spec:width-calculation:3b0efe - auto width fallback: 300px capped to device width
457                    // +spec:width-calculation:16c305 - auto height fallback: 2:1 ratio, max 150px
458                    let w = self.ctx.viewport_size.width.min(300.0);
459                    (w, w / 2.0)
460                };
461                // A replaced element with NO intrinsic size (e.g. a RenderImageCallback
462                // <img> like the AzulPaint canvas) must behave like a VirtualView: keep
463                // the 300×150 fallback as the min/max-content (so it has a sensible
464                // default) but leave `preferred` as None so `flex-grow` / explicit CSS
465                // can size it. A `Some(preferred)` here pins the box and defeats
466                // flex-grow (the canvas was laid out 300×0 — see the VirtualView arm
467                // above, which already uses None for exactly this reason). Images WITH
468                // a real intrinsic size keep `preferred = Some` so they display at their
469                // natural size when unconstrained.
470                let (pref_w, pref_h) = if has_intrinsic {
471                    (Some(width), Some(height))
472                } else {
473                    (None, None)
474                };
475                return Ok(IntrinsicSizes {
476                    min_content_width: width,
477                    max_content_width: width,
478                    preferred_width: pref_w,
479                    min_content_height: height,
480                    max_content_height: height,
481                    preferred_height: pref_h,
482                    preferred_aspect_ratio: None,
483                });
484            }
485        }
486
487        match node.formatting_context {
488            FormattingContext::Block { .. } => {
489                // Check if this block establishes an Inline Formatting Context (IFC).
490                // Per CSS 2.2 §9.2.1.1: A block container with mixed block-level and
491                // inline-level children creates anonymous block boxes to wrap the inline
492                // content. So we only treat as IFC root if there are NO block-level children.
493                //
494                // We check the actual CSS display property, NOT formatting_context,
495                // because a display:block element with only inline children gets
496                // FormattingContext::Inline (meaning "establishes IFC for its children"),
497                // which is different from being an inline element itself.
498                let has_block_child = tree.children(node_index).iter().any(|&child_idx| {
499                    tree.get(child_idx)
500                        .and_then(|c| c.dom_node_id)
501                        .is_some_and(|dom_id| {
502                            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
503                            // Text nodes are inline-level
504                            if matches!(node_data.get_node_type(), NodeType::Text(_)) {
505                                return false;
506                            }
507                            let display = get_display_type(self.ctx.styled_dom, dom_id);
508                            display.creates_block_context()
509                        })
510                });
511
512                let has_inline_child = tree.children(node_index).iter().any(|&child_idx| {
513                    tree.get(child_idx)
514                        .and_then(|c| c.dom_node_id)
515                        .is_some_and(|dom_id| {
516                            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
517                            if matches!(node_data.get_node_type(), NodeType::Text(_)) {
518                                return true;
519                            }
520                            let display = get_display_type(self.ctx.styled_dom, dom_id);
521                            matches!(display,
522                                LayoutDisplay::Inline
523                                | LayoutDisplay::InlineBlock
524                                | LayoutDisplay::InlineFlex
525                                | LayoutDisplay::InlineGrid
526                                | LayoutDisplay::InlineTable
527                            )
528                        })
529                });
530
531                // IFC root only if there are inline children and NO block children.
532                // If there are block children, text nodes get anonymous block wrappers.
533                let is_ifc_root = has_inline_child && !has_block_child;
534                
535                // Also check if this block has direct text content (text nodes in DOM)
536                // but ONLY if there are no block-level layout children
537                let has_direct_text = if has_block_child {
538                    false
539                } else if let Some(dom_id) = node.dom_node_id {
540                    let node_hierarchy = &self.ctx.styled_dom.node_hierarchy.as_container();
541                    dom_id.az_children(node_hierarchy).any(|child_id| {
542                        let child_node_data = &self.ctx.styled_dom.node_data.as_container()[child_id];
543                        matches!(child_node_data.get_node_type(), NodeType::Text(_))
544                    })
545                } else {
546                    false
547                };
548                
549                if is_ifc_root || has_direct_text {
550                    // This block is an IFC root - measure all inline content ONCE
551                    self.calculate_ifc_root_intrinsic_sizes(tree, node_index)
552                } else {
553                    // This is a BFC root (only block children) - aggregate child sizes
554                    self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics)
555                }
556            }
557            FormattingContext::Inline => {
558                // There are THREE cases for FormattingContext::Inline:
559                // 1. A Text node (NodeType::Text) - this IS the text content itself
560                //    -> Needs to measure itself as an atomic inline unit
561                // 2. An IFC root - a block with only inline children (has text child nodes)
562                //    -> Should measure its inline content
563                // 3. A true inline element (display: inline, e.g., <span>) with no text
564                //    -> Returns default(0,0), measured by parent IFC root
565                //
566                // We distinguish by:
567                // - Checking if THIS node is a Text node (case 1)
568                // - Checking if this subtree contains any text (case 2)
569                //
570                // Why descendants, not just direct children: for `<span><a>text</a></span>`,
571                // the `<span>` is a layout-tree IFC root (layout_ifc is called on it), but
572                // its direct DOM children are inline elements, not text. Restricting the
573                // check to direct text children would zero out the span's intrinsic width
574                // even though the cell content width depends on it.
575                let is_text_node = if let Some(dom_id) = node.dom_node_id {
576                    let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
577                    matches!(node_data.get_node_type(), NodeType::Text(_))
578                } else {
579                    false
580                };
581
582                let has_text_in_subtree = if let Some(dom_id) = node.dom_node_id {
583                    subtree_contains_text(self.ctx.styled_dom, dom_id)
584                } else {
585                    false
586                };
587
588                if is_text_node || has_text_in_subtree {
589                    // Case 1 or 2: Text node or IFC root - measure inline content
590                    self.calculate_ifc_root_intrinsic_sizes(tree, node_index)
591                } else {
592                    // Case 3: True inline element - measured by parent IFC root
593                    Ok(IntrinsicSizes::default())
594                }
595            }
596            FormattingContext::InlineBlock => {
597                // Inline-block IS an atomic inline - it needs its own intrinsic size.
598                // Check layout tree children AND direct DOM text children (text nodes
599                // are not in the layout tree, only in the DOM).
600                let has_inline_children = tree.children(node_index).iter().any(|&child_idx| {
601                    tree.get(child_idx)
602                        .is_some_and(|c| matches!(c.formatting_context, FormattingContext::Inline))
603                });
604
605                let has_direct_text = if let Some(dom_id) = node.dom_node_id {
606                    let node_hierarchy = &self.ctx.styled_dom.node_hierarchy.as_container();
607                    dom_id.az_children(node_hierarchy).any(|child_id| {
608                        let child_node_data = &self.ctx.styled_dom.node_data.as_container()[child_id];
609                        matches!(child_node_data.get_node_type(), NodeType::Text(_))
610                    })
611                } else {
612                    false
613                };
614
615                if has_inline_children || has_direct_text {
616                    // InlineBlock with inline children - measure as IFC root.
617                    // Returns content-level intrinsic sizes (no margin/padding/border).
618                    // The parent adds box-model extras via calculate_block_intrinsic_sizes,
619                    // and calculate_used_size_for_node adds padding+border for border-box.
620                    let intrinsic = self.calculate_ifc_root_intrinsic_sizes(tree, node_index)?;
621
622                    Ok(intrinsic)
623                } else {
624                    // InlineBlock with block children - aggregate like block
625                    self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics)
626                }
627            }
628            FormattingContext::Table => {
629                Ok(self.calculate_table_intrinsic_sizes(tree, node_index, child_intrinsics))
630            }
631            FormattingContext::Flex => {
632                self.calculate_flex_intrinsic_sizes(tree, node_index, child_intrinsics)
633            }
634            _ => self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics),
635        }
636    }
637    
638    // +spec:intrinsic-sizing:ea2c2c - §5.1 min-content size = size as float with auto; max-content = no wrapping
639    /// Calculate intrinsic sizes for an IFC root (a block containing inline content).
640    /// This collects ALL inline descendants' text and measures it ONCE.
641    // +spec:intrinsic-sizing:8f3c0c - hanging glyphs must be excluded from intrinsic size measurement
642    #[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
643    fn calculate_ifc_root_intrinsic_sizes(
644        &mut self,
645        tree: &LayoutTree,
646        node_index: usize,
647    ) -> Result<IntrinsicSizes> {
648        // [g75] 0x60758 = how many times this IFC sizer is entered; 0x6075C = node_index of THIS call.
649        unsafe {
650            let c = crate::az_mark_read(0x60758).wrapping_add(1);
651            crate::az_mark(0x60758_u32, (c));
652            crate::az_mark(0x6075C_u32, (node_index as u32));
653        }
654        // Collect all inline content from this IFC root and its inline descendants
655        // [g76] EXPLICIT match (was `?`): the g75 markers showed collect_inline_content reaching its
656        // completion marker B8 (Ok at the source level) yet the IFC sizer never advancing to 0xA1 —
657        // i.e. the lifted `Result<Vec<InlineContent>, LayoutError>` return arrives as Err at this call
658        // site (a complex by-value Result-return mis-lift). 0x60760 = 1 (Ok) / 0xEE (Err-but-B8-ran).
659        // RESILIENCE: on Err, degrade to empty content (→ default intrinsic) instead of aborting the
660        // WHOLE layout with InvalidTree, so the page renders and the next real blocker surfaces.
661        // g76 PROVED: degrading to Vec::new() here (resilience) lets layout proceed past this
662        // InvalidTree but then HANGS in the downstream actual-layout shaping (the documented g47
663        // hashbrown empty-map infinite loop). So for a CLEAN (non-hanging) state we PROPAGATE the Err
664        // (same as the original `?`), keeping the 0x60760 diagnostic. To chase the g47 hang, flip the
665        // Err arm back to `Vec::new()`. 0x60760 = 1 (Ok) / 0xEE (Err-despite-B8 = the Result mis-lift).
666        // [g78] OUT-PARAM refactor: the by-value `Result<Vec<InlineContent>, LayoutError>` return
667        // mis-lifted Ok→Err (g76/g77 PROVED it: 0x60760=0xEE despite the source reaching B8). Filling
668        // a `&mut Vec` out-param and returning `Result<()>` (register-returned, NO sret-of-Vec) lifts
669        // cleanly — the established M12.7 "a pointer arg lifts cleanly" pattern. 0x60760 should now =1.
670        let collect_result = collect_inline_content(self.ctx, tree, node_index);
671        #[cfg(feature = "web_lift")]
672        unsafe { crate::az_mark((0x60760) as u32, (if collect_result.is_ok() { 0x00000001u32 } else { 0x000000EEu32 }) as u32); }
673        let inline_content: Vec<InlineContent> = collect_result?;
674
675        if inline_content.is_empty() {
676            return Ok(IntrinsicSizes::default());
677        }
678
679        // Get pre-loaded fonts from font manager
680        let loaded_fonts = self.ctx.font_manager.get_loaded_fonts();
681
682        // +spec:intrinsic-sizing:ae8beb - min-content = zero-width CB, max-content = infinite-width CB
683        // +spec:intrinsic-sizing:8c94e2 - min-content/max-content intrinsic size determination via constrained layout
684        // Use `measure_intrinsic_widths` instead of two `layout_flow` passes (fix B):
685        // it runs stages 1–4 of the pipeline once (logical → BiDi → shape → orient)
686        // and derives min/max-content by scanning the shaped items directly. This
687        // avoids the BreakCursor line-breaking loop entirely — that loop clones
688        // every ShapedCluster it inspects via `peek_next_unit` and accounted for
689        // 24% of total CPU on the text_2000 stress fixture. Shaping is cached
690        // at the per-item level (keyed on text+style), so the subsequent real
691        // layout_flow call for this content gets pure cache hits for stages 1–3.
692        // Populate the measurement constraints from the IFC root's real white-space
693        // mode instead of always using defaults. With the default (Normal) the scan
694        // treats every space as a break opportunity, so a white-space:nowrap / pre
695        // element reports a min-content SMALLER than its true unbreakable width and
696        // the flex/shrink-to-fit algorithm clips it.
697        let mut constraints = UnifiedConstraints::default();
698        if let Some(dom_id) = tree.get(node_index).and_then(|n| n.dom_node_id) {
699            use crate::solver3::getters::{get_white_space_property, MultiValue};
700            use azul_css::props::style::text::StyleWhiteSpace;
701            let node_state =
702                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
703            let ws = match get_white_space_property(self.ctx.styled_dom, dom_id, node_state) {
704                MultiValue::Exact(v) => v,
705                _ => StyleWhiteSpace::Normal,
706            };
707            constraints.white_space_mode = match ws {
708                StyleWhiteSpace::Normal => crate::text3::cache::WhiteSpaceMode::Normal,
709                StyleWhiteSpace::Nowrap => crate::text3::cache::WhiteSpaceMode::Nowrap,
710                StyleWhiteSpace::Pre => crate::text3::cache::WhiteSpaceMode::Pre,
711                StyleWhiteSpace::PreWrap => crate::text3::cache::WhiteSpaceMode::PreWrap,
712                StyleWhiteSpace::PreLine => crate::text3::cache::WhiteSpaceMode::PreLine,
713                StyleWhiteSpace::BreakSpaces => crate::text3::cache::WhiteSpaceMode::BreakSpaces,
714            };
715        }
716        // [g79 DIAG] Probe the font state at shaping time, then convert the downstream shape_text
717        // HANG (g47 hashbrown empty-map loop) → trap so the harness RETURNS and these markers are
718        // readable (can't read markers from a hung wasm call). Tests the #4→#3 coupling: if
719        // 0x60768(font_chain_cache.len) / 0x6076C(loaded_fonts.len) are 0, the EMPTY FONT CHAIN is
720        // the ROOT of the hang (no font → allsorts builds empty hashbrown maps → RawIter loops).
721        // [g82] CONDITIONAL trap: if the font_chain_cache is still EMPTY, shaping would HANG (allsorts
722        // builds empty hashbrown maps → g47 RawIter loop) → trap instead so the markers are readable
723        // (non-hang). If the chain is NON-empty (unique_font_keys BTreeMap fix worked + populated it),
724        // PROCEED into measure → shape → text should MEASURE. g81 hung (no conditional) → need to know
725        // whether the chain populated. 0x60768=chain.len, 0x6076C=loaded_fonts.len, 0x60704=0xA15.
726        #[cfg(feature = "web_lift")]
727        {
728            let cl = self.ctx.font_manager.font_chain_cache.len();
729            unsafe {
730                crate::az_mark((0x60768) as u32, (cl as u32) as u32);
731                crate::az_mark((0x6076C) as u32, (loaded_fonts.len() as u32) as u32);
732                crate::az_mark((0x60704) as u32, (0xA15u32) as u32);
733            }
734            // [g88] g85+g87 PROVED whack-a-mole does NOT converge: BTreeMap'd unique_font_keys (chain
735            // ✓), supported_features+lookups_index (g85), ReadCache (g87) — STILL HANGS. Too many
736            // hashbrown empty-map sites across allsorts/std/rust-fontconfig. The ONLY convergent fix is
737            // the SYSTEMIC transpiler empty-static mirror (force the lifted hashbrown ctrl-scan to read
738            // 0xFF not 0x00). TEMP non-hang trap until that lands. ★ REMOVE to test the systemic fix.
739            // [g93] PROCEED into shaping to test the AZ_FORCE_MIRROR_VMADDRS hashbrown-EMPTY_GROUP fix.
740            // If text MEASURES → the forced const pages contained EMPTY_GROUP → systemic fix found.
741            let _ = (cl, loaded_fonts.len());
742        }
743        let Ok(intrinsic_text) = self.text_cache.measure_intrinsic_widths(
744            &inline_content,
745            &[],
746            &constraints,
747            &self.ctx.font_manager.font_chain_cache,
748            &self.ctx.font_manager.fc_cache,
749            &loaded_fonts,
750            self.ctx.debug_messages,
751        ) else {
752            return Ok(IntrinsicSizes {
753                min_content_width: FALLBACK_MIN_CONTENT_WIDTH,
754                max_content_width: FALLBACK_MAX_CONTENT_WIDTH,
755                preferred_width: None,
756                min_content_height: FALLBACK_MIN_CONTENT_HEIGHT,
757                max_content_height: FALLBACK_MAX_CONTENT_HEIGHT,
758                preferred_height: None,
759                preferred_aspect_ratio: None,
760            });
761        };
762
763        let min_width = intrinsic_text.min_content_width;
764        let max_width = intrinsic_text.max_content_width;
765
766        // +spec:display-property:c587fd - min-content block size equals max-content block size for block containers, tables, inline boxes
767        // +spec:intrinsic-sizing:02eedc - min-content block size equals max-content block size for block containers
768        // For a single-line max-content layout the height is one line box;
769        // `measure_intrinsic_widths` returns exactly that.
770        let max_content_height = intrinsic_text.max_content_height;
771
772        // NOTE(writing-modes): min_content_width / max_content_width are named for
773        // the physical axis. In vertical writing modes the "inline" axis is vertical,
774        // so these are swapped by calculate_block_intrinsic_sizes when computing
775        // the parent's intrinsic sizes. The physical naming is intentional here.
776        Ok(IntrinsicSizes {
777            min_content_width: min_width,
778            max_content_width: max_width,
779            preferred_width: None,
780            min_content_height: max_content_height,
781            max_content_height,
782            preferred_height: None,
783            preferred_aspect_ratio: None,
784        })
785    }
786
787    // +spec:containing-block:bb0658 - percentage block-sizes behave as auto during intrinsic computation (no CSS height resolution here)
788    // +spec:display-contents:84fe7f - cyclic percentage contributions: percentage-sized children use auto during intrinsic sizing
789    // +spec:min-max-sizing:411904 - percentage block-sizes treated as auto during intrinsic sizing (content-sized CB)
790    // +spec:min-max-sizing:737e62 - percentage heights don't resolve inside content-sized containing blocks
791    fn calculate_block_intrinsic_sizes(
792        &self,
793        tree: &LayoutTree,
794        node_index: usize,
795        child_intrinsics: &[(usize, IntrinsicSizes)],
796    ) -> Result<IntrinsicSizes> {
797        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
798        let writing_mode = node.dom_node_id.map_or_else(LayoutWritingMode::default, |dom_id| {
799            let node_state =
800                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
801            get_writing_mode(self.ctx.styled_dom, dom_id, node_state).unwrap_or_default()
802        });
803
804        // NOTE: Text content detection is now handled in calculate_node_intrinsic_sizes
805        // which calls calculate_ifc_root_intrinsic_sizes for blocks with inline content.
806        // This function now only handles pure block containers (BFC roots).
807        // +spec:height-calculation:d9ca8d - cyclic percentage contributions: percentage min-height/max-height on children should behave as auto when computing intrinsic contributions (not yet implemented)
808
809        let mut max_child_min_cross = 0.0f32;
810        let mut max_child_max_cross = 0.0f32;
811        let mut total_main_size = 0.0;
812        // Track margins for CSS 2.2 §8.3.1 collapsing in the block direction.
813        // Block margins collapse between siblings (max instead of sum) and
814        // parent-child margins can escape (first/last child).
815        let mut last_margin_main_end = 0.0f32;
816        let mut is_first_child = true;
817
818        for &child_index in tree.children(node_index) {
819            if let Some(child_intrinsic) = child_intrinsics.iter().find(|(k, _)| k == &child_index).map(|(_, v)| v) {
820                // +spec:intrinsic-sizing:ed72bb - intrinsic contributions based on outer size, auto margins as zero
821                let child_node = tree.get(child_index);
822                let (cross_extras, main_border_padding, main_margin_start, main_margin_end) =
823                    child_node.map_or((0.0, 0.0, 0.0, 0.0), |cn| {
824                        let bp = cn.box_props.unpack();
825                        let h = bp.margin.left + bp.margin.right
826                              + bp.border.left + bp.border.right
827                              + bp.padding.left + bp.padding.right;
828                        let v_bp = bp.border.top + bp.border.bottom
829                              + bp.padding.top + bp.padding.bottom;
830                        match writing_mode {
831                            LayoutWritingMode::HorizontalTb => (h, v_bp, bp.margin.top, bp.margin.bottom),
832                            _ => (v_bp, h, bp.margin.left, bp.margin.right),
833                        }
834                    });
835
836                let (child_min_cross, child_max_cross, child_border_box_main) = match writing_mode {
837                    LayoutWritingMode::HorizontalTb => (
838                        child_intrinsic.min_content_width + cross_extras,
839                        child_intrinsic.max_content_width + cross_extras,
840                        child_intrinsic.max_content_height + main_border_padding,
841                    ),
842                    _ => (
843                        child_intrinsic.min_content_height + cross_extras,
844                        child_intrinsic.max_content_height + cross_extras,
845                        child_intrinsic.max_content_width + main_border_padding,
846                    ),
847                };
848
849                max_child_min_cross = max_child_min_cross.max(child_min_cross);
850                max_child_max_cross = max_child_max_cross.max(child_max_cross);
851
852                // CSS 2.2 §8.3.1 margin collapsing for intrinsic sizing:
853                // - First child's margin-start can escape (don't add to total)
854                // - Between siblings: collapsed gap = max(prev_end, curr_start)
855                // - Last child's margin-end can escape (don't add to total)
856                if is_first_child {
857                    is_first_child = false;
858                    // First child: top margin may escape, don't add it
859                } else {
860                    // Sibling gap: collapsed margin between prev bottom and current top
861                    let collapsed_gap = crate::solver3::fc::collapse_margins(
862                        last_margin_main_end, main_margin_start
863                    );
864                    total_main_size += collapsed_gap;
865                }
866
867                total_main_size += child_border_box_main;
868                last_margin_main_end = main_margin_end;
869            }
870        }
871        // Last child's margin-end may escape — don't add it to total_main_size
872
873        let (min_width, max_width, min_height, max_height) = match writing_mode {
874            LayoutWritingMode::HorizontalTb => (
875                max_child_min_cross,
876                max_child_max_cross,
877                total_main_size,
878                total_main_size,
879            ),
880            _ => (
881                total_main_size,
882                total_main_size,
883                max_child_min_cross,
884                max_child_max_cross,
885            ),
886        };
887
888        Ok(IntrinsicSizes {
889            min_content_width: min_width,
890            max_content_width: max_width,
891            preferred_width: None,
892            min_content_height: min_height,
893            max_content_height: max_height,
894            preferred_height: None,
895            preferred_aspect_ratio: None,
896        })
897    }
898
899    // The max-content main size is the sum of items' max-content contributions.
900    // The min-content main size of a single-line flex container is the sum of items'
901    // min-content contributions. For multi-line, it is the largest min-content contribution.
902    // Auto margins on flex items are treated as 0 for this computation.
903    fn calculate_flex_intrinsic_sizes(
904        &self,
905        tree: &LayoutTree,
906        node_index: usize,
907        child_intrinsics: &[(usize, IntrinsicSizes)],
908    ) -> Result<IntrinsicSizes> {
909        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
910
911        // Determine flex-direction to know if main axis is horizontal or vertical
912        let is_row = node.dom_node_id.is_none_or(|dom_id| {
913            let node_state =
914                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
915            match get_flex_direction(self.ctx.styled_dom, dom_id, node_state) {
916                MultiValue::Exact(dir) => matches!(dir, LayoutFlexDirection::Row | LayoutFlexDirection::RowReverse),
917                _ => true, // default is row
918            }
919        });
920
921        let mut sum_main_min: f32 = 0.0;
922        let mut sum_main_max: f32 = 0.0;
923        let mut max_main_min: f32 = 0.0;
924        let mut max_cross_min: f32 = 0.0;
925        let mut max_cross_max: f32 = 0.0;
926
927        for &child_index in tree.children(node_index) {
928            if let Some(child_intrinsic) = child_intrinsics.iter().find(|(k, _)| k == &child_index).map(|(_, v)| v) {
929                let (child_main_min, child_main_max, child_cross_min, child_cross_max) = if is_row {
930                    (
931                        child_intrinsic.min_content_width,
932                        child_intrinsic.max_content_width,
933                        child_intrinsic.min_content_height,
934                        child_intrinsic.max_content_height,
935                    )
936                } else {
937                    (
938                        child_intrinsic.min_content_height,
939                        child_intrinsic.max_content_height,
940                        child_intrinsic.min_content_width,
941                        child_intrinsic.max_content_width,
942                    )
943                };
944
945                sum_main_max += child_main_max;
946                sum_main_min += child_main_min;
947                // For multi-line min-content, track the largest single item
948                max_main_min = max_main_min.max(child_main_min);
949
950                // Cross axis: largest child determines the container's cross size
951                max_cross_min = max_cross_min.max(child_cross_min);
952                max_cross_max = max_cross_max.max(child_cross_max);
953            }
954        }
955
956        // For single-line (nowrap), min-content = sum; for multi-line (wrap), min-content = max
957        // Default flex-wrap is nowrap (single-line)
958        let is_single_line = node.dom_node_id.is_none_or(|dom_id| {
959            let node_state =
960                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
961            let wrap_prop = crate::solver3::getters::get_flex_wrap_prop(
962                self.ctx.styled_dom, dom_id, node_state,
963            );
964            wrap_prop.is_none_or(|val| matches!(
965                    val.get_property_or_default().unwrap_or_default(),
966                    LayoutFlexWrap::NoWrap
967                ))
968        });
969
970        let min_main = if is_single_line { sum_main_min } else { max_main_min };
971        let max_main = sum_main_max;
972
973        if is_row {
974            Ok(IntrinsicSizes {
975                min_content_width: min_main,
976                max_content_width: max_main,
977                preferred_width: None,
978                min_content_height: max_cross_min,
979                max_content_height: max_cross_max,
980                preferred_height: None,
981                preferred_aspect_ratio: None,
982            })
983        } else {
984            Ok(IntrinsicSizes {
985                min_content_width: max_cross_min,
986                max_content_width: max_cross_max,
987                preferred_width: None,
988                min_content_height: min_main,
989                max_content_height: max_main,
990                preferred_height: None,
991                preferred_aspect_ratio: None,
992            })
993        }
994    }
995
996    /// Calculate intrinsic sizes for a table element by aggregating cell content
997    /// widths per column and row heights.
998    /// +spec:table-layout:93b13c - shrink-to-fit for tables uses intrinsic sizing
999    fn calculate_table_intrinsic_sizes(
1000        &mut self,
1001        tree: &LayoutTree,
1002        node_index: usize,
1003        child_intrinsics: &[(usize, IntrinsicSizes)],
1004    ) -> IntrinsicSizes {
1005        // Collect per-column min/max widths and total row heights.
1006        // Table structure: table > row-group? > row > cell
1007        let mut col_min: Vec<f32> = Vec::new();
1008        let mut col_max: Vec<f32> = Vec::new();
1009        let mut total_height = 0.0f32;
1010
1011        // Iterate rows — children may be row groups (thead/tbody/tfoot) or direct rows
1012        let mut rows: Vec<usize> = Vec::new();
1013        for &child_idx in tree.children(node_index) {
1014            let Some(child) = tree.get(child_idx) else { continue };
1015            match child.formatting_context {
1016                FormattingContext::TableRow => rows.push(child_idx),
1017                FormattingContext::TableRowGroup => {
1018                    // Row group contains rows
1019                    for &row_idx in tree.children(child_idx) {
1020                        if let Some(row) = tree.get(row_idx) {
1021                            if matches!(row.formatting_context, FormattingContext::TableRow) {
1022                                rows.push(row_idx);
1023                            }
1024                        }
1025                    }
1026                }
1027                _ => {}
1028            }
1029        }
1030
1031        for &row_idx in &rows {
1032            let mut row_height = 0.0f32;
1033            for (col, &cell_idx) in tree.children(row_idx).iter().enumerate() {
1034                let cell_intrinsic = child_intrinsics.iter().find(|(k, _)| k == &cell_idx).map(|(_, v)| *v)
1035                    .unwrap_or_default();
1036                // Also check if cell has IFC content we can measure
1037                let cell_is = if cell_intrinsic.max_content_width > 0.0 {
1038                    cell_intrinsic
1039                } else {
1040                    // Try to measure cell content via IFC
1041                    self.calculate_ifc_root_intrinsic_sizes(tree, cell_idx)
1042                        .unwrap_or_default()
1043                };
1044
1045                // Add cell box-model extras
1046                let cell_node = tree.get(cell_idx);
1047                let (h_extras, v_extras) = cell_node.map_or((0.0, 0.0), |cn| {
1048                    let bp = cn.box_props.unpack();
1049                    (bp.padding.left + bp.padding.right + bp.border.left + bp.border.right,
1050                     bp.padding.top + bp.padding.bottom + bp.border.top + bp.border.bottom)
1051                });
1052
1053                let cell_min = cell_is.min_content_width + h_extras;
1054                let cell_max = cell_is.max_content_width + h_extras;
1055                let cell_h = cell_is.max_content_height + v_extras;
1056
1057                if col >= col_min.len() {
1058                    col_min.push(cell_min);
1059                    col_max.push(cell_max);
1060                } else {
1061                    col_min[col] = col_min[col].max(cell_min);
1062                    col_max[col] = col_max[col].max(cell_max);
1063                }
1064                row_height = row_height.max(cell_h);
1065            }
1066            total_height += row_height;
1067        }
1068
1069        let min_width: f32 = col_min.iter().sum();
1070        let max_width: f32 = col_max.iter().sum();
1071
1072        IntrinsicSizes {
1073            min_content_width: min_width,
1074            max_content_width: max_width,
1075            min_content_height: total_height,
1076            max_content_height: total_height,
1077            preferred_width: None,
1078            preferred_height: None,
1079            preferred_aspect_ratio: None,
1080        }
1081    }
1082}
1083
1084/// Gathers all inline content for the intrinsic sizing pass.
1085///
1086/// This function recursively collects text and inline-level content according to
1087/// CSS Sizing Level 3, Section 4.1: "Intrinsic Sizes"
1088/// <https://www.w3.org/TR/css-sizing-3/#intrinsic-sizes>
1089///
1090/// For inline formatting contexts, we need to gather:
1091/// 1. Text nodes (inline content)
1092/// 2. Inline-level boxes (display: inline, inline-block, etc.)
1093/// 3. Atomic inline-level elements (replaced elements like images)
1094///
1095/// The key difference from `collect_and_measure_inline_content` in fc.rs is that
1096/// this version is used for intrinsic sizing (calculating min/max-content widths)
1097/// before the actual layout pass, so it must recursively gather content from
1098/// inline descendants without laying them out first.
1099fn collect_inline_content_for_sizing<T: ParsedFontTrait>(
1100    ctx: &mut LayoutContext<'_, T>,
1101    tree: &LayoutTree,
1102    ifc_root_index: usize,
1103    out: &mut Vec<InlineContent>,
1104) -> Result<()> {
1105    debug_log!(ctx, "Collecting inline content from node {} for intrinsic sizing", ifc_root_index);
1106
1107    // [g78] fill the caller's out-param (was a local Vec returned by value → Ok→Err mis-lift).
1108    // Recursively collect inline content from this node and its inline descendants
1109    collect_inline_content_recursive(ctx, tree, ifc_root_index, out)?;
1110    // [g73] B8 = top-level recursion returned Ok (collect_inline_content complete).
1111    unsafe { crate::az_mark(0x6071C_u32, (0xB8u32)); }
1112    debug_log!(ctx, "Collected {} inline content items from node {}", out.len(), ifc_root_index);
1113
1114    Ok(())
1115}
1116
1117/// Recursive helper for collecting inline content.
1118///
1119/// According to CSS Sizing Level 3, the intrinsic size of an inline formatting context
1120/// is based on all inline-level content, including text in nested inline elements.
1121///
1122/// This function:
1123/// - Collects text from the current node if it's a text node
1124/// - Collects text from DOM children (text nodes may not be in layout tree)
1125/// - Recursively collects from inline children (display: inline)
1126/// - Treats non-inline children as atomic inline-level boxes
1127#[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
1128fn collect_inline_content_recursive<T: ParsedFontTrait>(
1129    ctx: &mut LayoutContext<'_, T>,
1130    tree: &LayoutTree,
1131    node_index: usize,
1132    content: &mut Vec<InlineContent>,
1133) -> Result<()> {
1134    // [g75] capture node_index of EVERY recursion entry (0x60754) and mark the entry-tree.get
1135    // FAILURE distinctly (inline-phase=0xBAD) so a node_index that fails HERE (before B1) is
1136    // visible even though a PRIOR successful call already wrote B8. This is the suspected
1137    // InvalidTree site (phase stuck at 0xA0 + B8 reached ⇒ a 2nd IFC call fails at this get).
1138    unsafe { crate::az_mark(0x60754_u32, (node_index as u32)); }
1139    let Some(node) = tree.get(node_index) else {
1140        unsafe { crate::az_mark(0x6071C_u32, (0xBADu32)); }
1141        return Err(LayoutError::InvalidTree);
1142    };
1143
1144    // CRITICAL FIX: Text nodes may exist in the DOM but not as separate layout nodes!
1145    // We need to check the DOM children for text content.
1146    let Some(dom_id) = node.dom_node_id else {
1147        // No DOM ID means this is a synthetic node, skip text extraction
1148        return process_layout_children(ctx, tree, node_index, content);
1149    };
1150
1151    // First check if THIS node is a text node
1152    if let Some(text) = extract_text_from_node(ctx.styled_dom, dom_id) {
1153        let style_props = Arc::new(get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref(), azul_css::props::basic::PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height)));
1154        debug_log!(ctx, "Found text in node {}: '{}'", node_index, text);
1155        // Use split_text_for_whitespace to correctly handle white-space: pre with \n
1156        let text_items = split_text_for_whitespace(
1157            ctx.styled_dom,
1158            dom_id,
1159            &text,
1160            &style_props,
1161        );
1162        content.extend(text_items);
1163    }
1164
1165    // CRITICAL: Also check DOM children for text nodes!
1166    // Text nodes are often not represented as separate layout nodes.
1167    // However, we must SKIP children that already have a layout tree entry,
1168    // because those will be handled by process_layout_children() below.
1169    // Without this guard, text nodes present in both DOM and layout tree
1170    // get collected twice, causing inline-block containers to be ~2x too wide.
1171    let node_hierarchy = &ctx.styled_dom.node_hierarchy.as_container();
1172    for child_id in dom_id.az_children(node_hierarchy) {
1173        // Skip DOM children that have layout tree nodes - they will be
1174        // processed via process_layout_children -> collect_inline_content_recursive
1175        if tree.dom_to_layout.contains_key(&child_id) {
1176            continue;
1177        }
1178        // Check if this DOM child is a text node
1179        let child_dom_node = &ctx.styled_dom.node_data.as_container()[child_id];
1180        if let NodeType::Text(text_data) = child_dom_node.get_node_type() {
1181            let text = text_data.as_str().to_string();
1182            let style_props = Arc::new(get_style_properties(ctx.styled_dom, child_id, ctx.system_style.as_ref(), azul_css::props::basic::PhysicalSize::new(ctx.viewport_size.width, ctx.viewport_size.height)));
1183            debug_log!(ctx, "Found text in DOM child of node {}: '{}'", node_index, text);
1184            // Use split_text_for_whitespace to correctly handle white-space: pre with \n
1185            let text_items = split_text_for_whitespace(
1186                ctx.styled_dom,
1187                child_id,
1188                &text,
1189                &style_props,
1190            );
1191            content.extend(text_items);
1192        }
1193    }
1194    // [g73] B6 = DOM-children loop done (about to process_layout_children).
1195    unsafe { crate::az_mark(0x6071C_u32, (0xB6u32)); }
1196
1197    process_layout_children(ctx, tree, node_index, content)
1198}
1199
1200/// Helper to process layout tree children for inline content collection
1201#[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
1202#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
1203fn process_layout_children<T: ParsedFontTrait>(
1204    ctx: &mut LayoutContext<'_, T>,
1205    tree: &LayoutTree,
1206    node_index: usize,
1207    content: &mut Vec<InlineContent>,
1208) -> Result<()> {
1209    use azul_css::props::layout::{LayoutHeight, LayoutWidth};
1210
1211    // [g73] PLC entry: 0x60708 = 0xC0<<24 | node_index (which node's children we process).
1212    unsafe { crate::az_mark(0x60708_u32, (0xC000_0000_u32 | (node_index as u32 & 0x00FF_FFFF))); }
1213    // Process layout tree children (these are elements with layout properties)
1214    for &child_index in tree.children(node_index) {
1215        // [g73] PLC loop: 0x6070C = current child_index being processed.
1216        unsafe { crate::az_mark(0x6070C_u32, (child_index as u32)); }
1217        // 2026-06-02: was `.ok_or(LayoutError::InvalidTree)?` — a stray/invalid child_index in
1218        // tree.children (likely a Text node mis-listed during reconcile, since Text is INLINE
1219        // content not a layout-tree node) aborted the WHOLE intrinsic-sizing pass with
1220        // InvalidTree BEFORE the inline text got measured → label height 0. Skip gracefully so
1221        // measurement continues (the inline text is collected separately above, at the
1222        // collect_inline_content_recursive DOM-children loop). REAL fix = reconcile not listing it.
1223        let Some(child_node) = tree.get(child_index) else { continue; };
1224        let Some(child_dom_id) = child_node.dom_node_id else {
1225            continue;
1226        };
1227
1228        let display = get_display_property(ctx.styled_dom, Some(child_dom_id));
1229
1230        // CSS Sizing Level 3: Inline-level boxes participate in the IFC
1231        if display.unwrap_or_default() == LayoutDisplay::Inline {
1232            // Recursively collect content from inline children
1233            // This is CRITICAL for proper intrinsic width calculation!
1234            debug_log!(ctx, "Recursing into inline child at node {}", child_index);
1235            collect_inline_content_recursive(ctx, tree, child_index, content)?;
1236        } else {
1237            // Non-inline children are treated as atomic inline-level boxes
1238            // (e.g., inline-block, images, floats)
1239            // Their intrinsic size must have been calculated in the bottom-up pass
1240            let intrinsic_sizes = tree.warm(child_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
1241
1242            // CSS 2.2 § 10.3.9: For inline-block elements with explicit CSS width/height,
1243            // use the CSS-defined values instead of intrinsic sizes.
1244            let node_state =
1245                &ctx.styled_dom.styled_nodes.as_container()[child_dom_id].styled_node_state;
1246            let css_width = get_css_width(ctx.styled_dom, child_dom_id, node_state);
1247            let css_height = get_css_height(ctx.styled_dom, child_dom_id, node_state);
1248
1249            // Resolve CSS width - use explicit value if set, otherwise fall back to intrinsic
1250            let used_width = match css_width {
1251                MultiValue::Exact(LayoutWidth::Px(px)) => {
1252                    // +spec:containing-block:495930 - percentages in intrinsic sizing fall back to intrinsic contribution (css-sizing-3 §5.2.1)
1253                    // +spec:containing-block:5246c0 - cyclic percentage: when containing block size depends on this box's intrinsic contribution, percentages fall back to intrinsic size
1254                    // +spec:containing-block:598124 - cyclic percentage contributions use intrinsic size
1255                    // +spec:height-calculation:ca9f19 - percentage-sized boxes use intrinsic size as contribution during intrinsic sizing
1256                    // +spec:width-calculation:7a384a - percentage-sized boxes behave as width:auto for intrinsic contributions (cyclic percentage)
1257                    // Resolve em/rem against the element's OWN font-size and the root
1258                    // font-size, NOT a hard-coded 16px — otherwise `width: 5em` on a
1259                    // font-size:24px inline-block sizes to 80px instead of 120px.
1260                    let em = get_element_font_size(ctx.styled_dom, child_dom_id, node_state);
1261                    let rem = super::getters::get_root_font_size(ctx.styled_dom, node_state);
1262                    super::calc::resolve_pixel_value_no_percent(&px, em, rem)
1263                        .unwrap_or(intrinsic_sizes.max_content_width)
1264                }
1265                MultiValue::Exact(LayoutWidth::MinContent) => intrinsic_sizes.min_content_width,
1266                MultiValue::Exact(LayoutWidth::MaxContent) => intrinsic_sizes.max_content_width,
1267                MultiValue::Exact(LayoutWidth::FitContent(_)) => {
1268                    // During intrinsic sizing, fit-content resolves to max-content
1269                    intrinsic_sizes.max_content_width
1270                }
1271                // For Auto or other values, use intrinsic size
1272                _ => intrinsic_sizes.max_content_width,
1273            };
1274
1275            // +spec:containing-block:5145c5 - percentage block-size ignored in content-sized containing blocks during intrinsic sizing
1276            // Resolve CSS height - use explicit value if set, otherwise fall back to intrinsic
1277            let used_height = match css_height {
1278                MultiValue::Exact(LayoutHeight::Px(px)) => {
1279                    // +spec:containing-block:7d5e79 - percentages behave as auto when containing block height is auto (cyclic percentage contribution)
1280                    // +spec:height-calculation:7d807b - css-sizing-3 §5.2.1: percentage heights behave as auto during intrinsic sizing (cyclic percentage contribution)
1281                    // Resolve em/rem against the element's own + root font-size (see width above).
1282                    let em = get_element_font_size(ctx.styled_dom, child_dom_id, node_state);
1283                    let rem = super::getters::get_root_font_size(ctx.styled_dom, node_state);
1284                    super::calc::resolve_pixel_value_no_percent(&px, em, rem)
1285                        .unwrap_or(intrinsic_sizes.max_content_height)
1286                }
1287                // is equivalent to automatic size
1288                MultiValue::Exact(LayoutHeight::MinContent) => intrinsic_sizes.max_content_height,
1289                // is equivalent to automatic size
1290                MultiValue::Exact(LayoutHeight::MaxContent) => intrinsic_sizes.max_content_height,
1291                MultiValue::Exact(LayoutHeight::FitContent(_)) => intrinsic_sizes.max_content_height,
1292                _ => intrinsic_sizes.max_content_height,
1293            };
1294
1295            debug_log!(ctx, "Found atomic inline child at node {}: display={:?}, intrinsic_width={}, used_width={}, css_width={:?}",
1296                child_index, display, intrinsic_sizes.max_content_width, used_width, css_width);
1297
1298            // Represent as a rectangular shape with the resolved dimensions
1299            content.push(InlineContent::Shape(InlineShape {
1300                shape_def: ShapeDefinition::Rectangle {
1301                    size: crate::text3::cache::Size {
1302                        width: used_width,
1303                        height: used_height,
1304                    },
1305                    corner_radius: None,
1306                },
1307                fill: None,
1308                stroke: None,
1309                baseline_offset: used_height,
1310                alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, child_dom_id),
1311                source_node_id: Some(child_dom_id),
1312            }));
1313        }
1314    }
1315
1316    Ok(())
1317}
1318
1319// Keep old name as an alias for backward compatibility
1320/// # Errors
1321///
1322/// Returns a `LayoutError` if collecting inline content fails.
1323pub fn collect_inline_content<T: ParsedFontTrait>(
1324    ctx: &mut LayoutContext<'_, T>,
1325    tree: &LayoutTree,
1326    ifc_root_index: usize,
1327) -> Result<Vec<InlineContent>> {
1328    let mut out = Vec::new();
1329    collect_inline_content_for_sizing(ctx, tree, ifc_root_index, &mut out)?;
1330    Ok(out)
1331}
1332
1333// +spec:height-calculation:1c899b - width and height properties specify the preferred size of the box
1334/// Calculates the used size of a single node based on its CSS properties and
1335/// the available space provided by its containing block.
1336///
1337/// // +spec:display-contents:71ccde - extrinsic sizing: size determined by context (containing block), not contents
1338///
1339/// This implementation correctly handles writing modes and percentage-based sizes
1340/// according to the CSS specification:
1341/// 1. `width` and `height` CSS properties are resolved to pixel values. Percentages are calculated
1342///    based on the containing block's PHYSICAL dimensions (`width` for `width`, `height` for
1343///    `height`), regardless of writing mode.
1344/// 2. The resolved physical `width` is then mapped to the node's logical CROSS size.
1345/// 3. The resolved physical `height` is then mapped to the node's logical MAIN size.
1346/// 4. A final `LogicalSize` is constructed from these logical dimensions.
1347// +spec:overflow:3c4f25 - auto box sizes: four auto-determined size types resolved here
1348// +spec:width-calculation:fb0629 - width/margin used values depend on box type, auto replaced by suitable value
1349///    M12.7: out-of-line auto-width-block inline size — `(cb.width - margins - borders -
1350/// padding).max(0.0)`. Extracted from `calc_used_size`'s auto-width Block arm so the
1351///    `.max(0.0)` runs in a small fn (proven to lift correctly), with a FRESH pointer
1352///    deref (the huge `calc_used_size` body hoists/spills cb.width and the remill lift then
1353///    reads it back 0). Returns by f32 (D0/V0 — the standard scalar return), NOT an out-ptr:
1354///    the out-ptr version computed 800 correctly but the caller's reload was opt-forwarded
1355///    to the init 0.0 across the opaque call (the helper's `*out` lowers to a direct
1356///    linear-mem store not modeled as aliasing the caller's slot). The f32 return is the
1357///    call's SSA result, which opt cannot replace. (The earlier "f32-return mis-lift" worry
1358///    was the 2×f32 *struct* HFA — a single scalar f32 return is fine.)
1359#[inline(never)]
1360#[allow(clippy::trivially_copy_pass_by_ref)] // <=8B Copy param kept by-ref intentionally (hot pixel/coord path or to avoid churning call sites for a perf-neutral change)
1361fn auto_block_inline_size(cb: &LogicalSize, bp: &BoxProps) -> f32 {
1362    let aw = cb.width
1363        - bp.margin.left
1364        - bp.margin.right
1365        - bp.border.left
1366        - bp.border.right
1367        - bp.padding.left
1368        - bp.padding.right;
1369    aw.max(0.0)
1370}
1371
1372#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
1373#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
1374/// # Errors
1375///
1376/// Returns a `LayoutError` if computing the used size fails.
1377pub fn calculate_used_size_for_node(
1378    styled_dom: &StyledDom,
1379    dom_id: Option<NodeId>,
1380    // M12.7: by-reference (GP-register pointer). A by-value LogicalSize is an HFA
1381    // (2×f32) the remill lift stages as an 8-byte double into a V register, and that
1382    // f64/d-register copyload mis-tracks to 0 in the wasm lift (single-f32 reads work,
1383    // the 64-bit one doesn't) — so cb + viewport arrived 0 and every width came out 0.
1384    // A pointer arg lifts cleanly; the body reads only .width/.height (auto-deref).
1385    containing_block_size: &LogicalSize,
1386    intrinsic: IntrinsicSizes,
1387    box_props: &BoxProps,
1388    viewport_size: &LogicalSize,
1389) -> Result<LogicalSize> {
1390    let Some(id) = dom_id else {
1391        // Anonymous boxes:
1392        // CSS 2.2 § 9.2.1.1: Anonymous boxes inherit from their enclosing box.
1393        // The inline dimension fills the containing block's inline size,
1394        // and the block dimension is auto (content-based).
1395        // In horizontal-tb: inline=width, block=height.
1396        // In vertical modes: inline=height, block=width.
1397        //
1398        // Since anonymous boxes don't have a DOM node, we default to horizontal-tb.
1399        // The parent's writing mode is already reflected in containing_block_size.
1400        return Ok(LogicalSize::new(
1401            containing_block_size.width,
1402            if intrinsic.max_content_height > 0.0 {
1403                intrinsic.max_content_height
1404            } else {
1405                // Auto height - will be resolved from content
1406                0.0
1407            },
1408        ));
1409    };
1410
1411    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
1412    let css_width = get_css_width(styled_dom, id, node_state);
1413    let css_height = get_css_height(styled_dom, id, node_state);
1414    let writing_mode = get_writing_mode(styled_dom, id, node_state);
1415    let display = get_display_property(styled_dom, Some(id));
1416    let position = get_position_type(styled_dom, dom_id);
1417
1418    // Construct the full WritingModeContext from resolved styles.
1419    // This determines how logical dimensions (inline/block) map to physical (width/height).
1420    let wm_ctx = WritingModeContext::new(
1421        writing_mode.unwrap_or_default(),
1422        get_direction_property(styled_dom, id, node_state).unwrap_or_default(),
1423        get_text_orientation_property(styled_dom, id, node_state).unwrap_or_default(),
1424    );
1425    let is_vertical = !wm_ctx.is_horizontal();
1426
1427    // +spec:display-property:06e0b1 - form controls (non-image) treated as non-replaced
1428    // Determine if this element is a replaced element (images, virtual views)
1429    let node_data = &styled_dom.node_data.as_container()[id];
1430    let is_replaced = matches!(node_data.get_node_type(), NodeType::Image(_))
1431        || node_data.is_virtual_view_node();
1432
1433    // +spec:width-calculation:79cdf8 - inline non-replaced: width property does not apply
1434    // +spec:width-calculation:972e86 - §10.3.1: width property does not apply to inline non-replaced elements
1435    // For inline non-replaced elements, override any explicit width to Auto.
1436    let css_width = if display.unwrap_or_default() == LayoutDisplay::Inline
1437        && !is_replaced
1438    {
1439        MultiValue::Exact(LayoutWidth::Auto)
1440    } else {
1441        css_width
1442    };
1443
1444    // +spec:box-model:1197a5 - height does not apply to non-replaced inline elements
1445    // +spec:display-property:9cb33d - height does not apply to inline boxes
1446    // +spec:height-calculation:c03717 - height does not apply to inline non-replaced elements
1447    // CSS 2.2 §10.6.1 / CSS Inline 3 §6.4: height property does not apply to
1448    // inline, non-replaced elements. Override any explicit height to Auto.
1449    let css_height = if display.unwrap_or_default() == LayoutDisplay::Inline
1450        && !is_replaced
1451    {
1452        MultiValue::Exact(LayoutHeight::Auto)
1453    } else {
1454        css_height
1455    };
1456
1457    // Remember if width/height were auto before consuming them
1458    let width_is_auto = css_width.is_auto() || matches!(&css_width, MultiValue::Exact(LayoutWidth::Auto));
1459    let height_is_auto = css_height.is_auto() || matches!(&css_height, MultiValue::Exact(LayoutHeight::Auto));
1460
1461    // +spec:intrinsic-sizing:9e1c9d - non-quantitative values (auto, min-content, max-content) are not influenced by box-sizing
1462    let width_is_quantitative = matches!(
1463        &css_width,
1464        MultiValue::Exact(LayoutWidth::Px(_) | LayoutWidth::FitContent(_) | LayoutWidth::Calc(_))
1465    );
1466    let height_is_quantitative = matches!(
1467        &css_height,
1468        MultiValue::Exact(LayoutHeight::Px(_) | LayoutHeight::FitContent(_) | LayoutHeight::Calc(_))
1469    );
1470
1471    // +spec:width-calculation:50d67a - automatic sizing concepts (width/height auto resolution)
1472    // +spec:width-calculation:564315 - §10.3 width calculation dispatch for all box types
1473    // Step 1: Resolve the CSS `width` property into a concrete pixel value.
1474    // CSS `width` always refers to the physical horizontal dimension, regardless of writing mode.
1475    // Percentage values resolve against the containing block's physical width.
1476    // In horizontal-tb: width = inline size. In vertical modes: width = block size.
1477    // The physical-to-logical mapping happens in Step 5 below.
1478    // Percentage values for `width` are resolved against the containing block's width.
1479    // +spec:width-calculation:febf0c - width/height "behaves as auto" when computed auto or percentage resolves against indefinite
1480    let resolved_width = match css_width.unwrap_or_default() {
1481        LayoutWidth::Auto => {
1482            // +spec:width-calculation:ed6a34 - auto width on replaced element uses intrinsic width
1483            // CSS 2.2 §10.3.2: If 'width' has a computed value of 'auto', and the element
1484            // has an intrinsic width, then that intrinsic width is the used value of 'width'.
1485            // +spec:replaced-elements:992ea5 - block-level replaced elements use inline replaced width rules
1486            // §10.3.4: "The used value of 'width' is determined as for inline replaced elements."
1487            // +spec:replaced-elements:36de3e - §10.3.2/§10.3.4: auto width for inline/block replaced elements uses intrinsic width
1488            // +spec:replaced-elements:b9a780 - §10.3.2: inline replaced auto width = intrinsic width (conditions resolved during intrinsic size calc)
1489            if is_replaced {
1490                // +spec:width-calculation:b41dbe - floating/inline replaced: auto width = intrinsic width
1491                // +spec:width-calculation:c62d35 - §10.3.2: auto width for replaced elements uses intrinsic width
1492                // +spec:width-calculation:d87ca4 - abs-replaced: auto width+height uses intrinsic width
1493                // For replaced elements (inline or block-level), auto width = intrinsic width.
1494                // The intrinsic sizes were already computed with the 300px fallback per §10.3.2.
1495                intrinsic.max_content_width
1496            }
1497            // +spec:intrinsic-sizing:560697 - shrink-to-fit = clamp(min-content, stretch-fit, max-content)
1498            else if get_float(styled_dom, id, node_state).unwrap_or(LayoutFloat::None) != LayoutFloat::None {
1499                // +spec:width-calculation:8d7047 - shrink-to-fit width per CSS2.1§10.3.5
1500                // +spec:width-calculation:0bb038 - shrink-to-fit for floating non-replaced elements (§10.3.5)
1501                // shrink-to-fit = min(max(preferred minimum width, available width), preferred width)
1502                // +spec:table-layout:93b13c - shrink-to-fit for floats, inline-blocks, table-cells;
1503                // orthogonal flows would require child block size as input (not yet implemented)
1504                // +spec:width-calculation:a6fd29 - shrink-to-fit width for floats: min(max(preferred minimum, available), preferred)
1505                // CSS 2.2 §10.3.5: For floats, auto width = shrink-to-fit
1506                let available_width = (containing_block_size.width
1507                    - box_props.margin.left
1508                    - box_props.margin.right
1509                    - box_props.border.left
1510                    - box_props.border.right
1511                    - box_props.padding.left
1512                    - box_props.padding.right)
1513                    .max(0.0);
1514                let preferred_minimum = intrinsic.min_content_width;
1515                let preferred = intrinsic.max_content_width;
1516                preferred_minimum.max(available_width).min(preferred).max(0.0)
1517            }
1518            else if matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed) {
1519                // +spec:intrinsic-sizing:12a531 - abspos auto size = fit-content (shrink-to-fit)
1520                // +spec:width-calculation:0bb038 - shrink-to-fit width for abs-pos non-replaced elements
1521                // §10.3.7: abs-pos elements with auto width use shrink-to-fit
1522                // +spec:intrinsic-sizing:087b57 - abspos automatic size is fit-content (shrink-to-fit)
1523                // +spec:width-calculation:1661b4 - abs-pos non-replaced auto width uses shrink-to-fit (§10.3.7)
1524                // shrink-to-fit = min(max(preferred_minimum, available), preferred)
1525                let available_width = (containing_block_size.width
1526                    - box_props.margin.left
1527                    - box_props.margin.right
1528                    - box_props.border.left
1529                    - box_props.border.right
1530                    - box_props.padding.left
1531                    - box_props.padding.right)
1532                    .max(0.0);
1533                let preferred_minimum = intrinsic.min_content_width;
1534                let preferred = intrinsic.max_content_width;
1535                preferred_minimum.max(available_width).min(preferred).max(0.0)
1536            } else {
1537            // +spec:width-calculation:472065 - orthogonal flow auto inline size: if this block
1538            // container establishes an orthogonal flow (child writing mode axis differs from
1539            // parent), its auto inline size should use the parent's block-axis size as available
1540            // space, falling back to the initial containing block size. Currently not implemented;
1541            // auto width always resolves against the containing block's width.
1542            // 'auto' width resolution depends on the display type.
1543            match display.unwrap_or_default() {
1544                LayoutDisplay::Block
1545                | LayoutDisplay::FlowRoot
1546                | LayoutDisplay::ListItem
1547                | LayoutDisplay::Flex
1548                | LayoutDisplay::Grid => {
1549                    // +spec:box-model:503ea3 - margin + border + padding + width = containing block width
1550                    // +spec:box-model:5ed651 - stretch fit: size minus margins (auto=0), border, padding, floored at 0
1551                    // +spec:box-model:33b951 - stretch-fit inline size: available space minus margins/border/padding, floored at zero
1552                    // +spec:box-model:30b4d0 - stretch fit: available size minus margins (auto as zero), border, padding, floored at zero
1553                    // +spec:width-calculation:e2c8f6 - auto width for non-replaced blocks in normal flow per CSS2.1§10.3.3
1554                    // For block-level non-replaced elements,
1555                    // 'auto' width fills the containing block (minus margins, borders, padding).
1556                    // CSS 2.2 §10.3.3: width = containing_block_width - margin_left -
1557                    // margin_right - border_left - border_right - padding_left - padding_right
1558                    // +spec:width-calculation:aef2da - auto width: other auto values become 0, width follows from constraint equality
1559                    // M12.7: compute in a small #[inline(never)] helper with by-ref/out-ptr
1560                    // args. calc_used_size is a ~6KB fn (38 maxnum, heavy SROA); the remill
1561                    // lift spills + diverges the available_width copyload feeding `.max`
1562                    // (a marker read sees 800, the maxnum's copyload reads 0 → width 0). A
1563                    // small fn has clean register allocation; out-ptr avoids the f32-return
1564                    // mis-lift. cb/bp are already &-refs (GP-pointer args lift cleanly).
1565                    // M12.7: compute the auto-width in a small f32-RETURNING helper.
1566                    // Inline-in-calc reads cb.width back 0 (huge-fn lift divergence); the
1567                    // out-ptr helper's readback was opt-forwarded to init 0. The f32
1568                    // return comes back in D0 as the call's SSA result (opt can't forward
1569                    // the init over it), and with D8-D15 preserved across calc's later
1570                    // calls the value survives to the return.
1571                    auto_block_inline_size(containing_block_size, box_props)
1572                }
1573                LayoutDisplay::InlineBlock | LayoutDisplay::InlineGrid | LayoutDisplay::InlineFlex => {
1574                    // +spec:width-calculation:c01de8 - inline-block auto width uses shrink-to-fit (§10.3.9)
1575                    // shrink-to-fit = min(max(preferred_minimum, available), preferred)
1576                    let available_width = (containing_block_size.width
1577                        - box_props.margin.left
1578                        - box_props.margin.right
1579                        - box_props.border.left
1580                        - box_props.border.right
1581                        - box_props.padding.left
1582                        - box_props.padding.right)
1583                        .max(0.0);
1584                    let preferred_minimum = intrinsic.min_content_width;
1585                    let preferred = intrinsic.max_content_width;
1586                    preferred_minimum.max(available_width).min(preferred).max(0.0)
1587                }
1588                LayoutDisplay::Inline => {
1589                    // For inline elements, 'auto' width is the intrinsic/max-content width
1590                    intrinsic.max_content_width
1591                }
1592                LayoutDisplay::Table | LayoutDisplay::InlineTable => intrinsic.max_content_width,
1593                // Table cells: during intrinsic measurement, intrinsic sizes
1594                // aren't known yet (0). Use containing block width so content
1595                // can expand and be measured. The table layout algorithm sets
1596                // the final cell width from computed column widths.
1597                LayoutDisplay::TableCell => {
1598                    if intrinsic.max_content_width > 0.0 {
1599                        intrinsic.max_content_width
1600                    } else {
1601                        (containing_block_size.width
1602                            - box_props.margin.left
1603                            - box_props.margin.right
1604                            - box_props.border.left
1605                            - box_props.border.right
1606                            - box_props.padding.left
1607                            - box_props.padding.right)
1608                            .max(0.0)
1609                    }
1610                }
1611                // Other display types use intrinsic sizing
1612                _ => intrinsic.max_content_width,
1613            }
1614            }
1615        }
1616        LayoutWidth::Px(px) => {
1617            let em = get_element_font_size(styled_dom, id, node_state);
1618            let rem = super::getters::get_root_font_size(styled_dom, node_state);
1619            let pixels_opt = super::calc::resolve_pixel_value_no_percent_with_viewport(
1620                &px, em, rem,
1621                viewport_size.width, viewport_size.height,
1622            );
1623
1624            pixels_opt.unwrap_or_else(|| {
1625                px.to_percent().map_or(intrinsic.max_content_width, |p| {
1626                    resolve_percentage_with_box_model(
1627                        containing_block_size.width,
1628                        p.get(),
1629                        (box_props.margin.left, box_props.margin.right),
1630                        (box_props.border.left, box_props.border.right),
1631                        (box_props.padding.left, box_props.padding.right),
1632                    )
1633                })
1634            })
1635        }
1636        // +spec:intrinsic-sizing:069c75 - min-content, max-content, fit-content() sizing value keywords
1637        // +spec:intrinsic-sizing:1ce4fa - §3.2 min-content/max-content/fit-content() sizing values
1638        LayoutWidth::MinContent => intrinsic.min_content_width,
1639        LayoutWidth::MaxContent => intrinsic.max_content_width,
1640        // +spec:width-calculation:7b2128 - fit-content formula and non-negative inner size flooring (css-sizing-3 §3.2)
1641        // +spec:width-calculation:bf694a - min-content, max-content, fit-content() sizing values
1642        // css-sizing-3 §3.2: fit-content(<length-percentage>) = min(max-content, max(min-content, <length-percentage>))
1643        LayoutWidth::FitContent(px) => {
1644            let em = get_element_font_size(styled_dom, id, node_state);
1645            let rem = super::getters::get_root_font_size(styled_dom, node_state);
1646            let arg = super::calc::resolve_pixel_value_with_viewport(
1647                &px, containing_block_size.width, em, rem,
1648                viewport_size.width, viewport_size.height,
1649            );
1650            intrinsic.max_content_width.min(intrinsic.min_content_width.max(arg))
1651        }
1652        LayoutWidth::Calc(items) => {
1653            use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
1654            let em = get_element_font_size(styled_dom, id, node_state);
1655            let calc_ctx = super::calc::CalcResolveContext {
1656                items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
1657            };
1658            super::calc::evaluate_calc(&calc_ctx, containing_block_size.width)
1659        }
1660    };
1661    // css-sizing-3: "the used value is floored to preserve a non-negative inner size"
1662    let resolved_width = resolved_width.max(0.0);
1663
1664    // +spec:height-calculation:7880e3 - Distinction between box types for height/margin calculation
1665    // +spec:height-calculation:753d8d - Height calculation for various box types (§10.6)
1666    // +spec:positioning:d5184e - percentage height resolved against containing block height
1667    // +spec:height-calculation:6a6cac - §10.5 content height resolution (auto, length, percentage)
1668    // +spec:height-calculation:d398e4 - §10.5/10.6 height property resolution for different box types
1669    // Step 2: Resolve the CSS `height` property into a concrete pixel value.
1670    // CSS `height` always refers to the physical vertical dimension, regardless of writing mode.
1671    // Percentage values resolve against the containing block's physical height.
1672    // In horizontal-tb: height = block size. In vertical modes: height = inline size.
1673    // The physical-to-logical mapping happens in Step 5 below.
1674    // Percentage values for `height` are resolved against the containing block's height.
1675    // +spec:height-calculation:0b5b0a - abs-pos replaced elements use intrinsic height for auto
1676    let resolved_height = match css_height.unwrap_or_default() {
1677        LayoutHeight::Auto => {
1678            // +spec:width-calculation:be5eb1 - auto height means available block space is infinite (unconstrained)
1679            // +spec:replaced-elements:994ac6 - §10.6.2: auto height for replaced elements uses intrinsic height or (used width)/ratio
1680            //
1681            // For block-level non-replaced containers in normal flow, CSS 2.2 §10.6.3
1682            // says auto height is resolved from children after layout. We return 0.0
1683            // as a placeholder; `apply_content_based_height` (cache.rs) overwrites it
1684            // with the laid-out content size. Reading `intrinsic.max_content_height`
1685            // here is unsafe: when the intrinsic pass short-circuits (e.g. a non-STF
1686            // subtree whose intrinsics are never consumed), that field is zero anyway
1687            // — so any caller that "trusts" the pre-layout value is depending on an
1688            // estimate that isn't guaranteed to exist.
1689            //
1690            // Shrink-to-fit contexts (inline-block, float, abspos, table/table-cell)
1691            // genuinely need intrinsic for width sizing; auto-height for those is
1692            // still driven by content, but we keep the intrinsic fallback for
1693            // backwards compatibility with the existing paths.
1694            // CSS 2.2 §10.6.4: an absolutely/fixed-positioned non-replaced box with
1695            // `height:auto` and BOTH `top` and `bottom` specified has a STRETCH-FIT
1696            // height = cb_height − top − bottom − margins. `position_out_of_flow_
1697            // elements` also derives this, but it runs AFTER the subtree is laid out —
1698            // so resolving it HERE (a definite, computed height) lets percentage-height
1699            // CHILDREN resolve against the real box during their own layout instead of
1700            // collapsing against a 0 placeholder. (Root cause of the slippy-map
1701            // VirtualView blank-bounds bug: its container fills via abs inset:0.)
1702            let abs_stretch_fit = if matches!(
1703                position,
1704                LayoutPosition::Absolute | LayoutPosition::Fixed
1705            ) && !is_replaced
1706            {
1707                let off = crate::solver3::positioning::resolve_position_offsets(
1708                    styled_dom, dom_id, *containing_block_size, *viewport_size,
1709                );
1710                match (off.top, off.bottom) {
1711                    (Some(t), Some(b)) => Some(
1712                        (containing_block_size.height
1713                            - t
1714                            - b
1715                            - box_props.margin.top
1716                            - box_props.margin.bottom)
1717                            .max(0.0),
1718                    ),
1719                    _ => None,
1720                }
1721            } else {
1722                None
1723            };
1724            match abs_stretch_fit {
1725                Some(h) => h,
1726                // §10.6.2: auto height for a replaced element (image / VirtualView)
1727                // uses its intrinsic height — mirrors the auto-WIDTH replaced branch
1728                // above. Without this, replaced nodes (no flow content) get 0 height
1729                // (the blank-image / "300x0" bug).
1730                None if is_replaced => intrinsic.max_content_height,
1731                None => match display.unwrap_or_default() {
1732                    LayoutDisplay::Block
1733                    | LayoutDisplay::FlowRoot
1734                    | LayoutDisplay::ListItem
1735                    | LayoutDisplay::Flex
1736                    | LayoutDisplay::Grid => 0.0,
1737                    // Inline: height property does not apply (§10.6.1), handled earlier
1738                    // via css_height override, but be explicit anyway.
1739                    LayoutDisplay::Inline => 0.0,
1740                    // Shrink-to-fit and intrinsically-sized: keep using intrinsic pre-layout.
1741                    _ => intrinsic.max_content_height,
1742                },
1743            }
1744        }
1745        LayoutHeight::Px(px) => {
1746            let em = get_element_font_size(styled_dom, id, node_state);
1747            let rem = super::getters::get_root_font_size(styled_dom, node_state);
1748            let pixels_opt = super::calc::resolve_pixel_value_no_percent_with_viewport(
1749                &px, em, rem,
1750                viewport_size.width, viewport_size.height,
1751            );
1752
1753            // +spec:height-calculation:37bc8c - percentage heights resolve against definite containing block height
1754            pixels_opt.unwrap_or_else(|| {
1755                px.to_percent().map_or(intrinsic.max_content_height, |p| {
1756                    resolve_percentage_with_box_model(
1757                        containing_block_size.height,
1758                        p.get(),
1759                        (box_props.margin.top, box_props.margin.bottom),
1760                        (box_props.border.top, box_props.border.bottom),
1761                        (box_props.padding.top, box_props.padding.bottom),
1762                    )
1763                })
1764            })
1765        }
1766        // equivalent to automatic size (not min_content_height which is height at min-content width)
1767        LayoutHeight::MinContent => intrinsic.max_content_height,
1768        // equivalent to automatic size
1769        LayoutHeight::MaxContent => intrinsic.max_content_height,
1770        // css-sizing-3 §3.2: fit-content(<length-percentage>) = min(max-content, max(min-content, <length-percentage>))
1771        // For block axis, both min-content and max-content equal auto height
1772        LayoutHeight::FitContent(px) => {
1773            let em = get_element_font_size(styled_dom, id, node_state);
1774            let rem = super::getters::get_root_font_size(styled_dom, node_state);
1775            let arg = super::calc::resolve_pixel_value_with_viewport(
1776                &px, containing_block_size.height, em, rem,
1777                viewport_size.width, viewport_size.height,
1778            );
1779            let auto_height = intrinsic.max_content_height;
1780            auto_height.min(auto_height.max(arg))
1781        }
1782        LayoutHeight::Calc(items) => {
1783            use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
1784            let em = get_element_font_size(styled_dom, id, node_state);
1785            let calc_ctx = super::calc::CalcResolveContext {
1786                items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
1787            };
1788            super::calc::evaluate_calc(&calc_ctx, containing_block_size.height)
1789        }
1790    };
1791    // css-sizing-3: "the used value is floored to preserve a non-negative inner size"
1792    let resolved_height = resolved_height.max(0.0);
1793
1794    // +spec:replaced-elements:5a85ce - abs-pos replaced: derive auto width from height × intrinsic ratio
1795    // +spec:replaced-elements:aedb26 - abs-pos replaced: both auto, ratio but no intrinsic w/h → block constraint
1796    // CSS Position 3 §6.2 (abs-replaced-width): For absolutely positioned replaced elements,
1797    // if width is auto and the element has an intrinsic ratio, width may be derived from height.
1798    let (resolved_width, resolved_height) = if is_replaced
1799        && width_is_auto
1800        && matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed)
1801    {
1802        let has_intrinsic_width = intrinsic.preferred_width.is_some_and(|w| w > 0.0);
1803        let has_intrinsic_height = intrinsic.preferred_height.is_some_and(|h| h > 0.0);
1804        let intrinsic_ratio = match (intrinsic.preferred_width, intrinsic.preferred_height) {
1805            (Some(iw), Some(ih)) if ih > 0.0 => Some(iw / ih),
1806            _ => None,
1807        };
1808
1809        intrinsic_ratio.map_or((resolved_width, resolved_height), |ratio| if height_is_auto && !has_intrinsic_width && has_intrinsic_height {
1810                // §6.2 case: both auto, no intrinsic width, has intrinsic height + ratio
1811                // → width = used height × ratio
1812                (resolved_height * ratio, resolved_height)
1813            } else if !height_is_auto {
1814                // §6.2 case: width auto, height not auto, has intrinsic ratio
1815                // → width = used height × ratio
1816                (resolved_height * ratio, resolved_height)
1817            } else if height_is_auto && !has_intrinsic_width && !has_intrinsic_height {
1818                // §6.2 case: both auto, has ratio but no intrinsic width or height
1819                // → use block-level non-replaced constraint equation for width
1820                let block_width = (containing_block_size.width
1821                    - box_props.margin.left
1822                    - box_props.margin.right
1823                    - box_props.border.left
1824                    - box_props.border.right
1825                    - box_props.padding.left
1826                    - box_props.padding.right)
1827                    .max(0.0);
1828                (block_width, block_width / ratio)
1829            } else {
1830                (resolved_width, resolved_height)
1831            })
1832    } else {
1833        (resolved_width, resolved_height)
1834    };
1835
1836    // +spec:aspect-ratio:0 - CSS Sizing 4: a non-replaced box with `aspect-ratio` and
1837    // exactly one auto axis derives the auto axis from the definite one via the ratio.
1838    // The ratio is applied to the content box here (box-sizing:border-box, which would
1839    // fold in padding+border, is not yet handled). Replaced elements use their intrinsic
1840    // ratio in the block above.
1841    #[allow(clippy::cast_precision_loss)] // small integer aspect-ratio components (e.g. 2000/1000)
1842    let (resolved_width, resolved_height) = if is_replaced {
1843        (resolved_width, resolved_height)
1844    } else if let MultiValue::Exact(azul_css::props::style::effects::StyleAspectRatio::Ratio(ar)) =
1845        crate::solver3::getters::get_aspect_ratio_property(styled_dom, id, node_state)
1846    {
1847        let ratio = if ar.height == 0 { 0.0 } else { ar.width as f32 / ar.height as f32 };
1848        if ratio > 0.0 && height_is_auto && !width_is_auto {
1849            (resolved_width, resolved_width / ratio)
1850        } else if ratio > 0.0 && width_is_auto && !height_is_auto {
1851            (resolved_height * ratio, resolved_height)
1852        } else {
1853            (resolved_width, resolved_height)
1854        }
1855    } else {
1856        (resolved_width, resolved_height)
1857    };
1858
1859    // +spec:min-max-sizing:58869e - sizing properties width/height/min-width/min-height/max-width/max-height applied here
1860    // +spec:min-max-sizing:2e2414 - max-width/max-height specify maximum box dimensions, applied here
1861    // +spec:min-max-sizing:73f51a - tentative width clamped by max-width then min-width per §10.4
1862    // +spec:min-max-sizing:e98c4e - preferred size clamped by min/max, box-sizing handled
1863    // Step 3: Apply min/max constraints (CSS 2.2 § 10.4 and § 10.7)
1864    // "The tentative used width is calculated (without 'min-width' and 'max-width')
1865    // ...If the tentative used width is greater than 'max-width', the rules above are
1866    // applied again using the computed value of 'max-width' as the computed value for 'width'.
1867    // If the resulting width is smaller than 'min-width', the rules above are applied again
1868    // using the value of 'min-width' as the computed value for 'width'."
1869
1870    // use the constraint violation table to coordinate width+height together;
1871    // for non-replaced elements, apply width and height constraints independently
1872    let has_intrinsic_ratio = intrinsic.preferred_width.is_some()
1873        && intrinsic.preferred_height.is_some()
1874        && intrinsic.preferred_width.unwrap_or(0.0) > 0.0
1875        && intrinsic.preferred_height.unwrap_or(0.0) > 0.0;
1876
1877    // +spec:margin-collapsing:840eb6 - aspect ratio transfers size constraints across dimensions
1878    let (constrained_width, constrained_height) = if has_intrinsic_ratio {
1879        // +spec:width-calculation:ef71c4 - replaced elements with both width/height auto use constraint violation table
1880        // Replaced element with intrinsic ratio: use §10.4 constraint violation table
1881        apply_constraint_violation_table(
1882            styled_dom,
1883            id,
1884            node_state,
1885            resolved_width,
1886            resolved_height,
1887            containing_block_size.width,
1888            containing_block_size.height,
1889            box_props,
1890        )
1891    } else {
1892        // Non-replaced element: apply width and height constraints independently
1893        let cw = apply_width_constraints(
1894            styled_dom,
1895            id,
1896            node_state,
1897            resolved_width,
1898            containing_block_size.width,
1899            box_props,
1900        );
1901
1902        let ch = apply_height_constraints(
1903            styled_dom,
1904            id,
1905            node_state,
1906            resolved_height,
1907            containing_block_size.height,
1908            box_props,
1909        );
1910        (cw, ch)
1911    };
1912
1913    // +spec:box-model:cc170b - box-sizing: border-box includes padding+border in specified size; content-box adds them outside; content size floored at zero
1914    // +spec:box-model:d9d797 - box-sizing: content-box vs border-box dimension interpretation
1915    // +spec:box-model:e2a773 - box-sizing: border-box includes padding+border in width/height; content-box adds them outside
1916    // +spec:box-sizing:8159a8 - box-sizing property indicates whether content-box or border-box is measured
1917    // +spec:box-sizing:b0ff05 - border-box sets border-box to specified size, content-box calculated from it
1918    // +spec:box-sizing:aefeb2 - box-sizing: content-box vs border-box width/height interpretation
1919    // +spec:box-sizing:e2e28c - width/height refer to content-box size by default (content-box); box-sizing: border-box makes them refer to border-box size
1920    // Step 4: Convert to border-box dimensions, respecting box-sizing property
1921    // CSS box-sizing:
1922    // - content-box (default): width/height set content size, border+padding are added
1923    // - border-box: width/height set border-box size, border+padding are included
1924    let box_sizing = match get_css_box_sizing(styled_dom, id, node_state) {
1925        MultiValue::Exact(bs) => bs,
1926        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
1927            azul_css::props::layout::LayoutBoxSizing::ContentBox
1928        }
1929    };
1930
1931    let (border_box_width, border_box_height) = match box_sizing {
1932        azul_css::props::layout::LayoutBoxSizing::BorderBox => {
1933            // +spec:box-sizing:cdfe09 - box-sizing: border-box makes width/height set the border box
1934            // +spec:box-sizing:3ba6d3 - content-box floors at 0px, so border-box can't be less than padding+border
1935            let min_border_box_w = box_props.padding.left
1936                + box_props.padding.right
1937                + box_props.border.left
1938                + box_props.border.right;
1939            let min_border_box_h = box_props.padding.top
1940                + box_props.padding.bottom
1941                + box_props.border.top
1942                + box_props.border.bottom;
1943            // +spec:box-model:4f423b - used values refer to the border box when box-sizing: border-box
1944            // border-box: The width/height values already include border and padding
1945            // CSS Box Sizing Level 3: "the specified width and height (and respective min/max
1946            // properties) on this element determine the border box of the element"
1947            // However, non-quantitative values (auto, min-content, max-content) are not
1948            // influenced by box-sizing, so they still need border+padding added.
1949            // Floor: content-box cannot go negative, so border-box >= padding+border
1950            let bw = if width_is_quantitative {
1951                constrained_width.max(min_border_box_w)
1952            } else {
1953                constrained_width
1954                    + box_props.padding.left
1955                    + box_props.padding.right
1956                    + box_props.border.left
1957                    + box_props.border.right
1958            };
1959            let bh = if height_is_quantitative {
1960                constrained_height.max(min_border_box_h)
1961            } else {
1962                constrained_height
1963                    + box_props.padding.top
1964                    + box_props.padding.bottom
1965                    + box_props.border.top
1966                    + box_props.border.bottom
1967            };
1968            (bw, bh)
1969        }
1970        azul_css::props::layout::LayoutBoxSizing::ContentBox => {
1971            // +spec:box-sizing:fead70 - content-box: width/height set content size, border+padding added outside
1972            let border_box_width = constrained_width
1973                + box_props.padding.left
1974                + box_props.padding.right
1975                + box_props.border.left
1976                + box_props.border.right;
1977            let border_box_height = constrained_height
1978                + box_props.padding.top
1979                + box_props.padding.bottom
1980                + box_props.border.top
1981                + box_props.border.bottom;
1982            (border_box_width, border_box_height)
1983        }
1984    };
1985
1986    // +spec:block-formatting-context:c6fb58 - vertical writing modes swap layout dimensions
1987    // +spec:min-max-sizing:d97870 - width/height/min/max refer to physical dimensions; layout rules are logical
1988    // Step 5: Map the resolved physical dimensions to logical dimensions.
1989    //
1990    // CSS Writing Modes Level 4:
1991    // - In horizontal-tb: width = inline (cross) size, height = block (main) size.
1992    // - In vertical-rl/lr: width = block (main) size, height = inline (cross) size.
1993    //
1994    // `from_main_cross` handles this mapping: given (main, cross) and writing mode,
1995    // it produces the correct LogicalSize with physical (width, height).
1996    let (main_size, cross_size) = if is_vertical {
1997        // Vertical writing mode: width is the block (main) dimension,
1998        // height is the inline (cross) dimension.
1999        (border_box_width, border_box_height)
2000    } else {
2001        // Horizontal writing mode (default): width is cross, height is main.
2002        (border_box_height, border_box_width)
2003    };
2004
2005    // Step 6: Construct the final LogicalSize from the logical dimensions.
2006    // +spec:min-max-sizing:2f66a6 - direction-dependent layout rules abstracted to logical start/end via writing mode
2007    let result =
2008        LogicalSize::from_main_cross(main_size, cross_size, writing_mode.unwrap_or_default());
2009
2010    Ok(result)
2011}
2012
2013// +spec:min-max-sizing:b02ebc - sizing properties min-width/max-width/min-height/max-height and preferred aspect ratio
2014// +spec:replaced-elements:740f3e - constraint violation table for replaced elements with intrinsic ratio and both width/height auto
2015// +spec:min-max-sizing:939f2c - use min-width/min-height <length> with aspect ratio for replaced elements
2016// with intrinsic ratios. Implements all 10 cases from the spec table, coordinating
2017// +spec:min-max-sizing:07620d - CSS 2.2 §10.4 constraint violation table for replaced elements with intrinsic ratios
2018// Implements all 11 cases from the spec table, coordinating
2019// width and height together to preserve the aspect ratio while respecting min/max constraints.
2020fn apply_constraint_violation_table(
2021    styled_dom: &StyledDom,
2022    id: NodeId,
2023    node_state: &StyledNodeState,
2024    w: f32,  // tentative width (ignoring min/max)
2025    h: f32,  // tentative height (ignoring min/max)
2026    containing_block_width: f32,
2027    containing_block_height: f32,
2028    box_props: &BoxProps,
2029) -> (f32, f32) {
2030    use crate::solver3::getters::{
2031        get_css_min_width, get_css_max_width, get_css_min_height, get_css_max_height, MultiValue,
2032    };
2033
2034    // Resolve em against the element's OWN font-size and rem against the root
2035    // font-size, NOT a hard-coded 16px.
2036    let em = get_element_font_size(styled_dom, id, node_state);
2037    let rem = super::getters::get_root_font_size(styled_dom, node_state);
2038
2039    // +spec:min-max-sizing:92ab8d - constraint violation table for replaced elements with intrinsic ratio (cyclic percentage contributions use auto fallback)
2040    // +spec:min-max-sizing:ad8605 - min-height/max-height interact with percentage heights; percentages behave as auto in intrinsic contribution calc
2041
2042    // +spec:positioning:c0af55 - automatic minimum size of abspos box is always zero (default 0.0)
2043    // Resolve min-width (default 0)
2044    let min_w = match get_css_min_width(styled_dom, id, node_state) {
2045        MultiValue::Exact(mw) => resolve_px_with_box_model(&mw.inner, containing_block_width, box_props, true, em, rem).unwrap_or(0.0),
2046        _ => 0.0,
2047    };
2048
2049    // Resolve max-width (default infinity)
2050    let max_w = match get_css_max_width(styled_dom, id, node_state) {
2051        MultiValue::Exact(mw) => {
2052            if mw.inner.number.get() >= core::f32::MAX - 1.0 {
2053                f32::MAX
2054            } else {
2055                resolve_px_with_box_model(&mw.inner, containing_block_width, box_props, true, em, rem).unwrap_or(f32::MAX)
2056            }
2057        }
2058        _ => f32::MAX,
2059    };
2060
2061    // Resolve min-height (default 0)
2062    let min_h = match get_css_min_height(styled_dom, id, node_state) {
2063        MultiValue::Exact(mh) => resolve_px_with_box_model(&mh.inner, containing_block_height, box_props, false, em, rem).unwrap_or(0.0),
2064        _ => 0.0,
2065    };
2066
2067    // Resolve max-height (default infinity)
2068    let max_h = match get_css_max_height(styled_dom, id, node_state) {
2069        MultiValue::Exact(mh) => {
2070            if mh.inner.number.get() >= core::f32::MAX - 1.0 {
2071                f32::MAX
2072            } else {
2073                resolve_px_with_box_model(&mh.inner, containing_block_height, box_props, false, em, rem).unwrap_or(f32::MAX)
2074            }
2075        }
2076        _ => f32::MAX,
2077    };
2078
2079    // max(min, max) so that min ≤ max holds true."
2080    let max_w = max_w.max(min_w);
2081    let max_h = max_h.max(min_h);
2082
2083    // Guard against zero dimensions (avoid division by zero)
2084    if w <= 0.0 || h <= 0.0 {
2085        return (w.max(min_w).min(max_w), h.max(min_h).min(max_h));
2086    }
2087
2088    let w_over = w > max_w;
2089    let w_under = w < min_w;
2090    let h_over = h > max_h;
2091    let h_under = h < min_h;
2092
2093    // +spec:min-max-sizing:713560 - constraint violation table for replaced elements with intrinsic ratio
2094    match (w_over, w_under, h_over, h_under) {
2095        // Row 1: no constraint violation
2096        (false, false, false, false) => (w, h),
2097
2098        // Row 2: w > max-width only
2099        (true, false, false, false) => {
2100            (max_w, (max_w * h / w).max(min_h))
2101        }
2102
2103        // Row 3: w < min-width only
2104        (false, true, false, false) => {
2105            (min_w, (min_w * h / w).min(max_h))
2106        }
2107
2108        // Row 4: h > max-height only
2109        (false, false, true, false) => {
2110            ((max_h * w / h).max(min_w), max_h)
2111        }
2112
2113        // Row 5: h < min-height only
2114        (false, false, false, true) => {
2115            ((min_h * w / h).min(max_w), min_h)
2116        }
2117
2118        // Row 6+7: (w > max-width) and (h > max-height)
2119        (true, false, true, false) => {
2120            if max_w / w <= max_h / h {
2121                (max_w, (max_w * h / w).max(min_h))
2122            } else {
2123                ((max_h * w / h).max(min_w), max_h)
2124            }
2125        }
2126
2127        // Row 8+9: (w < min-width) and (h < min-height)
2128        (false, true, false, true) => {
2129            if min_w / w <= min_h / h {
2130                ((min_h * w / h).min(max_w), min_h)
2131            } else {
2132                (min_w, (min_w * h / w).min(max_h))
2133            }
2134        }
2135
2136        // Row 10: (w < min-width) and (h > max-height)
2137        (false, true, true, false) => (min_w, max_h),
2138
2139        // Row 11: (w > max-width) and (h < min-height)
2140        (true, false, false, true) => (max_w, min_h),
2141
2142        // Fallback (impossible combinations like w_over && w_under)
2143        _ => (w.max(min_w).min(max_w), h.max(min_h).min(max_h)),
2144    }
2145}
2146
2147// +spec:min-max-sizing:114b53 - min-width/max-width/min-height/max-height property definitions: initial values, percentage resolution against containing block, applies to elements accepting width/height
2148// +spec:min-max-sizing:12667d - width/height/min-width/min-height/max-width/max-height properties from CSS Sizing 3
2149/// +spec:min-max-sizing:205e9e - intrinsic size constraints (min/max-content contributions, min/max sizing properties)
2150// +spec:min-max-sizing:cac146 - min-width/min-height specify minimum box dimensions; max overridden by min
2151// +spec:width-calculation:e77d58 - min/max-width clamping algorithm per CSS 2.2 § 10.4
2152// +spec:width-calculation:1d63f0 - min-width/max-width property resolution and value meanings
2153/// Apply min-width and max-width constraints to tentative width
2154/// Per CSS 2.2 § 10.4: min-width overrides max-width if min > max
2155fn apply_width_constraints(
2156    styled_dom: &StyledDom,
2157    id: NodeId,
2158    node_state: &StyledNodeState,
2159    tentative_width: f32,
2160    containing_block_width: f32,
2161    box_props: &BoxProps,
2162) -> f32 {
2163    use crate::solver3::getters::{get_css_max_width, get_css_min_width, MultiValue};
2164
2165    // Resolve em against the element's OWN font-size and rem against the root.
2166    let em = get_element_font_size(styled_dom, id, node_state);
2167    let rem = super::getters::get_root_font_size(styled_dom, node_state);
2168
2169    // +spec:display-property:0c55e5 - auto min-width resolves to 0 for CSS2 display types
2170    // Resolve min-width (default is 0)
2171    let min_width = match get_css_min_width(styled_dom, id, node_state) {
2172        MultiValue::Exact(mw) => resolve_px_with_box_model(&mw.inner, containing_block_width, box_props, true, em, rem).unwrap_or(0.0),
2173        _ => 0.0,
2174    };
2175
2176    // Resolve max-width (default is infinity/none)
2177    let max_width = match get_css_max_width(styled_dom, id, node_state) {
2178        MultiValue::Exact(mw) => {
2179            if mw.inner.number.get() >= core::f32::MAX - 1.0 {
2180                None
2181            } else {
2182                resolve_px_with_box_model(&mw.inner, containing_block_width, box_props, true, em, rem)
2183            }
2184        }
2185        _ => None,
2186    };
2187
2188    // Apply constraints: max(min_width, min(tentative, max_width))
2189    // If min > max, min wins per CSS spec
2190    let mut result = tentative_width;
2191    if let Some(max) = max_width {
2192        result = result.min(max);
2193    }
2194    result.max(min_width)
2195}
2196
2197/// Apply min-height and max-height constraints to tentative height
2198/// Per CSS 2.2 § 10.7: min-height overrides max-height if min > max
2199// +spec:height-calculation:22a77a - percentage min/max-height resolved against containing block; if CB height depends on content and element is not absolutely positioned, percentage treated as 0 (min-height) or none (max-height)
2200// +spec:height-calculation:982aaf - min-height/max-height constrain box heights to a range
2201// +spec:height-calculation:c6c33a - min-height and max-height property resolution and application
2202fn apply_height_constraints(
2203    styled_dom: &StyledDom,
2204    id: NodeId,
2205    node_state: &StyledNodeState,
2206    tentative_height: f32,
2207    containing_block_height: f32,
2208    box_props: &BoxProps,
2209) -> f32 {
2210    use crate::solver3::getters::{get_css_max_height, get_css_min_height, MultiValue};
2211
2212    // Resolve em against the element's OWN font-size and rem against the root.
2213    let em = get_element_font_size(styled_dom, id, node_state);
2214    let rem = super::getters::get_root_font_size(styled_dom, node_state);
2215
2216    // for backwards-compat with CSS2 display types (block, inline, inline-block, table)
2217    // Resolve min-height (default is 0)
2218    let min_height = match get_css_min_height(styled_dom, id, node_state) {
2219        MultiValue::Exact(mh) => resolve_px_with_box_model(&mh.inner, containing_block_height, box_props, false, em, rem).unwrap_or(0.0),
2220        _ => 0.0,
2221    };
2222
2223    // Resolve max-height (default is infinity/none)
2224    let max_height = match get_css_max_height(styled_dom, id, node_state) {
2225        MultiValue::Exact(mh) => {
2226            if mh.inner.number.get() >= core::f32::MAX - 1.0 {
2227                None
2228            } else {
2229                resolve_px_with_box_model(&mh.inner, containing_block_height, box_props, false, em, rem)
2230            }
2231        }
2232        _ => None,
2233    };
2234
2235    // +spec:height-calculation:297001 - min/max height constraint algorithm per CSS 2.2 §10.7
2236    // Apply constraints: max(min_height, min(tentative, max_height))
2237    // If min > max, min wins per CSS spec
2238    let mut result = tentative_height;
2239    if let Some(max) = max_height {
2240        result = result.min(max);
2241    }
2242    result.max(min_height)
2243}
2244
2245#[must_use] pub fn extract_text_from_node(styled_dom: &StyledDom, node_id: NodeId) -> Option<String> {
2246    match &styled_dom.node_data.as_container()[node_id].get_node_type() {
2247        NodeType::Text(text_data) => {
2248            Some(text_data.as_str().to_string())
2249        }
2250        _ => None,
2251    }
2252}
2253
2254#[cfg(test)]
2255#[allow(clippy::float_cmp, clippy::too_many_lines)]
2256mod autotest_generated {
2257    use std::collections::{BTreeMap, HashMap, HashSet};
2258
2259    use azul_core::{
2260        dom::{Dom, DomId, IdOrClass},
2261        selection::TextSelection,
2262    };
2263    use azul_css::props::basic::{FontRef, SizeMetric};
2264
2265    use super::*;
2266    use crate::solver3::{
2267        geometry::{EdgeSizes, MarginAuto, PackedBoxProps},
2268        layout_tree::{generate_layout_tree, LayoutNodeCold, LayoutNodeWarm},
2269    };
2270
2271    // ==================================================================
2272    // Fixtures
2273    // ==================================================================
2274
2275    const VIEWPORT: LogicalSize = LogicalSize {
2276        width: 800.0,
2277        height: 600.0,
2278    };
2279
2280    const BLOCK: FormattingContext = FormattingContext::Block {
2281        establishes_new_context: false,
2282    };
2283
2284    fn size(w: f32, h: f32) -> LogicalSize {
2285        LogicalSize::new(w, h)
2286    }
2287
2288    fn all_edges(v: f32) -> EdgeSizes {
2289        EdgeSizes {
2290            top: v,
2291            right: v,
2292            bottom: v,
2293            left: v,
2294        }
2295    }
2296
2297    /// `BoxProps` with the same value on every edge of each ring.
2298    fn props(margin: f32, border: f32, padding: f32) -> BoxProps {
2299        BoxProps {
2300            margin: all_edges(margin),
2301            border: all_edges(border),
2302            padding: all_edges(padding),
2303            margin_auto: MarginAuto::default(),
2304        }
2305    }
2306
2307    fn zero_props() -> BoxProps {
2308        props(0.0, 0.0, 0.0)
2309    }
2310
2311    fn isz(min_w: f32, max_w: f32, min_h: f32, max_h: f32) -> IntrinsicSizes {
2312        IntrinsicSizes {
2313            min_content_width: min_w,
2314            max_content_width: max_w,
2315            preferred_width: None,
2316            min_content_height: min_h,
2317            max_content_height: max_h,
2318            preferred_height: None,
2319            preferred_aspect_ratio: None,
2320        }
2321    }
2322
2323    fn styled(dom: Dom, css_str: &str) -> StyledDom {
2324        let mut dom = dom;
2325        let (css, _warnings) = azul_css::parser2::new_from_str(css_str);
2326        StyledDom::create(&mut dom, css)
2327    }
2328
2329    fn div_class(class: &str) -> Dom {
2330        Dom::create_div().with_ids_and_classes(vec![IdOrClass::Class(class.into())].into())
2331    }
2332
2333    /// Owns everything a `LayoutContext` borrows. A font-less `FontManager`
2334    /// (empty `FcFontCache`) is enough for every function exercised here:
2335    /// text *shaping* is never reached — the DOM fixtures that go through the
2336    /// intrinsic-sizing pass carry no text nodes, and the text fixtures only
2337    /// go through `collect_inline_content`, which gathers but never measures.
2338    struct Env {
2339        styled_dom: StyledDom,
2340        font_manager: FontManager<FontRef>,
2341        text_selections: BTreeMap<DomId, TextSelection>,
2342        counters: HashMap<(usize, String), i32>,
2343        image_cache: azul_core::resources::ImageCache,
2344        debug_messages: Option<Vec<LayoutDebugMessage>>,
2345    }
2346
2347    impl Env {
2348        fn new(styled_dom: StyledDom) -> Self {
2349            Self {
2350                styled_dom,
2351                font_manager: FontManager::new(FcFontCache::default())
2352                    .expect("FontManager over an empty font cache"),
2353                text_selections: BTreeMap::new(),
2354                counters: HashMap::new(),
2355                image_cache: azul_core::resources::ImageCache::default(),
2356                debug_messages: None,
2357            }
2358        }
2359
2360        fn ctx(&mut self) -> LayoutContext<'_, FontRef> {
2361            LayoutContext {
2362                scrollbar_style_cache: core::cell::RefCell::new(HashMap::new()),
2363                styled_dom: &self.styled_dom,
2364                font_manager: &self.font_manager,
2365                text_selections: &self.text_selections,
2366                debug_messages: &mut self.debug_messages,
2367                counters: &mut self.counters,
2368                viewport_size: VIEWPORT,
2369                fragmentation_context: None,
2370                cursor_is_visible: true,
2371                cursor_locations: Vec::new(),
2372                preedit_text: None,
2373                dirty_text_overrides: BTreeMap::new(),
2374                cache_map: crate::solver3::cache::LayoutCacheMap::default(),
2375                image_cache: &self.image_cache,
2376                system_style: None,
2377                get_system_time_fn: azul_core::task::GetSystemTimeCallback {
2378                    cb: azul_core::task::get_system_time_libstd,
2379                },
2380            }
2381        }
2382    }
2383
2384    fn hot(parent: Option<usize>, fc: FormattingContext, bp: &BoxProps) -> LayoutNodeHot {
2385        LayoutNodeHot {
2386            box_props: PackedBoxProps::pack(bp),
2387            dom_node_id: None,
2388            used_size: None,
2389            formatting_context: fc,
2390            parent,
2391        }
2392    }
2393
2394    /// Hand-builds a `LayoutTree` (SoA invariants kept consistent) from hot
2395    /// nodes + per-node child lists. `dom_node_id` is `None` throughout, which
2396    /// is exactly the "anonymous box" path through the sizing code.
2397    fn tree_of(nodes: Vec<LayoutNodeHot>, child_lists: &[Vec<usize>]) -> LayoutTree {
2398        let n = nodes.len();
2399        let mut children_arena: Vec<usize> = Vec::new();
2400        let mut children_offsets: Vec<(u32, u32)> = Vec::with_capacity(n);
2401        for cl in child_lists {
2402            let start = u32::try_from(children_arena.len()).expect("arena fits in u32");
2403            children_arena.extend_from_slice(cl);
2404            children_offsets.push((start, u32::try_from(cl.len()).expect("len fits in u32")));
2405        }
2406        while children_offsets.len() < n {
2407            children_offsets.push((0, 0));
2408        }
2409        LayoutTree {
2410            nodes,
2411            warm: vec![LayoutNodeWarm::default(); n],
2412            cold: vec![LayoutNodeCold::default(); n],
2413            root: 0,
2414            dom_to_layout: BTreeMap::new(),
2415            children_arena,
2416            children_offsets,
2417            subtree_needs_intrinsic: Vec::new(),
2418        }
2419    }
2420
2421    /// The single layout index of a DOM node (fixtures never produce splits).
2422    fn layout_index(tree: &LayoutTree, dom_id: NodeId) -> usize {
2423        *tree
2424            .dom_to_layout
2425            .get(&dom_id)
2426            .and_then(|v| v.first())
2427            .expect("DOM node has a layout node")
2428    }
2429
2430    // ==================================================================
2431    // resolve_percentage_with_box_model  (numeric)
2432    // ==================================================================
2433
2434    #[test]
2435    fn resolve_percentage_at_zero_is_zero_on_both_operands() {
2436        assert_eq!(
2437            resolve_percentage_with_box_model(0.0, 0.5, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)),
2438            0.0
2439        );
2440        assert_eq!(
2441            resolve_percentage_with_box_model(800.0, 0.0, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)),
2442            0.0
2443        );
2444    }
2445
2446    #[test]
2447    fn resolve_percentage_ignores_the_box_model_arguments_entirely() {
2448        // Documented contract: margins/borders/paddings are accepted for
2449        // call-site convenience and MUST NOT influence the result (CSS 2.1
2450        // §10.2 — percentages resolve against the containing block itself).
2451        let plain =
2452            resolve_percentage_with_box_model(800.0, 0.5, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0));
2453        let poisoned = resolve_percentage_with_box_model(
2454            800.0,
2455            0.5,
2456            (f32::NAN, f32::INFINITY),
2457            (f32::MAX, f32::MIN),
2458            (-1e30, 1e30),
2459        );
2460        assert_eq!(plain, 400.0);
2461        assert_eq!(poisoned, 400.0, "box-model args must not leak into the result");
2462    }
2463
2464    #[test]
2465    fn resolve_percentage_floors_negative_products_at_zero() {
2466        // +spec:containing-block:f1344e — negative CB width yields zero.
2467        assert_eq!(
2468            resolve_percentage_with_box_model(-800.0, 0.5, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)),
2469            0.0
2470        );
2471        assert_eq!(
2472            resolve_percentage_with_box_model(800.0, -0.5, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)),
2473            0.0
2474        );
2475        // Two negatives multiply back to a positive — still deterministic.
2476        assert_eq!(
2477            resolve_percentage_with_box_model(-800.0, -0.5, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0)),
2478            400.0
2479        );
2480    }
2481
2482    #[test]
2483    fn resolve_percentage_never_returns_nan() {
2484        // f32::max(NaN, 0.0) == 0.0, so every NaN-producing combination
2485        // (NaN operand, or inf * 0 == NaN) collapses to a defined 0.0.
2486        for (cb, pct) in [
2487            (f32::NAN, 0.5),
2488            (800.0, f32::NAN),
2489            (f32::NAN, f32::NAN),
2490            (f32::INFINITY, 0.0),
2491            (f32::NEG_INFINITY, 0.0),
2492            (f32::NEG_INFINITY, 0.5),
2493        ] {
2494            let r = resolve_percentage_with_box_model(cb, pct, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0));
2495            assert!(!r.is_nan(), "NaN escaped for cb={cb}, pct={pct}");
2496            assert_eq!(r, 0.0, "cb={cb}, pct={pct}");
2497        }
2498    }
2499
2500    #[test]
2501    fn resolve_percentage_saturates_to_infinity_on_overflow() {
2502        // f32::MAX * 100 overflows to +inf — saturation, not a panic.
2503        let r =
2504            resolve_percentage_with_box_model(f32::MAX, 100.0, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0));
2505        assert!(r.is_infinite() && r.is_sign_positive());
2506        // An infinite CB with a finite non-zero percentage stays infinite.
2507        let r = resolve_percentage_with_box_model(
2508            f32::INFINITY,
2509            0.5,
2510            (0.0, 0.0),
2511            (0.0, 0.0),
2512            (0.0, 0.0),
2513        );
2514        assert!(r.is_infinite() && r.is_sign_positive());
2515    }
2516
2517    #[test]
2518    fn resolve_percentage_is_monotone_in_the_percentage() {
2519        let at = |p: f32| {
2520            resolve_percentage_with_box_model(800.0, p, (0.0, 0.0), (0.0, 0.0), (0.0, 0.0))
2521        };
2522        assert!(at(0.0) <= at(0.25) && at(0.25) <= at(0.5) && at(0.5) <= at(1.0));
2523        assert_eq!(at(1.0), 800.0);
2524    }
2525
2526    // ==================================================================
2527    // resolve_px_with_box_model  (numeric)
2528    // ==================================================================
2529
2530    #[test]
2531    fn resolve_px_absolute_length_ignores_the_containing_block() {
2532        let bp = props(7.0, 3.0, 11.0);
2533        let px = PixelValue::const_px(50);
2534        assert_eq!(
2535            resolve_px_with_box_model(&px, 800.0, &bp, true, 16.0, 16.0),
2536            Some(50.0)
2537        );
2538        // A wildly different containing block cannot move an absolute length.
2539        assert_eq!(
2540            resolve_px_with_box_model(&px, -1.0e30, &bp, false, 16.0, 16.0),
2541            Some(50.0)
2542        );
2543    }
2544
2545    #[test]
2546    fn resolve_px_percentage_resolves_against_the_containing_block_on_either_axis() {
2547        let bp = props(10.0, 2.0, 5.0);
2548        let px = PixelValue::const_percent(50);
2549        let horizontal = resolve_px_with_box_model(&px, 800.0, &bp, true, 16.0, 16.0);
2550        let vertical = resolve_px_with_box_model(&px, 800.0, &bp, false, 16.0, 16.0);
2551        assert_eq!(horizontal, Some(400.0));
2552        // `is_horizontal` picks which box-model edges are passed down, but the
2553        // resolver discards them — so both axes agree for the same CB extent.
2554        assert_eq!(vertical, horizontal);
2555    }
2556
2557    #[test]
2558    fn resolve_px_percentage_against_a_degenerate_containing_block_is_zero() {
2559        let bp = zero_props();
2560        let px = PixelValue::const_percent(50);
2561        for cb in [-800.0, f32::NAN, f32::NEG_INFINITY] {
2562            let r = resolve_px_with_box_model(&px, cb, &bp, true, 16.0, 16.0)
2563                .expect("a percentage always resolves to Some");
2564            assert!(!r.is_nan(), "NaN escaped for cb={cb}");
2565            assert_eq!(r, 0.0, "cb={cb}");
2566        }
2567    }
2568
2569    #[test]
2570    fn resolve_px_em_and_rem_resolve_against_the_supplied_font_sizes() {
2571        let bp = zero_props();
2572        assert_eq!(
2573            resolve_px_with_box_model(&PixelValue::const_em(3), 800.0, &bp, true, 20.0, 16.0),
2574            Some(60.0)
2575        );
2576        assert_eq!(
2577            resolve_px_with_box_model(
2578                &PixelValue::from_metric(SizeMetric::Rem, 2.0),
2579                800.0,
2580                &bp,
2581                true,
2582                20.0,
2583                16.0
2584            ),
2585            Some(32.0)
2586        );
2587        // A zero font-size collapses em to 0 rather than producing NaN.
2588        assert_eq!(
2589            resolve_px_with_box_model(&PixelValue::const_em(3), 800.0, &bp, true, 0.0, 0.0),
2590            Some(0.0)
2591        );
2592    }
2593
2594    #[test]
2595    fn resolve_px_returns_none_for_viewport_units() {
2596        // Viewport units are neither absolute (no viewport is threaded in here)
2597        // nor percentages, so this resolver reports "cannot resolve". Every
2598        // caller (`apply_width_constraints`, `apply_height_constraints`,
2599        // `apply_constraint_violation_table`) turns that into the *default*
2600        // (0 / none), i.e. a `min-width: 10vw` constraint is silently dropped.
2601        let bp = zero_props();
2602        for metric in [
2603            SizeMetric::Vw,
2604            SizeMetric::Vh,
2605            SizeMetric::Vmin,
2606            SizeMetric::Vmax,
2607        ] {
2608            let px = PixelValue::from_metric(metric, 10.0);
2609            assert_eq!(
2610                resolve_px_with_box_model(&px, 800.0, &bp, true, 16.0, 16.0),
2611                None,
2612                "{metric:?} unexpectedly resolved"
2613            );
2614        }
2615    }
2616
2617    #[test]
2618    fn resolve_px_extreme_lengths_stay_finite() {
2619        // `PixelValue` stores its number as a fixed-point isize, so f32
2620        // extremes are clamped at construction — nothing infinite or NaN can
2621        // reach the sizing math through a `PixelValue`.
2622        let bp = zero_props();
2623        for raw in [f32::MAX, f32::MIN, f32::INFINITY, f32::NEG_INFINITY, f32::NAN] {
2624            let px = PixelValue::px(raw);
2625            let r = resolve_px_with_box_model(&px, 800.0, &bp, true, 16.0, 16.0)
2626                .expect("px metric always resolves to Some");
2627            assert!(r.is_finite(), "non-finite length from PixelValue::px({raw})");
2628        }
2629        assert_eq!(
2630            resolve_px_with_box_model(&PixelValue::px(f32::NAN), 800.0, &bp, true, 16.0, 16.0),
2631            Some(0.0),
2632            "NaN saturates to 0 in the fixed-point encoding"
2633        );
2634    }
2635
2636    // ==================================================================
2637    // auto_block_inline_size  (numeric)
2638    // ==================================================================
2639
2640    #[test]
2641    fn auto_block_inline_size_subtracts_the_full_horizontal_box_model() {
2642        let cb = size(800.0, 600.0);
2643        // margin 10 + border 2 + padding 5, on both sides = 34 total.
2644        assert_eq!(auto_block_inline_size(&cb, &props(10.0, 2.0, 5.0)), 766.0);
2645        assert_eq!(auto_block_inline_size(&cb, &zero_props()), 800.0);
2646    }
2647
2648    #[test]
2649    fn auto_block_inline_size_floors_at_zero_when_the_box_model_exceeds_the_cb() {
2650        let cb = size(10.0, 600.0);
2651        assert_eq!(auto_block_inline_size(&cb, &props(100.0, 50.0, 25.0)), 0.0);
2652        // A zero-width containing block is exactly the boundary case.
2653        assert_eq!(auto_block_inline_size(&size(0.0, 0.0), &zero_props()), 0.0);
2654        // A negative containing block never yields a negative inline size.
2655        assert_eq!(auto_block_inline_size(&size(-800.0, 0.0), &zero_props()), 0.0);
2656    }
2657
2658    #[test]
2659    fn auto_block_inline_size_never_returns_nan() {
2660        let cases = [
2661            (size(f32::NAN, 0.0), zero_props()),
2662            (size(f32::INFINITY, 0.0), props(f32::INFINITY, 0.0, 0.0)),
2663            (size(f32::NEG_INFINITY, 0.0), zero_props()),
2664            (size(0.0, 0.0), props(f32::NAN, 0.0, 0.0)),
2665        ];
2666        for (cb, bp) in cases {
2667            let r = auto_block_inline_size(&cb, &bp);
2668            assert!(!r.is_nan(), "NaN escaped for cb.width={}", cb.width);
2669            assert_eq!(r, 0.0);
2670        }
2671    }
2672
2673    #[test]
2674    fn auto_block_inline_size_saturates_rather_than_overflowing() {
2675        // f32::MAX minus finite edges stays finite; +inf CB stays +inf.
2676        let r = auto_block_inline_size(&size(f32::MAX, 0.0), &props(1.0, 1.0, 1.0));
2677        assert!(r.is_finite() && r > 0.0);
2678        let r = auto_block_inline_size(&size(f32::INFINITY, 0.0), &props(1.0, 1.0, 1.0));
2679        assert!(r.is_infinite() && r.is_sign_positive());
2680    }
2681
2682    // ==================================================================
2683    // compute_dirty_ancestor_closure  (other)
2684    // ==================================================================
2685
2686    /// 0 → 1 → 2 (2's parent is 1, 1's parent is 0).
2687    fn chain_tree() -> LayoutTree {
2688        let bp = zero_props();
2689        tree_of(
2690            vec![
2691                hot(None, BLOCK, &bp),
2692                hot(Some(0), BLOCK, &bp),
2693                hot(Some(1), BLOCK, &bp),
2694            ],
2695            &[vec![1], vec![2], vec![]],
2696        )
2697    }
2698
2699    #[test]
2700    fn dirty_closure_of_an_empty_set_is_empty() {
2701        let tree = chain_tree();
2702        let closure = compute_dirty_ancestor_closure(&tree, &BTreeSet::new());
2703        assert!(closure.is_empty());
2704    }
2705
2706    #[test]
2707    fn dirty_closure_of_a_leaf_contains_every_ancestor_up_to_the_root() {
2708        let tree = chain_tree();
2709        let dirty: BTreeSet<usize> = [2].into_iter().collect();
2710        let closure = compute_dirty_ancestor_closure(&tree, &dirty);
2711        assert_eq!(closure, [0, 1, 2].into_iter().collect::<HashSet<usize>>());
2712    }
2713
2714    #[test]
2715    fn dirty_closure_tolerates_out_of_range_dirty_indices() {
2716        let tree = chain_tree();
2717        let dirty: BTreeSet<usize> = [usize::MAX, 999, 2].into_iter().collect();
2718        let closure = compute_dirty_ancestor_closure(&tree, &dirty);
2719        // The bogus ids are inserted (they have no parent to walk), the real
2720        // one still drags in its ancestors. No panic, no index arithmetic.
2721        assert!(closure.contains(&usize::MAX) && closure.contains(&999));
2722        assert!(closure.contains(&0) && closure.contains(&1) && closure.contains(&2));
2723    }
2724
2725    #[test]
2726    fn dirty_closure_terminates_on_a_cyclic_parent_chain() {
2727        // A malformed tree (0's parent is 1, 1's parent is 0) must not spin
2728        // forever: the `insert` returning false breaks the walk.
2729        let bp = zero_props();
2730        let tree = tree_of(
2731            vec![hot(Some(1), BLOCK, &bp), hot(Some(0), BLOCK, &bp)],
2732            &[vec![], vec![]],
2733        );
2734        let dirty: BTreeSet<usize> = [0].into_iter().collect();
2735        let closure = compute_dirty_ancestor_closure(&tree, &dirty);
2736        assert_eq!(closure, [0, 1].into_iter().collect::<HashSet<usize>>());
2737    }
2738
2739    #[test]
2740    fn dirty_closure_terminates_on_a_self_parenting_node() {
2741        let bp = zero_props();
2742        let tree = tree_of(vec![hot(Some(0), BLOCK, &bp)], &[vec![]]);
2743        let dirty: BTreeSet<usize> = [0].into_iter().collect();
2744        let closure = compute_dirty_ancestor_closure(&tree, &dirty);
2745        assert_eq!(closure, [0].into_iter().collect::<HashSet<usize>>());
2746    }
2747
2748    // ==================================================================
2749    // IntrinsicSizeCalculator::new  (constructor)
2750    // ==================================================================
2751
2752    #[test]
2753    fn intrinsic_size_calculator_new_starts_without_a_dirty_closure() {
2754        let mut env = Env::new(styled(Dom::create_body(), ""));
2755        let mut ctx = env.ctx();
2756        let mut text_cache = LayoutCache::new();
2757        let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2758        assert!(
2759            calc.dirty_closure.is_none(),
2760            "a fresh calculator must not skip any node"
2761        );
2762        assert_eq!(calc.ctx.viewport_size, VIEWPORT, "ctx is threaded through");
2763    }
2764
2765    // ==================================================================
2766    // calculate_intrinsic_recursive / calculate_node_intrinsic_sizes
2767    // ==================================================================
2768
2769    #[test]
2770    fn calculate_intrinsic_recursive_rejects_an_out_of_range_node_index() {
2771        let mut env = Env::new(styled(Dom::create_body(), ""));
2772        let mut ctx = env.ctx();
2773        let mut text_cache = LayoutCache::new();
2774        let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2775        let mut tree = chain_tree();
2776
2777        for bogus in [3, 999, usize::MAX] {
2778            let r = calc.calculate_intrinsic_recursive(&mut tree, bogus, false);
2779            assert!(
2780                matches!(r, Err(LayoutError::InvalidTree)),
2781                "index {bogus} must be rejected, not panic"
2782            );
2783        }
2784    }
2785
2786    #[test]
2787    fn calculate_node_intrinsic_sizes_rejects_an_out_of_range_node_index() {
2788        let mut env = Env::new(styled(Dom::create_body(), ""));
2789        let mut ctx = env.ctx();
2790        let mut text_cache = LayoutCache::new();
2791        let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2792        let tree = chain_tree();
2793        let r = calc.calculate_node_intrinsic_sizes(&tree, usize::MAX, &[]);
2794        assert!(matches!(r, Err(LayoutError::InvalidTree)));
2795    }
2796
2797    #[test]
2798    fn calculate_intrinsic_recursive_skips_stray_child_indices_instead_of_aborting() {
2799        // Reconcile can mis-list a child index that has no node (the g52 case).
2800        // The whole pass must survive it, not abort with InvalidTree.
2801        let bp = zero_props();
2802        let mut tree = tree_of(
2803            vec![hot(None, BLOCK, &bp), hot(Some(0), BLOCK, &bp)],
2804            &[vec![1, 4242], vec![]],
2805        );
2806        let mut env = Env::new(styled(Dom::create_body(), ""));
2807        let mut ctx = env.ctx();
2808        let mut text_cache = LayoutCache::new();
2809        let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2810
2811        let r = calc.calculate_intrinsic_recursive(&mut tree, 0, false);
2812        let sizes = r.expect("a stray child index must be skipped, not fatal");
2813        assert!(sizes.min_content_width.is_finite());
2814        assert!(tree.warm(0).and_then(|w| w.intrinsic_sizes).is_some());
2815    }
2816
2817    #[test]
2818    fn calculate_intrinsic_recursive_reuses_the_cache_for_nodes_outside_the_dirty_closure() {
2819        let mut tree = chain_tree();
2820        let cached = isz(11.0, 22.0, 33.0, 44.0);
2821        tree.warm_mut(0)
2822            .expect("root warm slot")
2823            .intrinsic_sizes = Some(cached);
2824
2825        let mut env = Env::new(styled(Dom::create_body(), ""));
2826        let mut ctx = env.ctx();
2827        let mut text_cache = LayoutCache::new();
2828        let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2829        // An empty closure means "nothing is dirty" — the cached value wins
2830        // and the descent is skipped entirely.
2831        calc.dirty_closure = Some(HashSet::new());
2832
2833        let sizes = calc
2834            .calculate_intrinsic_recursive(&mut tree, 0, false)
2835            .expect("cached path");
2836        assert_eq!(sizes.min_content_width, 11.0);
2837        assert_eq!(sizes.max_content_width, 22.0);
2838        assert_eq!(sizes.min_content_height, 33.0);
2839        assert_eq!(sizes.max_content_height, 44.0);
2840        // Children were never visited, so their warm slots stay empty.
2841        assert!(tree.warm(1).and_then(|w| w.intrinsic_sizes).is_none());
2842    }
2843
2844    // ==================================================================
2845    // calculate_block_intrinsic_sizes  (numeric)
2846    // ==================================================================
2847
2848    /// root(0) with `n` block children, no box-model extras.
2849    fn block_parent_with_children(n: usize) -> LayoutTree {
2850        parent_with_children(BLOCK, n)
2851    }
2852
2853    /// root(0) establishing `fc`, with `n` childless block children.
2854    fn parent_with_children(fc: FormattingContext, n: usize) -> LayoutTree {
2855        let bp = zero_props();
2856        let mut nodes = vec![hot(None, fc, &bp)];
2857        let mut kids = Vec::new();
2858        for i in 0..n {
2859            nodes.push(hot(Some(0), BLOCK, &bp));
2860            kids.push(i + 1);
2861        }
2862        let mut child_lists = vec![kids];
2863        child_lists.resize(n + 1, Vec::new());
2864        tree_of(nodes, &child_lists)
2865    }
2866
2867    #[test]
2868    fn block_intrinsic_sizes_take_the_max_width_and_the_sum_of_heights() {
2869        let tree = block_parent_with_children(2);
2870        let mut env = Env::new(styled(Dom::create_body(), ""));
2871        let mut ctx = env.ctx();
2872        let mut text_cache = LayoutCache::new();
2873        let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2874
2875        let children = [(1usize, isz(10.0, 20.0, 5.0, 6.0)), (2usize, isz(30.0, 40.0, 7.0, 8.0))];
2876        let r = calc
2877            .calculate_block_intrinsic_sizes(&tree, 0, &children)
2878            .expect("valid tree");
2879        assert_eq!(r.min_content_width, 30.0, "cross axis = widest child");
2880        assert_eq!(r.max_content_width, 40.0);
2881        assert_eq!(r.min_content_height, 14.0, "main axis = stacked heights");
2882        assert_eq!(r.max_content_height, 14.0);
2883    }
2884
2885    #[test]
2886    fn block_intrinsic_sizes_ignore_children_missing_from_the_intrinsics_slice() {
2887        let tree = block_parent_with_children(2);
2888        let mut env = Env::new(styled(Dom::create_body(), ""));
2889        let mut ctx = env.ctx();
2890        let mut text_cache = LayoutCache::new();
2891        let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2892
2893        let r = calc
2894            .calculate_block_intrinsic_sizes(&tree, 0, &[])
2895            .expect("valid tree");
2896        assert_eq!(r.min_content_width, 0.0);
2897        assert_eq!(r.max_content_width, 0.0);
2898        assert_eq!(r.min_content_height, 0.0);
2899        assert_eq!(r.max_content_height, 0.0);
2900    }
2901
2902    #[test]
2903    fn block_intrinsic_sizes_saturate_to_infinity_instead_of_overflowing() {
2904        let tree = block_parent_with_children(2);
2905        let mut env = Env::new(styled(Dom::create_body(), ""));
2906        let mut ctx = env.ctx();
2907        let mut text_cache = LayoutCache::new();
2908        let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2909
2910        let huge = isz(f32::MAX, f32::MAX, f32::MAX, f32::MAX);
2911        let children = [(1usize, huge), (2usize, huge)];
2912        let r = calc
2913            .calculate_block_intrinsic_sizes(&tree, 0, &children)
2914            .expect("valid tree");
2915        // MAX + MAX overflows the f32 range: +inf, not a wrap, not a panic.
2916        assert!(r.min_content_height.is_infinite() && r.min_content_height.is_sign_positive());
2917        assert_eq!(r.max_content_width, f32::MAX, "cross axis only takes a max");
2918    }
2919
2920    #[test]
2921    fn block_intrinsic_sizes_sanitize_nan_on_the_cross_axis() {
2922        let tree = block_parent_with_children(1);
2923        let mut env = Env::new(styled(Dom::create_body(), ""));
2924        let mut ctx = env.ctx();
2925        let mut text_cache = LayoutCache::new();
2926        let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2927
2928        let nan = isz(f32::NAN, f32::NAN, f32::NAN, f32::NAN);
2929        let r = calc
2930            .calculate_block_intrinsic_sizes(&tree, 0, &[(1usize, nan)])
2931            .expect("valid tree");
2932        // The cross axis goes through `f32::max`, which drops NaN — so a NaN
2933        // child cannot poison the parent's width. The main axis is a plain
2934        // sum, so it does carry the NaN through (unreachable in practice:
2935        // every measured/fallback intrinsic is finite).
2936        assert!(!r.min_content_width.is_nan() && r.min_content_width == 0.0);
2937        assert!(!r.max_content_width.is_nan() && r.max_content_width == 0.0);
2938        assert!(r.min_content_height.is_nan());
2939    }
2940
2941    #[test]
2942    fn block_intrinsic_sizes_reject_an_out_of_range_node_index() {
2943        let tree = block_parent_with_children(1);
2944        let mut env = Env::new(styled(Dom::create_body(), ""));
2945        let mut ctx = env.ctx();
2946        let mut text_cache = LayoutCache::new();
2947        let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2948        let r = calc.calculate_block_intrinsic_sizes(&tree, usize::MAX, &[]);
2949        assert!(matches!(r, Err(LayoutError::InvalidTree)));
2950    }
2951
2952    // ==================================================================
2953    // calculate_flex_intrinsic_sizes  (numeric)
2954    // ==================================================================
2955
2956    fn flex_parent_with_children(n: usize) -> LayoutTree {
2957        parent_with_children(FormattingContext::Flex, n)
2958    }
2959
2960    #[test]
2961    fn flex_row_intrinsic_sizes_sum_the_main_axis_and_max_the_cross_axis() {
2962        let tree = flex_parent_with_children(2);
2963        let mut env = Env::new(styled(Dom::create_body(), ""));
2964        let mut ctx = env.ctx();
2965        let mut text_cache = LayoutCache::new();
2966        let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2967
2968        let children = [(1usize, isz(10.0, 20.0, 5.0, 6.0)), (2usize, isz(30.0, 40.0, 7.0, 8.0))];
2969        let r = calc
2970            .calculate_flex_intrinsic_sizes(&tree, 0, &children)
2971            .expect("valid tree");
2972        // No DOM node → default flex-direction: row, default flex-wrap: nowrap
2973        // → single line → min-content main = SUM of item min-contents.
2974        assert_eq!(r.min_content_width, 40.0);
2975        assert_eq!(r.max_content_width, 60.0);
2976        assert_eq!(r.min_content_height, 7.0);
2977        assert_eq!(r.max_content_height, 8.0);
2978    }
2979
2980    #[test]
2981    fn flex_intrinsic_sizes_are_zero_when_no_child_intrinsics_are_supplied() {
2982        let tree = flex_parent_with_children(3);
2983        let mut env = Env::new(styled(Dom::create_body(), ""));
2984        let mut ctx = env.ctx();
2985        let mut text_cache = LayoutCache::new();
2986        let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
2987
2988        let r = calc
2989            .calculate_flex_intrinsic_sizes(&tree, 0, &[])
2990            .expect("valid tree");
2991        assert_eq!(r.min_content_width, 0.0);
2992        assert_eq!(r.max_content_width, 0.0);
2993        assert_eq!(r.min_content_height, 0.0);
2994        assert_eq!(r.max_content_height, 0.0);
2995    }
2996
2997    #[test]
2998    fn flex_intrinsic_sizes_saturate_on_a_summing_overflow() {
2999        let tree = flex_parent_with_children(2);
3000        let mut env = Env::new(styled(Dom::create_body(), ""));
3001        let mut ctx = env.ctx();
3002        let mut text_cache = LayoutCache::new();
3003        let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
3004
3005        let huge = isz(f32::MAX, f32::MAX, 1.0, 2.0);
3006        let children = [(1usize, huge), (2usize, huge)];
3007        let r = calc
3008            .calculate_flex_intrinsic_sizes(&tree, 0, &children)
3009            .expect("valid tree");
3010        assert!(r.min_content_width.is_infinite() && r.min_content_width.is_sign_positive());
3011        assert!(r.max_content_width.is_infinite() && r.max_content_width.is_sign_positive());
3012        // The cross axis only takes maxima, so it stays finite.
3013        assert_eq!(r.max_content_height, 2.0);
3014    }
3015
3016    #[test]
3017    fn flex_intrinsic_sizes_reject_an_out_of_range_node_index() {
3018        let tree = flex_parent_with_children(1);
3019        let mut env = Env::new(styled(Dom::create_body(), ""));
3020        let mut ctx = env.ctx();
3021        let mut text_cache = LayoutCache::new();
3022        let calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
3023        let r = calc.calculate_flex_intrinsic_sizes(&tree, usize::MAX, &[]);
3024        assert!(matches!(r, Err(LayoutError::InvalidTree)));
3025    }
3026
3027    // ==================================================================
3028    // calculate_table_intrinsic_sizes  (numeric)
3029    // ==================================================================
3030
3031    /// table(0) > row(1) > [cell(2), cell(3)]
3032    fn table_tree() -> LayoutTree {
3033        let bp = zero_props();
3034        tree_of(
3035            vec![
3036                hot(None, FormattingContext::Table, &bp),
3037                hot(Some(0), FormattingContext::TableRow, &bp),
3038                hot(Some(1), FormattingContext::TableCell, &bp),
3039                hot(Some(1), FormattingContext::TableCell, &bp),
3040            ],
3041            &[vec![1], vec![2, 3], vec![], vec![]],
3042        )
3043    }
3044
3045    #[test]
3046    fn table_intrinsic_sizes_sum_columns_and_stack_row_heights() {
3047        let tree = table_tree();
3048        let mut env = Env::new(styled(Dom::create_body(), ""));
3049        let mut ctx = env.ctx();
3050        let mut text_cache = LayoutCache::new();
3051        let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
3052
3053        // Cell intrinsics keyed by the *cell* indices (the aggregation path).
3054        let cells = [(2usize, isz(30.0, 50.0, 10.0, 20.0)), (3usize, isz(40.0, 60.0, 10.0, 15.0))];
3055        let r = calc.calculate_table_intrinsic_sizes(&tree, 0, &cells);
3056        assert_eq!(r.min_content_width, 70.0, "sum of per-column minima");
3057        assert_eq!(r.max_content_width, 110.0, "sum of per-column maxima");
3058        assert_eq!(r.min_content_height, 20.0, "row height = tallest cell");
3059        assert_eq!(r.max_content_height, 20.0);
3060    }
3061
3062    #[test]
3063    fn table_intrinsic_sizes_are_zero_when_cells_carry_no_measurable_content() {
3064        // The real caller passes the table's *direct* children (rows), so cell
3065        // lookups miss and each cell is re-measured through the IFC path. With
3066        // anonymous (DOM-less) cells there is nothing to measure → all zeros.
3067        let tree = table_tree();
3068        let mut env = Env::new(styled(Dom::create_body(), ""));
3069        let mut ctx = env.ctx();
3070        let mut text_cache = LayoutCache::new();
3071        let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
3072
3073        let rows = [(1usize, isz(1.0, 2.0, 3.0, 4.0))];
3074        let r = calc.calculate_table_intrinsic_sizes(&tree, 0, &rows);
3075        assert_eq!(r.min_content_width, 0.0);
3076        assert_eq!(r.max_content_width, 0.0);
3077        assert_eq!(r.max_content_height, 0.0);
3078    }
3079
3080    #[test]
3081    fn table_intrinsic_sizes_of_a_table_without_rows_are_zero() {
3082        // A `FormattingContext::Table` whose children are neither rows nor row
3083        // groups must not panic — it simply aggregates nothing.
3084        let bp = zero_props();
3085        let tree = tree_of(
3086            vec![
3087                hot(None, FormattingContext::Table, &bp),
3088                hot(Some(0), BLOCK, &bp),
3089            ],
3090            &[vec![1], vec![]],
3091        );
3092        let mut env = Env::new(styled(Dom::create_body(), ""));
3093        let mut ctx = env.ctx();
3094        let mut text_cache = LayoutCache::new();
3095        let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
3096
3097        let r = calc.calculate_table_intrinsic_sizes(&tree, 0, &[(1usize, isz(9.0, 9.0, 9.0, 9.0))]);
3098        assert_eq!(r.min_content_width, 0.0);
3099        assert_eq!(r.max_content_width, 0.0);
3100        assert_eq!(r.min_content_height, 0.0);
3101    }
3102
3103    #[test]
3104    fn table_intrinsic_sizes_saturate_on_extreme_cell_widths() {
3105        let tree = table_tree();
3106        let mut env = Env::new(styled(Dom::create_body(), ""));
3107        let mut ctx = env.ctx();
3108        let mut text_cache = LayoutCache::new();
3109        let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
3110
3111        let huge = isz(f32::MAX, f32::MAX, 1.0, 1.0);
3112        let cells = [(2usize, huge), (3usize, huge)];
3113        let r = calc.calculate_table_intrinsic_sizes(&tree, 0, &cells);
3114        assert!(r.min_content_width.is_infinite() && r.min_content_width.is_sign_positive());
3115        assert!(r.max_content_width.is_infinite() && r.max_content_width.is_sign_positive());
3116        assert_eq!(r.max_content_height, 1.0);
3117    }
3118
3119    #[test]
3120    fn table_intrinsic_sizes_with_an_out_of_range_index_are_zero() {
3121        let tree = table_tree();
3122        let mut env = Env::new(styled(Dom::create_body(), ""));
3123        let mut ctx = env.ctx();
3124        let mut text_cache = LayoutCache::new();
3125        let mut calc = IntrinsicSizeCalculator::new(&mut ctx, &mut text_cache);
3126
3127        // `tree.children(usize::MAX)` must yield an empty slice, not panic.
3128        let r = calc.calculate_table_intrinsic_sizes(&tree, usize::MAX, &[]);
3129        assert_eq!(r.min_content_width, 0.0);
3130        assert_eq!(r.max_content_height, 0.0);
3131    }
3132
3133    // ==================================================================
3134    // calculate_intrinsic_sizes (phase 2a entry point)
3135    // ==================================================================
3136
3137    /// `body(0) > .flex(1) > .a(2)` — deliberately text-free, so the whole
3138    /// intrinsic pass runs without ever entering text shaping. `.flex` is a
3139    /// shrink-to-fit context, so Fix C does not short-circuit the subtree.
3140    fn flex_dom() -> StyledDom {
3141        styled(
3142            Dom::create_body().with_child(div_class("flex").with_child(div_class("a"))),
3143            ".flex { display: flex; } .a { display: block; min-width: 120px; min-height: 30px; }",
3144        )
3145    }
3146
3147    #[test]
3148    fn calculate_intrinsic_sizes_is_a_no_op_when_nothing_is_dirty() {
3149        let mut env = Env::new(flex_dom());
3150        let mut ctx = env.ctx();
3151        let mut tree = generate_layout_tree(&mut ctx).expect("layout tree");
3152        let mut text_cache = LayoutCache::new();
3153
3154        calculate_intrinsic_sizes(&mut ctx, &mut tree, &mut text_cache, &BTreeSet::new())
3155            .expect("empty dirty set returns early");
3156        assert!(
3157            tree.warm.iter().all(|w| w.intrinsic_sizes.is_none()),
3158            "an empty dirty set must not compute anything"
3159        );
3160    }
3161
3162    #[test]
3163    fn calculate_intrinsic_sizes_applies_the_min_width_floor_bottom_up() {
3164        let mut env = Env::new(flex_dom());
3165        let mut ctx = env.ctx();
3166        let mut tree = generate_layout_tree(&mut ctx).expect("layout tree");
3167        let mut text_cache = LayoutCache::new();
3168        let dirty: BTreeSet<usize> = (0..tree.nodes.len()).collect();
3169
3170        calculate_intrinsic_sizes(&mut ctx, &mut tree, &mut text_cache, &dirty).expect("sizing");
3171
3172        // `.a` is an empty block: its content intrinsic is 0, but `min-width`
3173        // / `min-height` are <length>s, so they floor both min- and
3174        // max-content (+spec:min-max-sizing:970fef).
3175        let a = layout_index(&tree, NodeId::new(2));
3176        let a_sizes = tree
3177            .warm(a)
3178            .and_then(|w| w.intrinsic_sizes)
3179            .expect("`.a` was measured");
3180        assert_eq!(a_sizes.min_content_width, 120.0);
3181        assert_eq!(a_sizes.max_content_width, 120.0);
3182        assert_eq!(a_sizes.min_content_height, 30.0);
3183        assert_eq!(a_sizes.max_content_height, 30.0);
3184
3185        // The flex container aggregates its single item on both axes.
3186        let f = layout_index(&tree, NodeId::new(1));
3187        let f_sizes = tree
3188            .warm(f)
3189            .and_then(|w| w.intrinsic_sizes)
3190            .expect("`.flex` was measured");
3191        assert_eq!(f_sizes.min_content_width, 120.0);
3192        assert_eq!(f_sizes.max_content_width, 120.0);
3193        assert_eq!(f_sizes.max_content_height, 30.0);
3194    }
3195
3196    #[test]
3197    fn calculate_intrinsic_sizes_tolerates_bogus_dirty_node_indices() {
3198        let mut env = Env::new(flex_dom());
3199        let mut ctx = env.ctx();
3200        let mut tree = generate_layout_tree(&mut ctx).expect("layout tree");
3201        let mut text_cache = LayoutCache::new();
3202        // Dirty ids that no longer exist (a stale dirty set after a DOM shrink)
3203        // must not index out of bounds nor abort the pass.
3204        let dirty: BTreeSet<usize> = [0, 999, usize::MAX].into_iter().collect();
3205
3206        calculate_intrinsic_sizes(&mut ctx, &mut tree, &mut text_cache, &dirty)
3207            .expect("stale dirty ids must be ignored, not fatal");
3208        let root = tree.warm(tree.root).and_then(|w| w.intrinsic_sizes);
3209        assert!(root.is_some(), "the root is still measured");
3210    }
3211
3212    #[test]
3213    fn calculate_intrinsic_sizes_is_idempotent_across_repeated_passes() {
3214        let mut env = Env::new(flex_dom());
3215        let mut ctx = env.ctx();
3216        let mut tree = generate_layout_tree(&mut ctx).expect("layout tree");
3217        let mut text_cache = LayoutCache::new();
3218        let dirty: BTreeSet<usize> = (0..tree.nodes.len()).collect();
3219
3220        calculate_intrinsic_sizes(&mut ctx, &mut tree, &mut text_cache, &dirty).expect("pass 1");
3221        let a = layout_index(&tree, NodeId::new(2));
3222        let first = tree.warm(a).and_then(|w| w.intrinsic_sizes).expect("measured");
3223
3224        calculate_intrinsic_sizes(&mut ctx, &mut tree, &mut text_cache, &dirty).expect("pass 2");
3225        let second = tree.warm(a).and_then(|w| w.intrinsic_sizes).expect("measured");
3226
3227        assert_eq!(first.min_content_width, second.min_content_width);
3228        assert_eq!(first.max_content_width, second.max_content_width);
3229        assert_eq!(first.min_content_height, second.min_content_height);
3230        assert_eq!(first.max_content_height, second.max_content_height);
3231    }
3232
3233    // ==================================================================
3234    // collect_inline_content / collect_inline_content_recursive
3235    // ==================================================================
3236
3237    fn text_dom(text: &str) -> StyledDom {
3238        styled(
3239            Dom::create_body().with_child(div_class("p").with_child(Dom::create_text(text))),
3240            ".p { display: block; }",
3241        )
3242    }
3243
3244    fn collected_text(items: &[InlineContent]) -> String {
3245        items
3246            .iter()
3247            .filter_map(|item| match item {
3248                InlineContent::Text(run) => Some(run.text.as_str().to_string()),
3249                _ => None,
3250            })
3251            .collect()
3252    }
3253
3254    /// The layout node the IFC sizer measures the text through: the text node's
3255    /// own layout node when reconcile produced one, otherwise the enclosing
3256    /// block (whose DOM-children scan then picks the text up). Both routes must
3257    /// surface the same characters — which is exactly the invariant under test.
3258    fn text_ifc_index(tree: &LayoutTree, text_dom: NodeId, block_dom: NodeId) -> usize {
3259        tree.dom_to_layout
3260            .get(&text_dom)
3261            .and_then(|v| v.first())
3262            .copied()
3263            .unwrap_or_else(|| layout_index(tree, block_dom))
3264    }
3265
3266    #[test]
3267    fn collect_inline_content_gathers_the_text_of_an_ifc_root() {
3268        let mut env = Env::new(text_dom("hello world"));
3269        let mut ctx = env.ctx();
3270        let tree = generate_layout_tree(&mut ctx).expect("layout tree");
3271        let idx = text_ifc_index(&tree, NodeId::new(2), NodeId::new(1));
3272
3273        let items = collect_inline_content(&mut ctx, &tree, idx).expect("collect");
3274        assert!(!items.is_empty(), "the IFC root must see its text");
3275        assert!(collected_text(&items).contains("hello"));
3276    }
3277
3278    #[test]
3279    fn collect_inline_content_preserves_unicode_verbatim() {
3280        // Combining marks, an RTL run, an emoji ZWJ sequence — none of this may
3281        // be truncated, re-encoded, or split mid-scalar.
3282        let needle = "e\u{301}llo مرحبا 👨\u{200d}👩\u{200d}👧";
3283        let mut env = Env::new(text_dom(needle));
3284        let mut ctx = env.ctx();
3285        let tree = generate_layout_tree(&mut ctx).expect("layout tree");
3286        let idx = text_ifc_index(&tree, NodeId::new(2), NodeId::new(1));
3287
3288        let items = collect_inline_content(&mut ctx, &tree, idx).expect("collect");
3289        let text = collected_text(&items);
3290        assert!(text.contains('\u{301}'), "combining acute survived");
3291        assert!(text.contains("مرحبا"), "RTL run survived");
3292        assert!(text.contains("👨\u{200d}👩\u{200d}👧"), "ZWJ sequence survived");
3293    }
3294
3295    #[test]
3296    fn collect_inline_content_handles_whitespace_only_and_very_long_text() {
3297        for text in [" \n\t".to_string(), "x".repeat(20_000)] {
3298            let mut env = Env::new(text_dom(&text));
3299            let mut ctx = env.ctx();
3300            let tree = generate_layout_tree(&mut ctx).expect("layout tree");
3301            let idx = text_ifc_index(&tree, NodeId::new(2), NodeId::new(1));
3302            let items = collect_inline_content(&mut ctx, &tree, idx)
3303                .expect("degenerate text must still collect");
3304            // The DOM-children scan and the layout-children walk must not BOTH
3305            // pick the run up (that double-count made inline-blocks 2× too wide).
3306            assert!(
3307                collected_text(&items).len() <= text.len(),
3308                "the same text run was collected more than once"
3309            );
3310        }
3311    }
3312
3313    #[test]
3314    fn collect_inline_content_rejects_an_out_of_range_root_index() {
3315        let mut env = Env::new(text_dom("hello"));
3316        let mut ctx = env.ctx();
3317        let tree = generate_layout_tree(&mut ctx).expect("layout tree");
3318
3319        for bogus in [tree.nodes.len(), 999, usize::MAX] {
3320            let r = collect_inline_content(&mut ctx, &tree, bogus);
3321            assert!(
3322                matches!(r, Err(LayoutError::InvalidTree)),
3323                "index {bogus} must be rejected, not panic"
3324            );
3325        }
3326    }
3327
3328    #[test]
3329    fn collect_inline_content_of_a_text_free_subtree_is_empty() {
3330        let mut env = Env::new(flex_dom());
3331        let mut ctx = env.ctx();
3332        let tree = generate_layout_tree(&mut ctx).expect("layout tree");
3333        let a = layout_index(&tree, NodeId::new(2));
3334
3335        let items = collect_inline_content(&mut ctx, &tree, a).expect("collect");
3336        assert!(
3337            collected_text(&items).is_empty(),
3338            "a childless block has no inline text"
3339        );
3340    }
3341
3342    // ==================================================================
3343    // subtree_contains_text  (other)
3344    // ==================================================================
3345
3346    #[test]
3347    fn subtree_contains_text_sees_the_node_itself_and_its_descendants() {
3348        let dom = text_dom("hi");
3349        // body(0) > .p(1) > text(2)
3350        assert!(subtree_contains_text(&dom, NodeId::new(2)), "the text node itself");
3351        assert!(subtree_contains_text(&dom, NodeId::new(1)), "its parent");
3352        assert!(subtree_contains_text(&dom, NodeId::new(0)), "the root");
3353    }
3354
3355    #[test]
3356    fn subtree_contains_text_is_false_for_a_text_free_subtree() {
3357        let dom = styled(
3358            Dom::create_body().with_child(div_class("a").with_child(div_class("b"))),
3359            "",
3360        );
3361        assert!(!subtree_contains_text(&dom, NodeId::new(0)));
3362        assert!(!subtree_contains_text(&dom, NodeId::new(1)));
3363        assert!(!subtree_contains_text(&dom, NodeId::new(2)));
3364    }
3365
3366    #[test]
3367    fn subtree_contains_text_walks_a_deeply_nested_subtree() {
3368        // The recursion is unbounded in depth — 200 levels must not blow up.
3369        const DEPTH: usize = 200;
3370        let mut inner = Dom::create_div().with_child(Dom::create_text("deep"));
3371        for _ in 0..DEPTH {
3372            inner = Dom::create_div().with_child(inner);
3373        }
3374        let dom = styled(Dom::create_body().with_child(inner), "");
3375        assert!(subtree_contains_text(&dom, NodeId::new(0)));
3376
3377        let mut empty = Dom::create_div();
3378        for _ in 0..DEPTH {
3379            empty = Dom::create_div().with_child(empty);
3380        }
3381        let dom = styled(Dom::create_body().with_child(empty), "");
3382        assert!(!subtree_contains_text(&dom, NodeId::new(0)));
3383    }
3384
3385    // ==================================================================
3386    // extract_text_from_node  (other)
3387    // ==================================================================
3388
3389    #[test]
3390    fn extract_text_from_node_round_trips_the_exact_string() {
3391        for needle in [
3392            "hello world",
3393            " \n\t",
3394            "e\u{301}llo مرحبا 👨\u{200d}👩\u{200d}👧",
3395            "line1\nline2\r\n\u{0}nul",
3396        ] {
3397            let dom = text_dom(needle);
3398            assert_eq!(
3399                extract_text_from_node(&dom, NodeId::new(2)).as_deref(),
3400                Some(needle),
3401                "text must survive the DOM round-trip byte for byte"
3402            );
3403        }
3404    }
3405
3406    #[test]
3407    fn extract_text_from_node_is_none_for_non_text_nodes() {
3408        let dom = text_dom("hello");
3409        assert_eq!(extract_text_from_node(&dom, NodeId::new(0)), None, "body");
3410        assert_eq!(extract_text_from_node(&dom, NodeId::new(1)), None, "div");
3411    }
3412
3413    #[test]
3414    fn extract_text_from_node_handles_a_very_long_string() {
3415        let long = "ü".repeat(50_000);
3416        let dom = text_dom(&long);
3417        let got = extract_text_from_node(&dom, NodeId::new(2)).expect("text node");
3418        assert_eq!(got.chars().count(), 50_000);
3419        assert_eq!(got, long);
3420    }
3421
3422    // ==================================================================
3423    // calculate_used_size_for_node  (numeric)
3424    // ==================================================================
3425
3426    /// One classed div per constraint case; DOM ids follow pre-order.
3427    ///  body(0), .plain(1), .pct(2), .clamped(3), .maxed(4), .pctmin(5),
3428    ///  .bbox(6), .autoblock(7), .vwmin(8), .em(9), .hclamped(10), .row10(11)
3429    fn constraints_dom() -> StyledDom {
3430        styled(
3431            Dom::create_body()
3432                .with_child(div_class("plain"))
3433                .with_child(div_class("pct"))
3434                .with_child(div_class("clamped"))
3435                .with_child(div_class("maxed"))
3436                .with_child(div_class("pctmin"))
3437                .with_child(div_class("bbox"))
3438                .with_child(div_class("autoblock"))
3439                .with_child(div_class("vwmin"))
3440                .with_child(div_class("em"))
3441                .with_child(div_class("hclamped"))
3442                .with_child(div_class("row10")),
3443            "
3444            .plain     { display: block; width: 50px; height: 20px; }
3445            .pct       { display: block; width: 50%; height: 25%; }
3446            .clamped   { display: block; width: 300px; min-width: 200px; max-width: 100px; }
3447            .maxed     { display: block; width: 300px; max-width: 100px; }
3448            .pctmin    { display: block; min-width: 50%; }
3449            .bbox      { display: block; width: 5px; height: 5px; box-sizing: border-box; }
3450            .autoblock { display: block; }
3451            .vwmin     { display: block; width: 300px; min-width: 10vw; }
3452            .em        { display: block; font-size: 20px; min-width: 3em; }
3453            .hclamped  { display: block; height: 300px; min-height: 200px; max-height: 100px; }
3454            .row10     { display: block; min-width: 200px; max-height: 50px; }
3455            ",
3456        )
3457    }
3458
3459    const PLAIN: NodeId = NodeId::new(1);
3460    const PCT: NodeId = NodeId::new(2);
3461    const CLAMPED: NodeId = NodeId::new(3);
3462    const MAXED: NodeId = NodeId::new(4);
3463    const PCTMIN: NodeId = NodeId::new(5);
3464    const BBOX: NodeId = NodeId::new(6);
3465    const AUTOBLOCK: NodeId = NodeId::new(7);
3466    const VWMIN: NodeId = NodeId::new(8);
3467    const EM: NodeId = NodeId::new(9);
3468    const HCLAMPED: NodeId = NodeId::new(10);
3469    const ROW10: NodeId = NodeId::new(11);
3470
3471    fn node_state(dom: &StyledDom, id: NodeId) -> StyledNodeState {
3472        dom.styled_nodes.as_container()[id]
3473            .styled_node_state
3474    }
3475
3476    fn used_size(
3477        dom: &StyledDom,
3478        id: NodeId,
3479        cb: LogicalSize,
3480        bp: &BoxProps,
3481    ) -> LogicalSize {
3482        calculate_used_size_for_node(
3483            dom,
3484            Some(id),
3485            &cb,
3486            IntrinsicSizes::default(),
3487            bp,
3488            &VIEWPORT,
3489        )
3490        .expect("used size")
3491    }
3492
3493    #[test]
3494    fn used_size_of_an_anonymous_box_fills_the_cb_inline_and_uses_content_height() {
3495        let dom = constraints_dom();
3496        let cb = size(800.0, 600.0);
3497        let bp = zero_props();
3498
3499        let r = calculate_used_size_for_node(&dom, None, &cb, isz(0.0, 0.0, 0.0, 42.0), &bp, &VIEWPORT)
3500            .expect("anonymous box");
3501        assert_eq!(r.width, 800.0);
3502        assert_eq!(r.height, 42.0);
3503
3504        // A non-positive content height means "auto" — resolved later from the
3505        // laid-out children, so 0.0 (not the negative value) is stored now.
3506        let r = calculate_used_size_for_node(&dom, None, &cb, isz(0.0, 0.0, 0.0, -5.0), &bp, &VIEWPORT)
3507            .expect("anonymous box");
3508        assert_eq!(r.height, 0.0);
3509    }
3510
3511    #[test]
3512    fn used_size_resolves_absolute_lengths_and_adds_the_content_box_extras() {
3513        let dom = constraints_dom();
3514        let cb = size(800.0, 600.0);
3515
3516        let r = used_size(&dom, PLAIN, cb, &zero_props());
3517        assert_eq!(r.width, 50.0);
3518        assert_eq!(r.height, 20.0);
3519
3520        // content-box (default): padding + border grow the border box.
3521        let r = used_size(&dom, PLAIN, cb, &props(0.0, 2.0, 10.0));
3522        assert_eq!(r.width, 50.0 + 2.0 * (2.0 + 10.0));
3523        assert_eq!(r.height, 20.0 + 2.0 * (2.0 + 10.0));
3524    }
3525
3526    #[test]
3527    fn used_size_resolves_percentages_against_the_physical_containing_block() {
3528        let dom = constraints_dom();
3529        let r = used_size(&dom, PCT, size(800.0, 600.0), &zero_props());
3530        assert_eq!(r.width, 400.0, "50% of the CB width");
3531        assert_eq!(r.height, 150.0, "25% of the CB height");
3532    }
3533
3534    #[test]
3535    fn used_size_percentages_against_degenerate_containing_blocks_never_produce_nan() {
3536        let dom = constraints_dom();
3537        let bp = zero_props();
3538        for cb in [
3539            size(f32::NAN, f32::NAN),
3540            size(-800.0, -600.0),
3541            size(0.0, 0.0),
3542            size(f32::NEG_INFINITY, f32::NEG_INFINITY),
3543        ] {
3544            let r = used_size(&dom, PCT, cb, &bp);
3545            assert!(!r.width.is_nan() && !r.height.is_nan(), "NaN for cb={cb:?}");
3546            assert!(r.width >= 0.0 && r.height >= 0.0, "negative size for cb={cb:?}");
3547        }
3548        // An infinite CB stays infinite (saturation), never NaN.
3549        let r = used_size(&dom, PCT, size(f32::INFINITY, f32::INFINITY), &bp);
3550        assert!(r.width.is_infinite() && r.width.is_sign_positive());
3551    }
3552
3553    #[test]
3554    fn used_size_min_width_overrides_max_width_when_they_conflict() {
3555        // CSS 2.2 §10.4: if min-width > max-width, min-width wins.
3556        let dom = constraints_dom();
3557        let cb = size(800.0, 600.0);
3558        assert_eq!(used_size(&dom, CLAMPED, cb, &zero_props()).width, 200.0);
3559        // Without the conflicting min, max-width clamps normally.
3560        assert_eq!(used_size(&dom, MAXED, cb, &zero_props()).width, 100.0);
3561    }
3562
3563    #[test]
3564    fn used_size_min_height_overrides_max_height_when_they_conflict() {
3565        let dom = constraints_dom();
3566        let cb = size(800.0, 600.0);
3567        assert_eq!(used_size(&dom, HCLAMPED, cb, &zero_props()).height, 200.0);
3568    }
3569
3570    #[test]
3571    fn used_size_border_box_floors_at_the_padding_plus_border_sum() {
3572        // box-sizing: border-box with width:5px and 10px padding per side:
3573        // the content box cannot go negative, so the border box floors at 20.
3574        let dom = constraints_dom();
3575        let r = used_size(&dom, BBOX, size(800.0, 600.0), &props(0.0, 0.0, 10.0));
3576        assert_eq!(r.width, 20.0);
3577        assert_eq!(r.height, 20.0);
3578
3579        // With no padding/border the specified size IS the border box.
3580        let r = used_size(&dom, BBOX, size(800.0, 600.0), &zero_props());
3581        assert_eq!(r.width, 5.0);
3582        assert_eq!(r.height, 5.0);
3583    }
3584
3585    #[test]
3586    fn used_size_auto_width_block_fills_the_cb_minus_its_box_model() {
3587        let dom = constraints_dom();
3588        let cb = size(800.0, 600.0);
3589
3590        let r = used_size(&dom, AUTOBLOCK, cb, &props(100.0, 0.0, 0.0));
3591        assert_eq!(r.width, 600.0, "800 - 2*100 margin");
3592        assert_eq!(r.height, 0.0, "auto block height is filled in after layout");
3593
3594        // Box model wider than the CB → floored at 0, never negative.
3595        let r = used_size(&dom, AUTOBLOCK, size(10.0, 600.0), &props(100.0, 0.0, 0.0));
3596        assert_eq!(r.width, 0.0);
3597    }
3598
3599    // ==================================================================
3600    // apply_width_constraints / apply_height_constraints  (numeric)
3601    // ==================================================================
3602
3603    fn width_constrained(dom: &StyledDom, id: NodeId, tentative: f32, cb_width: f32) -> f32 {
3604        let state = node_state(dom, id);
3605        apply_width_constraints(dom, id, &state, tentative, cb_width, &zero_props())
3606    }
3607
3608    fn height_constrained(dom: &StyledDom, id: NodeId, tentative: f32, cb_height: f32) -> f32 {
3609        let state = node_state(dom, id);
3610        apply_height_constraints(dom, id, &state, tentative, cb_height, &zero_props())
3611    }
3612
3613    #[test]
3614    fn width_constraints_are_the_identity_without_min_or_max() {
3615        let dom = constraints_dom();
3616        for tentative in [0.0, 42.0, f32::MAX] {
3617            assert_eq!(width_constrained(&dom, PLAIN, tentative, 800.0), tentative);
3618        }
3619    }
3620
3621    #[test]
3622    fn width_constraints_clamp_then_let_min_win_over_max() {
3623        let dom = constraints_dom();
3624        assert_eq!(width_constrained(&dom, MAXED, 300.0, 800.0), 100.0, "max clamps");
3625        assert_eq!(width_constrained(&dom, MAXED, 50.0, 800.0), 50.0, "below max: untouched");
3626        assert_eq!(
3627            width_constrained(&dom, CLAMPED, 300.0, 800.0),
3628            200.0,
3629            "min-width overrides max-width per §10.4"
3630        );
3631    }
3632
3633    #[test]
3634    fn width_constraints_resolve_percentage_minimums_against_the_containing_block() {
3635        let dom = constraints_dom();
3636        assert_eq!(width_constrained(&dom, PCTMIN, 10.0, 800.0), 400.0);
3637        // A negative CB floors the percentage at 0, so the min is inert.
3638        assert_eq!(width_constrained(&dom, PCTMIN, 10.0, -800.0), 10.0);
3639        // A NaN CB must not poison the result.
3640        let r = width_constrained(&dom, PCTMIN, 10.0, f32::NAN);
3641        assert!(!r.is_nan() && r == 10.0);
3642    }
3643
3644    #[test]
3645    fn width_constraints_resolve_em_minimums_against_the_elements_own_font_size() {
3646        // .em has font-size: 20px and min-width: 3em → 60px, NOT 3 × 16px.
3647        let dom = constraints_dom();
3648        assert_eq!(width_constrained(&dom, EM, 10.0, 800.0), 60.0);
3649    }
3650
3651    #[test]
3652    fn width_constraints_never_return_nan() {
3653        // A NaN tentative width is sanitized by the final `.max(min_width)`
3654        // (f32::max drops NaN), so nothing downstream can see a NaN size.
3655        let dom = constraints_dom();
3656        assert_eq!(width_constrained(&dom, PLAIN, f32::NAN, 800.0), 0.0);
3657        // MAXED has a max-width, so the FIRST `.min(max_width)` absorbs the NaN (IEEE
3658        // 754: f32::min drops NaN) and lands on the max, before `.max(min_width)` ever
3659        // sees it. NaN doesn't always collapse to min_width -- it collapses to
3660        // whichever clamp touches it first.
3661        assert_eq!(width_constrained(&dom, MAXED, f32::NAN, 800.0), 100.0);
3662        assert_eq!(width_constrained(&dom, CLAMPED, f32::NAN, 800.0), 200.0);
3663    }
3664
3665    #[test]
3666    fn width_constraints_handle_infinite_tentative_widths() {
3667        let dom = constraints_dom();
3668        assert_eq!(width_constrained(&dom, MAXED, f32::INFINITY, 800.0), 100.0);
3669        // No max-width → +inf survives (a definite size is never produced from
3670        // an infinite one, but the function must not panic or wrap).
3671        assert!(width_constrained(&dom, PLAIN, f32::INFINITY, 800.0).is_infinite());
3672        assert_eq!(width_constrained(&dom, CLAMPED, f32::NEG_INFINITY, 800.0), 200.0);
3673    }
3674
3675    #[test]
3676    fn width_constraints_ignore_viewport_unit_minimums() {
3677        // KNOWN GAP: `resolve_px_with_box_model` cannot resolve vw/vh/vmin/vmax
3678        // (no viewport is threaded into it), so `min-width: 10vw` silently
3679        // defaults to 0 and never floors the width — even though `width: 10vw`
3680        // on the very same element DOES resolve (via
3681        // `resolve_pixel_value_no_percent_with_viewport`).
3682        let dom = constraints_dom();
3683        assert_eq!(width_constrained(&dom, VWMIN, 300.0, 800.0), 300.0);
3684        assert_eq!(
3685            width_constrained(&dom, VWMIN, 10.0, 800.0),
3686            10.0,
3687            "10vw (= 80px on an 800px viewport) does not floor the width"
3688        );
3689    }
3690
3691    #[test]
3692    fn height_constraints_clamp_then_let_min_win_over_max() {
3693        let dom = constraints_dom();
3694        assert_eq!(height_constrained(&dom, HCLAMPED, 300.0, 600.0), 200.0);
3695        assert_eq!(height_constrained(&dom, PLAIN, 42.0, 600.0), 42.0);
3696    }
3697
3698    #[test]
3699    fn height_constraints_never_return_nan() {
3700        let dom = constraints_dom();
3701        assert_eq!(height_constrained(&dom, PLAIN, f32::NAN, 600.0), 0.0);
3702        assert_eq!(height_constrained(&dom, HCLAMPED, f32::NAN, 600.0), 200.0);
3703    }
3704
3705    #[test]
3706    fn height_constraints_are_stable_at_the_f32_boundaries() {
3707        let dom = constraints_dom();
3708        assert_eq!(height_constrained(&dom, HCLAMPED, f32::MAX, 600.0), 200.0);
3709        assert_eq!(height_constrained(&dom, HCLAMPED, f32::MIN, 600.0), 200.0);
3710        // The implicit min-height of 0 floors any negative tentative height,
3711        // so a negative size can never escape into layout.
3712        assert_eq!(height_constrained(&dom, PLAIN, f32::MIN, 600.0), 0.0);
3713        assert_eq!(height_constrained(&dom, PLAIN, -1.0, 600.0), 0.0);
3714        assert!(height_constrained(&dom, PLAIN, f32::MAX, 600.0).is_finite());
3715    }
3716
3717    // ==================================================================
3718    // apply_constraint_violation_table  (numeric)
3719    // ==================================================================
3720
3721    fn cvt(dom: &StyledDom, id: NodeId, w: f32, h: f32) -> (f32, f32) {
3722        let state = node_state(dom, id);
3723        apply_constraint_violation_table(dom, id, &state, w, h, 800.0, 600.0, &zero_props())
3724    }
3725
3726    #[test]
3727    fn constraint_violation_table_row1_leaves_an_unviolated_box_alone() {
3728        let dom = constraints_dom();
3729        assert_eq!(cvt(&dom, PLAIN, 200.0, 100.0), (200.0, 100.0));
3730    }
3731
3732    #[test]
3733    fn constraint_violation_table_row2_preserves_the_aspect_ratio_under_max_width() {
3734        // w=200 > max-width=100 → w := 100, h scaled by the same factor.
3735        let dom = constraints_dom();
3736        assert_eq!(cvt(&dom, MAXED, 200.0, 100.0), (100.0, 50.0));
3737    }
3738
3739    #[test]
3740    fn constraint_violation_table_row10_pins_min_width_and_max_height_together() {
3741        // .row10: min-width 200, max-height 50. w=100 (< min) and h=100 (> max)
3742        // → the ratio cannot be preserved; the spec pins both constraints.
3743        let dom = constraints_dom();
3744        assert_eq!(cvt(&dom, ROW10, 100.0, 100.0), (200.0, 50.0));
3745    }
3746
3747    #[test]
3748    fn constraint_violation_table_guards_against_division_by_zero() {
3749        // The w<=0 / h<=0 guard must fire BEFORE any `w / h` division.
3750        let dom = constraints_dom();
3751        assert_eq!(cvt(&dom, MAXED, 0.0, 100.0), (0.0, 100.0));
3752        assert_eq!(cvt(&dom, MAXED, 200.0, 0.0), (100.0, 0.0));
3753        assert_eq!(cvt(&dom, MAXED, 0.0, 0.0), (0.0, 0.0));
3754        assert_eq!(cvt(&dom, MAXED, -50.0, -50.0), (0.0, 0.0), "negatives clamp up to 0");
3755    }
3756
3757    #[test]
3758    fn constraint_violation_table_survives_extreme_ratios() {
3759        // A near-degenerate ratio (MAX / MIN_POSITIVE) must not produce NaN or
3760        // panic — the scaled dimension underflows to 0 and is then floored.
3761        let dom = constraints_dom();
3762        let (w, h) = cvt(&dom, MAXED, f32::MAX, f32::MIN_POSITIVE);
3763        assert_eq!(w, 100.0, "max-width still clamps");
3764        assert!(h.is_finite() && h >= 0.0, "scaled height stays finite: {h}");
3765
3766        let (w, h) = cvt(&dom, MAXED, f32::MIN_POSITIVE, f32::MAX);
3767        assert!(w.is_finite() && w >= 0.0, "w={w}");
3768        assert!(h.is_finite() && h >= 0.0, "h={h}");
3769    }
3770
3771    #[test]
3772    fn constraint_violation_table_is_idempotent() {
3773        // Re-applying the table to its own output must be a fixed point —
3774        // otherwise a re-layout would keep shrinking the box.
3775        let dom = constraints_dom();
3776        for (id, w, h) in [
3777            (MAXED, 200.0_f32, 100.0_f32),
3778            (ROW10, 100.0, 100.0),
3779            (PLAIN, 200.0, 100.0),
3780        ] {
3781            let first = cvt(&dom, id, w, h);
3782            let second = cvt(&dom, id, first.0, first.1);
3783            assert_eq!(first, second, "not a fixed point for {id:?}");
3784        }
3785    }
3786}