1
//! Intrinsic and used size calculations for layout nodes
2

            
3
use std::{
4
    collections::BTreeSet,
5
    sync::Arc,
6
};
7

            
8
use azul_core::{
9
    dom::{FormattingContext, NodeId, NodeType},
10
    geom::LogicalSize,
11
    resources::RendererResources,
12
    styled_dom::{StyledDom, StyledNodeState},
13
};
14
use azul_css::{
15
    css::CssPropertyValue,
16
    props::{
17
        basic::PixelValue,
18
        layout::{LayoutDisplay, LayoutFlexDirection, LayoutFlexWrap, LayoutFloat, LayoutHeight, LayoutPosition, LayoutWidth, LayoutWritingMode},
19
        property::{CssProperty, CssPropertyType},
20
    },
21
    LayoutDebugMessage,
22
};
23
use rust_fontconfig::FcFontCache;
24

            
25
#[cfg(feature = "text_layout")]
26
use crate::text3;
27
use crate::{
28
    font::parsed::ParsedFont,
29
    font_traits::{
30
        AvailableSpace, FontLoaderTrait, FontManager, ImageSource, InlineContent, InlineImage,
31
        InlineShape, LayoutCache, LayoutFragment, ObjectFit, ParsedFontTrait, ShapeDefinition,
32
        StyleProperties, UnifiedConstraints,
33
    },
34
    solver3::{
35
        fc::split_text_for_whitespace,
36
        geometry::{BoxProps, BoxSizing, IntrinsicSizes, WritingModeContext},
37
        getters::{
38
            get_css_box_sizing, get_css_height, get_css_width, get_display_property,
39
            get_direction_property, get_element_font_size, get_flex_direction, get_float,
40
            get_style_properties, get_text_orientation_property, get_writing_mode, MultiValue,
41
        },
42
        layout_tree::{LayoutNodeHot, LayoutTree, get_display_type},
43
        positioning::get_position_type,
44
        LayoutContext, LayoutError, Result,
45
    },
46
};
47

            
48
/// Resolves a percentage value against the containing block dimension.
49
///
50
/// Per CSS 2.1 Section 10.2, percentages resolve directly against the containing
51
/// block's width or height. The margin/border/padding parameters are accepted for
52
/// call-site convenience but are intentionally unused — percentage resolution does
53
/// not subtract box-model extras in content-box sizing.
54
///
55
/// Returns `(containing_block_dimension * percentage).max(0.0)`.
56
// +spec:containing-block:43c719 - percentages resolved against containing block width/height
57
// +spec:containing-block:723eee - Percentages specify sizing with respect to the containing block
58
// +spec:containing-block:8ad6f4 - Percentage resolution against containing block (editorial note: transferred percentages)
59
// +spec:containing-block:257f3b - Block-axis percentages resolve against containing block size
60
// +spec:containing-block:f1344e - percentage min/max-width resolved against containing block width; negative CB width yields zero
61
2156
pub fn resolve_percentage_with_box_model(
62
2156
    containing_block_dimension: f32,
63
2156
    percentage: f32,
64
2156
    _margins: (f32, f32),
65
2156
    _borders: (f32, f32),
66
2156
    _paddings: (f32, f32),
67
2156
) -> f32 {
68
    // +spec:containing-block:b3388b - percentage resolved against containing block size without re-resolution (css-sizing-3 §5.2.1)
69
    // CSS 2.1 Section 10.2: percentages resolve against containing block,
70
    // not available space after margins/borders/padding
71
2156
    (containing_block_dimension * percentage).max(0.0)
72
2156
}
73

            
74
/// Returns true if the DOM subtree rooted at `dom_id` contains any `NodeType::Text`.
75
///
76
/// Used when deciding whether a `FormattingContext::Inline` node should measure
77
/// its inline content (it acts as an IFC root when nested inlines eventually
78
/// hold text) versus returning zero (pure inline wrapper with no text reaches).
79
27500
fn subtree_contains_text(styled_dom: &StyledDom, dom_id: NodeId) -> bool {
80
27500
    let node_hierarchy = styled_dom.node_hierarchy.as_container();
81
27500
    let node_data = styled_dom.node_data.as_container();
82
27500
    if matches!(node_data[dom_id].get_node_type(), NodeType::Text(_)) {
83
16984
        return true;
84
10516
    }
85
10516
    dom_id
86
10516
        .az_children(&node_hierarchy)
87
10516
        .any(|child| subtree_contains_text(styled_dom, child))
88
27500
}
89

            
90
/// Phase 2a: Calculate intrinsic sizes (bottom-up pass)
91
/// // +spec:display-contents:f12d4e - intrinsic sizing: size determined by contents, not context
92
// [g71 TEST] #[inline(never)] — RELIABLE bisection (g70, markers in free band) showed new_tree drops
93
// 2→0 RIGHT BEFORE this call. It's currently INLINED into layout_document (absent from the lift log),
94
// so its frame/entry isn't a separate SP-wrapped @sub_ call. Forcing it OUT makes the call a wrapped
95
// @sub_ → enforce_sp_preservation save/restores SP around it. If new_tree survives (sizingEntry=2),
96
// the inlined entry/frame-setup was mis-lifting SP. (g60's inline(always) was a no-op — already inlined.)
97
#[inline(never)]
98
7260
pub fn calculate_intrinsic_sizes<T: ParsedFontTrait>(
99
7260
    ctx: &mut LayoutContext<'_, T>,
100
7260
    tree: &mut LayoutTree,
101
7260
    text_cache: &mut LayoutCache,
102
7260
    dirty_nodes: &BTreeSet<usize>,
103
7260
) -> Result<()> {
104
    // [az-diag g59 REVERT] RELIABLE field-access bracket (pointer CASTS mis-lift to 0 — g58 proved
105
    // it; use tree.nodes.len() which is reliable). 0x407B0 = nodes.len at ENTRY. If 2 here but
106
    // 0x40734 (line ~142, after compute_dirty_ancestor_closure + calculator) reads 0, the corruption
107
    // is in 121-142. compute_dirty_ancestor_closure RETURNS a HashSet by sret — prime suspect:
108
    // sret-slot overlapping new_tree, or the hashbrown empty-map bug. 0x407B4 (post-compute_dirty)
109
    // isolates compute_dirty vs calculator-creation.
110
7260
    unsafe { crate::az_mark((0x607B0) as u32, (tree.nodes.len() as u32) as u32); }
111
7260
    if dirty_nodes.is_empty() {
112
        return Ok(());
113
7260
    }
114

            
115
7260
    ctx.debug_log("Starting intrinsic size calculation");
116
    // Pre-compute the "ancestor closure" of dirty_nodes: every dirty
117
    // node AND each of its ancestors up to root. A node not in this
118
    // set (and whose `intrinsic_sizes` is already populated) can
119
    // reuse its cached intrinsic — we skip its entire subtree walk.
120
    // Before this, `calculate_intrinsic_recursive` walked the full
121
    // tree from root regardless, costing ~2 ms per warm render on
122
    // excel.html even when only 3 nodes were actually dirty.
123
7260
    let dirty_closure = compute_dirty_ancestor_closure(tree, dirty_nodes);
124
    // [az-diag g59 REVERT] 0x407B4 = nodes.len AFTER compute_dirty_ancestor_closure (its HashSet sret).
125
7260
    unsafe { crate::az_mark((0x607B4) as u32, (tree.nodes.len() as u32) as u32); }
126

            
127
7260
    let mut calculator = IntrinsicSizeCalculator::new(ctx, text_cache);
128
7260
    calculator.dirty_closure = Some(dirty_closure);
129
    // Fix C (re-enabled §58 Win #3): skip intrinsic computation for subtrees
130
    // whose values will never be consumed. `tree.subtree_needs_intrinsic` is a
131
    // static-DOM bitmap precomputed at tree-build time — true if this node or
132
    // any descendant establishes a shrink-to-fit context. When both the
133
    // caller and the subtree are non-STF, no one reads the intrinsic, so the
134
    // whole descent is pure waste.
135
    //
136
    // The previous attempt (7667d13e, reverted in bd9ad36d) wrote default
137
    // (zero) intrinsics and broke auto-height rendering because
138
    // calculate_used_size_for_node read intrinsic.max_content_height as the
139
    // height:auto fallback. 97c3d3db refactored that dependency away: for
140
    // block-level auto-height, used_size.height is 0 pre-layout and
141
    // apply_content_based_height fills it from the laid-out content size.
142
    // With that gone, skipping intrinsic is safe.
143
    // [az-diag g53 REVERT] DECISIVE: is the lifted LayoutTree itself empty/broken? If
144
    // tree.get(root)=None (0x40738=0) or nodes.len()=0 (0x40734), the InvalidTree@229 is
145
    // because RECONCILE produced a broken tree — root cause is reconcile, not sizing.
146
7260
    unsafe {
147
7260
        crate::az_mark((0x60730) as u32, (tree.root as u32) as u32);
148
7260
        crate::az_mark((0x60734) as u32, (tree.nodes.len() as u32) as u32);
149
7260
        crate::az_mark((0x60738) as u32, (tree.get(tree.root).is_some() as u32) as u32);
150
7260
        // [az-diag g55] 0x4075C = the `tree` ptr the CALLEE sees. Compare with 0x40748
151
7260
        // (caller's &new_tree). Same → nodes-field-offset mis-lift; differ → &mut arg mis-passed.
152
7260
        crate::az_mark((0x6075C) as u32, ((tree as *const LayoutTree as usize) as u32) as u32);
153
7260
    }
154
7260
    calculator.calculate_intrinsic_recursive(tree, tree.root, false)?;
155
7260
    ctx.debug_log("Finished intrinsic size calculation");
156
7260
    Ok(())
157
7260
}
158

            
159
7260
fn compute_dirty_ancestor_closure(
160
7260
    tree: &LayoutTree,
161
7260
    dirty_nodes: &BTreeSet<usize>,
162
7260
) -> std::collections::HashSet<usize> {
163
7260
    let mut closure: std::collections::HashSet<usize> = std::collections::HashSet::new();
164
62964
    for &dirty in dirty_nodes {
165
55704
        let mut cur = Some(dirty);
166
111496
        while let Some(idx) = cur {
167
104236
            if !closure.insert(idx) {
168
48444
                break;
169
55792
            }
170
55792
            cur = tree.get(idx).and_then(|n| n.parent);
171
        }
172
    }
173
7260
    closure
174
7260
}
175

            
176
struct IntrinsicSizeCalculator<'a, 'b, 'c, T: ParsedFontTrait> {
177
    ctx: &'a mut LayoutContext<'b, T>,
178
    /// Shared text shaping cache, threaded through from the caller so
179
    /// stages 1–3 of the inline layout pipeline (logical / BiDi / shaping)
180
    /// are cache-hits across the sizing pass's min/max-content measurements
181
    /// AND the subsequent real layout pass. Previously each pass held its
182
    /// own `LayoutCache`, so identical text was shaped three times per
183
    /// root_layout_pass — once per min-content measurement, once per
184
    /// max-content measurement, once at final layout.
185
    text_cache: &'c mut LayoutCache,
186
    /// If `Some`, only nodes in this set (the ancestor-closure of
187
    /// dirty nodes) need recomputation. A clean node whose
188
    /// `warm.intrinsic_sizes` is already populated reuses the
189
    /// cached value and skips its entire subtree descent.
190
    dirty_closure: Option<std::collections::HashSet<usize>>,
191
}
192

            
193
impl<'a, 'b, 'c, T: ParsedFontTrait> IntrinsicSizeCalculator<'a, 'b, 'c, T> {
194
7260
    fn new(ctx: &'a mut LayoutContext<'b, T>, text_cache: &'c mut LayoutCache) -> Self {
195
7260
        Self {
196
7260
            ctx,
197
7260
            text_cache,
198
7260
            dirty_closure: None,
199
7260
        }
200
7260
    }
201

            
202
37664
    fn calculate_intrinsic_recursive(
203
37664
        &mut self,
204
37664
        tree: &mut LayoutTree,
205
37664
        node_index: usize,
206
37664
        ancestor_is_stf: bool,
207
37664
    ) -> Result<IntrinsicSizes> {
208
        // [az-diag g52 REVERT] 0x40720 = node_index entering calculate_intrinsic_recursive
209
        // (last value after the run = the node that InvalidTree'd or the stray child).
210
37664
        unsafe { crate::az_mark((0x60720) as u32, (node_index as u32) as u32); }
211
        // Fast path: if this subtree has no dirty nodes AND we
212
        // already have a cached intrinsic, return the cached value
213
        // and skip the whole descent. Caller is the ancestor-closure
214
        // computation in `calculate_intrinsic_sizes` — anything not
215
        // in that set is guaranteed clean through every descendant.
216
37664
        if let Some(closure) = self.dirty_closure.as_ref() {
217
37664
            if !closure.contains(&node_index) {
218
176
                if let Some(cached) = tree
219
176
                    .warm(node_index)
220
176
                    .and_then(|w| w.intrinsic_sizes.clone())
221
                {
222
176
                    return Ok(cached);
223
                }
224
37488
            }
225
        }
226

            
227
        // Fix C static-DOM short-circuit: if no ancestor needs this intrinsic
228
        // (none are STF) AND no descendant in this subtree is STF, nobody
229
        // will ever read the value. Write a default and skip the recursion.
230
        // `subtree_needs_intrinsic` is precomputed at tree-build time from
231
        // the DOM's display/position/float properties, so this is a constant
232
        // lookup with no per-pass work.
233
37488
        if !ancestor_is_stf
234
15972
            && tree
235
15972
                .subtree_needs_intrinsic
236
15972
                .get(node_index)
237
15972
                .copied()
238
15972
                .map(|v| !v)
239
15972
                .unwrap_or(false)
240
        {
241
2068
            let default = IntrinsicSizes::default();
242
2068
            if let Some(n) = tree.warm_mut(node_index) {
243
2068
                n.intrinsic_sizes = Some(default);
244
2068
            }
245
2068
            return Ok(default);
246
35420
        }
247

            
248
        // Previously cloned the full LayoutNode to sidestep borrow conflicts
249
        // with the `&mut tree` recursive calls below, but we only need the
250
        // DOM id here — a `Copy` scalar. The clone was allocating a
251
        // Vec<usize> for children and a TaffyCache on every recursion
252
        // (~300x on excel.html).
253
35420
        let dom_node_id = tree
254
35420
            .get(node_index)
255
35420
            .ok_or(LayoutError::InvalidTree)?
256
            .dom_node_id;
257

            
258
        // Out-of-flow elements do not contribute to their parent's intrinsic size.
259
35420
        let position = get_position_type(self.ctx.styled_dom, dom_node_id);
260
35420
        if position == LayoutPosition::Absolute || position == LayoutPosition::Fixed {
261
528
            if let Some(n) = tree.warm_mut(node_index) {
262
528
                n.intrinsic_sizes = Some(IntrinsicSizes::default());
263
528
            }
264
528
            return Ok(IntrinsicSizes::default());
265
34892
        }
266

            
267
        // Copy child indices before recursive calls (which need &mut tree).
268
        // Stack buffer for the common case (≤32 children); heap only for huge nodes.
269
34892
        let children_slice = tree.children(node_index);
270
34892
        let n = children_slice.len();
271
34892
        let mut stack_buf = [0usize; 32];
272
        let heap_buf: Vec<usize>;
273
34892
        let children: &[usize] = if n <= 32 {
274
34892
            stack_buf[..n].copy_from_slice(children_slice);
275
34892
            &stack_buf[..n]
276
        } else {
277
            heap_buf = children_slice.to_vec();
278
            &heap_buf
279
        };
280
        // Propagate STF flag: children inherit `ancestor_is_stf=true` if any
281
        // ancestor up to and including self is STF.
282
34892
        let self_is_stf = tree
283
34892
            .get(node_index)
284
34892
            .map(|n| {
285
34892
                crate::solver3::layout_tree::is_shrink_to_fit_context(
286
34892
                    self.ctx.styled_dom,
287
34892
                    n.dom_node_id,
288
34892
                    &n.formatting_context,
289
                )
290
34892
            })
291
34892
            .unwrap_or(false);
292
34892
        let child_ancestor_is_stf = ancestor_is_stf || self_is_stf;
293

            
294
34892
        let mut child_intrinsics = Vec::with_capacity(n);
295
65296
        for &child_index in children {
296
            // [az-diag g52 REVERT] 0x40728 = child_index about to recurse (last = the stray).
297
30404
            unsafe { crate::az_mark((0x60728) as u32, (child_index as u32) as u32); }
298
            // [g52 FIX] Defensive: reconcile can mis-list a stray/out-of-range child_index
299
            // (a Text node mis-listed as a layout child, or a lift artifact in the children
300
            // array). The unguarded recursion would hit `tree.get(child_index).ok_or(InvalidTree)`
301
            // at line ~226 and abort the WHOLE intrinsic-sizing pass. Skip gracefully so
302
            // measurement continues — mirrors process_layout_children's guard (line ~1079).
303
            // REAL fix = reconcile not listing the stray child.
304
30404
            if tree.get(child_index).is_none() {
305
                continue;
306
30404
            }
307
30404
            let child_intrinsic =
308
30404
                self.calculate_intrinsic_recursive(tree, child_index, child_ancestor_is_stf)?;
309
30404
            child_intrinsics.push((child_index, child_intrinsic));
310
        }
311

            
312
        // Then calculate this node's intrinsic size based on its children
313
34892
        let mut intrinsic = self.calculate_node_intrinsic_sizes(tree, node_index, &child_intrinsics)?;
314

            
315
        // +spec:min-max-sizing:970fef - if min-width/min-height is a <length>, use as floor for intrinsic sizes
316
34892
        if let Some(dom_id) = tree.get(node_index).and_then(|n| n.dom_node_id) {
317
            use azul_css::props::basic::{pixel::{DEFAULT_FONT_SIZE, PT_TO_PX}, SizeMetric};
318
            use crate::solver3::getters::{get_css_min_width, get_css_min_height, MultiValue};
319

            
320
34804
            let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
321

            
322
34804
            if let MultiValue::Exact(mw) = get_css_min_width(self.ctx.styled_dom, dom_id, node_state) {
323
                let px = &mw.inner;
324
                let resolved = match px.metric {
325
                    SizeMetric::Px => Some(px.number.get()),
326
                    SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
327
                    SizeMetric::In => Some(px.number.get() * 96.0),
328
                    SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
329
                    SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
330
                    SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
331
                    _ => None, // percentages are not <length>
332
                };
333
                if let Some(min_w) = resolved {
334
                    intrinsic.min_content_width = intrinsic.min_content_width.max(min_w);
335
                    intrinsic.max_content_width = intrinsic.max_content_width.max(min_w);
336
                }
337
34804
            }
338

            
339
34804
            if let MultiValue::Exact(mh) = get_css_min_height(self.ctx.styled_dom, dom_id, node_state) {
340
352
                let px = &mh.inner;
341
352
                let resolved = match px.metric {
342
352
                    SizeMetric::Px => Some(px.number.get()),
343
                    SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
344
                    SizeMetric::In => Some(px.number.get() * 96.0),
345
                    SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
346
                    SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
347
                    SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
348
                    _ => None,
349
                };
350
352
                if let Some(min_h) = resolved {
351
352
                    intrinsic.min_content_height = intrinsic.min_content_height.max(min_h);
352
352
                    intrinsic.max_content_height = intrinsic.max_content_height.max(min_h);
353
352
                }
354
34452
            }
355
88
        }
356

            
357
34892
        if let Some(n) = tree.warm_mut(node_index) {
358
34892
            n.intrinsic_sizes = Some(intrinsic);
359
34892
        }
360

            
361
34892
        Ok(intrinsic)
362
37664
    }
363

            
364
34892
    fn calculate_node_intrinsic_sizes(
365
34892
        &mut self,
366
34892
        tree: &LayoutTree,
367
34892
        node_index: usize,
368
34892
        child_intrinsics: &[(usize, IntrinsicSizes)],
369
34892
    ) -> Result<IntrinsicSizes> {
370
34892
        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
371

            
372
        // +spec:block-formatting-context:30def2 - replaced elements use physical 300x150 default, not re-oriented by writing-mode
373
        // +spec:display-property:015c41 - replaced elements default to 300x150 intrinsic size per css-sizing-3 §5.1
374
        // +spec:display-property:2c6af3 - replaced elements with auto width/height use max-content size
375
        // +spec:replaced-elements:6d6030 - Intrinsic sizes for replaced elements (images, virtual views)
376
        // VirtualViews are replaced elements with a default intrinsic size of 300x150px
377
        // (same as virtualized view elements)
378
34892
        if let Some(dom_id) = node.dom_node_id {
379
34804
            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
380
34804
            if node_data.is_virtual_view_node() {
381
220
                return Ok(IntrinsicSizes {
382
220
                    min_content_width: 300.0,
383
220
                    max_content_width: 300.0,
384
220
                    preferred_width: None, // Will be determined by CSS or flex-grow
385
220
                    min_content_height: 150.0,
386
220
                    max_content_height: 150.0,
387
220
                    preferred_height: None, // Will be determined by CSS or flex-grow
388
220
                });
389
34584
            }
390
            
391
            // +spec:containing-block:bb5a12 - replaced element intrinsic sizes using initial containing block
392
            // +spec:display-property:7127f9 - intrinsic sizes of replaced elements without natural sizes (300x150 fallback, aspect ratio)
393
            // +spec:display-property:f9cede - replaced elements derive intrinsic size from natural dimensions
394
            // +spec:writing-modes:b18121 - stretch fit inline size from available space, calculate block size via aspect ratio
395
34584
            if let NodeType::Image(image_ref) = node_data.get_node_type() {
396
220
                let size = image_ref.get_size();
397
                // +spec:containing-block:1da6dc - use initial CB inline size for replaced elements with aspect ratio but no intrinsic size
398
                // Per css-sizing-3 §5.1: "use an inline size matching the corresponding dimension
399
                // of the initial containing block and calculate the other dimension using the aspect ratio"
400
220
                let has_intrinsic = size.width > 0.0 || size.height > 0.0;
401
220
                let (width, height) = if size.width > 0.0 && size.height > 0.0 {
402
132
                    (size.width, size.height)
403
88
                } else if size.width > 0.0 {
404
                    (size.width, size.width / 2.0)
405
88
                } else if size.height > 0.0 {
406
                    // Has intrinsic height but no width — use initial CB inline dimension
407
                    (self.ctx.viewport_size.width, size.height)
408
                } else {
409
                    // +spec:replaced-elements:43376b - 300px fallback with 2:1 ratio for replaced elements
410
                    // No intrinsic dimensions — cap at 300x150 per CSS 2.2 §10.3.2
411
                    // +spec:width-calculation:3b0efe - auto width fallback: 300px capped to device width
412
                    // +spec:width-calculation:16c305 - auto height fallback: 2:1 ratio, max 150px
413
88
                    let w = self.ctx.viewport_size.width.min(300.0);
414
88
                    (w, w / 2.0)
415
                };
416
                // A replaced element with NO intrinsic size (e.g. a RenderImageCallback
417
                // <img> like the AzulPaint canvas) must behave like a VirtualView: keep
418
                // the 300×150 fallback as the min/max-content (so it has a sensible
419
                // default) but leave `preferred` as None so `flex-grow` / explicit CSS
420
                // can size it. A `Some(preferred)` here pins the box and defeats
421
                // flex-grow (the canvas was laid out 300×0 — see the VirtualView arm
422
                // above, which already uses None for exactly this reason). Images WITH
423
                // a real intrinsic size keep `preferred = Some` so they display at their
424
                // natural size when unconstrained.
425
220
                let (pref_w, pref_h) = if has_intrinsic {
426
132
                    (Some(width), Some(height))
427
                } else {
428
88
                    (None, None)
429
                };
430
220
                return Ok(IntrinsicSizes {
431
220
                    min_content_width: width,
432
220
                    max_content_width: width,
433
220
                    preferred_width: pref_w,
434
220
                    min_content_height: height,
435
220
                    max_content_height: height,
436
220
                    preferred_height: pref_h,
437
220
                });
438
34364
            }
439
88
        }
440

            
441
34452
        match node.formatting_context {
442
            FormattingContext::Block { .. } => {
443
                // Check if this block establishes an Inline Formatting Context (IFC).
444
                // Per CSS 2.2 §9.2.1.1: A block container with mixed block-level and
445
                // inline-level children creates anonymous block boxes to wrap the inline
446
                // content. So we only treat as IFC root if there are NO block-level children.
447
                //
448
                // We check the actual CSS display property, NOT formatting_context,
449
                // because a display:block element with only inline children gets
450
                // FormattingContext::Inline (meaning "establishes IFC for its children"),
451
                // which is different from being an inline element itself.
452
7876
                let has_block_child = tree.children(node_index).iter().any(|&child_idx| {
453
5192
                    tree.get(child_idx)
454
5192
                        .and_then(|c| c.dom_node_id)
455
5192
                        .map(|dom_id| {
456
5104
                            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
457
                            // Text nodes are inline-level
458
5104
                            if matches!(node_data.get_node_type(), NodeType::Text(_)) {
459
                                return false;
460
5104
                            }
461
5104
                            let display = get_display_type(self.ctx.styled_dom, dom_id);
462
5104
                            display.creates_block_context()
463
5104
                        })
464
5192
                        .unwrap_or(false)
465
5192
                });
466

            
467
7876
                let has_inline_child = tree.children(node_index).iter().any(|&child_idx| {
468
7304
                    tree.get(child_idx)
469
7304
                        .and_then(|c| c.dom_node_id)
470
7304
                        .map(|dom_id| {
471
7216
                            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
472
7216
                            if matches!(node_data.get_node_type(), NodeType::Text(_)) {
473
                                return true;
474
7216
                            }
475
7216
                            let display = get_display_type(self.ctx.styled_dom, dom_id);
476
7216
                            matches!(display,
477
                                LayoutDisplay::Inline
478
                                | LayoutDisplay::InlineBlock
479
                                | LayoutDisplay::InlineFlex
480
                                | LayoutDisplay::InlineGrid
481
                                | LayoutDisplay::InlineTable
482
                            )
483
7216
                        })
484
7304
                        .unwrap_or(false)
485
7304
                });
486

            
487
                // IFC root only if there are inline children and NO block children.
488
                // If there are block children, text nodes get anonymous block wrappers.
489
7876
                let is_ifc_root = has_inline_child && !has_block_child;
490
                
491
                // Also check if this block has direct text content (text nodes in DOM)
492
                // but ONLY if there are no block-level layout children
493
7876
                let has_direct_text = if !has_block_child {
494
2772
                    if let Some(dom_id) = node.dom_node_id {
495
2772
                        let node_hierarchy = &self.ctx.styled_dom.node_hierarchy.as_container();
496
2772
                        dom_id.az_children(node_hierarchy).any(|child_id| {
497
                            let child_node_data = &self.ctx.styled_dom.node_data.as_container()[child_id];
498
                            matches!(child_node_data.get_node_type(), NodeType::Text(_))
499
                        })
500
                    } else {
501
                        false
502
                    }
503
                } else {
504
5104
                    false
505
                };
506
                
507
7876
                if is_ifc_root || has_direct_text {
508
                    // This block is an IFC root - measure all inline content ONCE
509
                    self.calculate_ifc_root_intrinsic_sizes(tree, node_index)
510
                } else {
511
                    // This is a BFC root (only block children) - aggregate child sizes
512
7876
                    self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics)
513
                }
514
            }
515
            FormattingContext::Inline => {
516
                // There are THREE cases for FormattingContext::Inline:
517
                // 1. A Text node (NodeType::Text) - this IS the text content itself
518
                //    -> Needs to measure itself as an atomic inline unit
519
                // 2. An IFC root - a block with only inline children (has text child nodes)
520
                //    -> Should measure its inline content
521
                // 3. A true inline element (display: inline, e.g., <span>) with no text
522
                //    -> Returns default(0,0), measured by parent IFC root
523
                //
524
                // We distinguish by:
525
                // - Checking if THIS node is a Text node (case 1)
526
                // - Checking if this subtree contains any text (case 2)
527
                //
528
                // Why descendants, not just direct children: for `<span><a>text</a></span>`,
529
                // the `<span>` is a layout-tree IFC root (layout_ifc is called on it), but
530
                // its direct DOM children are inline elements, not text. Restricting the
531
                // check to direct text children would zero out the span's intrinsic width
532
                // even though the cell content width depends on it.
533
17072
                let is_text_node = if let Some(dom_id) = node.dom_node_id {
534
16984
                    let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
535
16984
                    matches!(node_data.get_node_type(), NodeType::Text(_))
536
                } else {
537
88
                    false
538
                };
539

            
540
17072
                let has_text_in_subtree = if let Some(dom_id) = node.dom_node_id {
541
16984
                    subtree_contains_text(self.ctx.styled_dom, dom_id)
542
                } else {
543
88
                    false
544
                };
545

            
546
17072
                if is_text_node || has_text_in_subtree {
547
                    // Case 1 or 2: Text node or IFC root - measure inline content
548
16984
                    self.calculate_ifc_root_intrinsic_sizes(tree, node_index)
549
                } else {
550
                    // Case 3: True inline element - measured by parent IFC root
551
88
                    Ok(IntrinsicSizes::default())
552
                }
553
            }
554
            FormattingContext::InlineBlock => {
555
                // Inline-block IS an atomic inline - it needs its own intrinsic size.
556
                // Check layout tree children AND direct DOM text children (text nodes
557
                // are not in the layout tree, only in the DOM).
558
88
                let has_inline_children = tree.children(node_index).iter().any(|&child_idx| {
559
88
                    tree.get(child_idx)
560
88
                        .map(|c| matches!(c.formatting_context, FormattingContext::Inline))
561
88
                        .unwrap_or(false)
562
88
                });
563

            
564
88
                let has_direct_text = if let Some(dom_id) = node.dom_node_id {
565
88
                    let node_hierarchy = &self.ctx.styled_dom.node_hierarchy.as_container();
566
88
                    dom_id.az_children(node_hierarchy).any(|child_id| {
567
88
                        let child_node_data = &self.ctx.styled_dom.node_data.as_container()[child_id];
568
88
                        matches!(child_node_data.get_node_type(), NodeType::Text(_))
569
88
                    })
570
                } else {
571
                    false
572
                };
573

            
574
88
                if has_inline_children || has_direct_text {
575
                    // InlineBlock with inline children - measure as IFC root.
576
                    // Returns content-level intrinsic sizes (no margin/padding/border).
577
                    // The parent adds box-model extras via calculate_block_intrinsic_sizes,
578
                    // and calculate_used_size_for_node adds padding+border for border-box.
579
88
                    let intrinsic = self.calculate_ifc_root_intrinsic_sizes(tree, node_index)?;
580

            
581
88
                    Ok(intrinsic)
582
                } else {
583
                    // InlineBlock with block children - aggregate like block
584
                    self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics)
585
                }
586
            }
587
            FormattingContext::Table => {
588
1452
                self.calculate_table_intrinsic_sizes(tree, node_index, child_intrinsics)
589
            }
590
            FormattingContext::Flex => {
591
2728
                self.calculate_flex_intrinsic_sizes(tree, node_index, child_intrinsics)
592
            }
593
5236
            _ => self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics),
594
        }
595
34892
    }
596
    
597
    // +spec:intrinsic-sizing:ea2c2c - §5.1 min-content size = size as float with auto; max-content = no wrapping
598
    /// Calculate intrinsic sizes for an IFC root (a block containing inline content).
599
    /// This collects ALL inline descendants' text and measures it ONCE.
600
    // +spec:intrinsic-sizing:8f3c0c - hanging glyphs must be excluded from intrinsic size measurement
601
20812
    fn calculate_ifc_root_intrinsic_sizes(
602
20812
        &mut self,
603
20812
        tree: &LayoutTree,
604
20812
        node_index: usize,
605
20812
    ) -> Result<IntrinsicSizes> {
606
        // [g75] 0x60758 = how many times this IFC sizer is entered; 0x6075C = node_index of THIS call.
607
20812
        unsafe {
608
20812
            let c = crate::az_mark_read(0x60758).wrapping_add(1);
609
20812
            crate::az_mark((0x60758) as u32, (c) as u32);
610
20812
            crate::az_mark((0x6075C) as u32, (node_index as u32) as u32);
611
20812
        }
612
        // Collect all inline content from this IFC root and its inline descendants
613
        // [g76] EXPLICIT match (was `?`): the g75 markers showed collect_inline_content reaching its
614
        // completion marker B8 (Ok at the source level) yet the IFC sizer never advancing to 0xA1 —
615
        // i.e. the lifted `Result<Vec<InlineContent>, LayoutError>` return arrives as Err at this call
616
        // site (a complex by-value Result-return mis-lift). 0x60760 = 1 (Ok) / 0xEE (Err-but-B8-ran).
617
        // RESILIENCE: on Err, degrade to empty content (→ default intrinsic) instead of aborting the
618
        // WHOLE layout with InvalidTree, so the page renders and the next real blocker surfaces.
619
        // g76 PROVED: degrading to Vec::new() here (resilience) lets layout proceed past this
620
        // InvalidTree but then HANGS in the downstream actual-layout shaping (the documented g47
621
        // hashbrown empty-map infinite loop). So for a CLEAN (non-hanging) state we PROPAGATE the Err
622
        // (same as the original `?`), keeping the 0x60760 diagnostic. To chase the g47 hang, flip the
623
        // Err arm back to `Vec::new()`. 0x60760 = 1 (Ok) / 0xEE (Err-despite-B8 = the Result mis-lift).
624
        // [g78] OUT-PARAM refactor: the by-value `Result<Vec<InlineContent>, LayoutError>` return
625
        // mis-lifted Ok→Err (g76/g77 PROVED it: 0x60760=0xEE despite the source reaching B8). Filling
626
        // a `&mut Vec` out-param and returning `Result<()>` (register-returned, NO sret-of-Vec) lifts
627
        // cleanly — the established M12.7 "a pointer arg lifts cleanly" pattern. 0x60760 should now =1.
628
20812
        let collect_result = collect_inline_content(&mut self.ctx, tree, node_index);
629
        #[cfg(feature = "web_lift")]
630
        unsafe { crate::az_mark((0x60760) as u32, (if collect_result.is_ok() { 0x00000001u32 } else { 0x000000EEu32 }) as u32); }
631
20812
        let inline_content: Vec<InlineContent> = collect_result?;
632

            
633
20812
        if inline_content.is_empty() {
634
            return Ok(IntrinsicSizes::default());
635
20812
        }
636

            
637
        // Get pre-loaded fonts from font manager
638
20812
        let loaded_fonts = self.ctx.font_manager.get_loaded_fonts();
639

            
640
        // +spec:intrinsic-sizing:ae8beb - min-content = zero-width CB, max-content = infinite-width CB
641
        // +spec:intrinsic-sizing:8c94e2 - min-content/max-content intrinsic size determination via constrained layout
642
        // Use `measure_intrinsic_widths` instead of two `layout_flow` passes (fix B):
643
        // it runs stages 1–4 of the pipeline once (logical → BiDi → shape → orient)
644
        // and derives min/max-content by scanning the shaped items directly. This
645
        // avoids the BreakCursor line-breaking loop entirely — that loop clones
646
        // every ShapedCluster it inspects via `peek_next_unit` and accounted for
647
        // 24% of total CPU on the text_2000 stress fixture. Shaping is cached
648
        // at the per-item level (keyed on text+style), so the subsequent real
649
        // layout_flow call for this content gets pure cache hits for stages 1–3.
650
20812
        let constraints = UnifiedConstraints::default();
651
        // [g79 DIAG] Probe the font state at shaping time, then convert the downstream shape_text
652
        // HANG (g47 hashbrown empty-map loop) → trap so the harness RETURNS and these markers are
653
        // readable (can't read markers from a hung wasm call). Tests the #4→#3 coupling: if
654
        // 0x60768(font_chain_cache.len) / 0x6076C(loaded_fonts.len) are 0, the EMPTY FONT CHAIN is
655
        // the ROOT of the hang (no font → allsorts builds empty hashbrown maps → RawIter loops).
656
        // [g82] CONDITIONAL trap: if the font_chain_cache is still EMPTY, shaping would HANG (allsorts
657
        // builds empty hashbrown maps → g47 RawIter loop) → trap instead so the markers are readable
658
        // (non-hang). If the chain is NON-empty (unique_font_keys BTreeMap fix worked + populated it),
659
        // PROCEED into measure → shape → text should MEASURE. g81 hung (no conditional) → need to know
660
        // whether the chain populated. 0x60768=chain.len, 0x6076C=loaded_fonts.len, 0x60704=0xA15.
661
        #[cfg(feature = "web_lift")]
662
        {
663
            let cl = self.ctx.font_manager.font_chain_cache.len();
664
            unsafe {
665
                crate::az_mark((0x60768) as u32, (cl as u32) as u32);
666
                crate::az_mark((0x6076C) as u32, (loaded_fonts.len() as u32) as u32);
667
                crate::az_mark((0x60704) as u32, (0xA15u32) as u32);
668
            }
669
            // [g88] g85+g87 PROVED whack-a-mole does NOT converge: BTreeMap'd unique_font_keys (chain
670
            // ✓), supported_features+lookups_index (g85), ReadCache (g87) — STILL HANGS. Too many
671
            // hashbrown empty-map sites across allsorts/std/rust-fontconfig. The ONLY convergent fix is
672
            // the SYSTEMIC transpiler empty-static mirror (force the lifted hashbrown ctrl-scan to read
673
            // 0xFF not 0x00). TEMP non-hang trap until that lands. ★ REMOVE to test the systemic fix.
674
            // [g93] PROCEED into shaping to test the AZ_FORCE_MIRROR_VMADDRS hashbrown-EMPTY_GROUP fix.
675
            // If text MEASURES → the forced const pages contained EMPTY_GROUP → systemic fix found.
676
            let _ = (cl, loaded_fonts.len());
677
        }
678
20812
        let intrinsic_text = match self.text_cache.measure_intrinsic_widths(
679
20812
            &inline_content,
680
20812
            &[],
681
20812
            &constraints,
682
20812
            &self.ctx.font_manager.font_chain_cache,
683
20812
            &self.ctx.font_manager.fc_cache,
684
20812
            &loaded_fonts,
685
20812
            self.ctx.debug_messages,
686
20812
        ) {
687
20812
            Ok(r) => r,
688
            Err(_) => {
689
                return Ok(IntrinsicSizes {
690
                    min_content_width: 100.0,
691
                    max_content_width: 300.0,
692
                    preferred_width: None,
693
                    min_content_height: 20.0,
694
                    max_content_height: 20.0,
695
                    preferred_height: None,
696
                });
697
            }
698
        };
699

            
700
20812
        let min_width = intrinsic_text.min_content_width;
701
20812
        let max_width = intrinsic_text.max_content_width;
702

            
703
        // +spec:display-property:c587fd - min-content block size equals max-content block size for block containers, tables, inline boxes
704
        // +spec:intrinsic-sizing:02eedc - min-content block size equals max-content block size for block containers
705
        // For a single-line max-content layout the height is one line box;
706
        // `measure_intrinsic_widths` returns exactly that.
707
20812
        let max_content_height = intrinsic_text.max_content_height;
708

            
709
        // NOTE(writing-modes): min_content_width / max_content_width are named for
710
        // the physical axis. In vertical writing modes the "inline" axis is vertical,
711
        // so these are swapped by calculate_block_intrinsic_sizes when computing
712
        // the parent's intrinsic sizes. The physical naming is intentional here.
713
20812
        Ok(IntrinsicSizes {
714
20812
            min_content_width: min_width,
715
20812
            max_content_width: max_width,
716
20812
            preferred_width: None,
717
20812
            min_content_height: max_content_height,
718
20812
            max_content_height,
719
20812
            preferred_height: None,
720
20812
        })
721
20812
    }
722

            
723
    // +spec:containing-block:bb0658 - percentage block-sizes behave as auto during intrinsic computation (no CSS height resolution here)
724
    // +spec:display-contents:84fe7f - cyclic percentage contributions: percentage-sized children use auto during intrinsic sizing
725
    // +spec:min-max-sizing:411904 - percentage block-sizes treated as auto during intrinsic sizing (content-sized CB)
726
    // +spec:min-max-sizing:737e62 - percentage heights don't resolve inside content-sized containing blocks
727
13112
    fn calculate_block_intrinsic_sizes(
728
13112
        &mut self,
729
13112
        tree: &LayoutTree,
730
13112
        node_index: usize,
731
13112
        child_intrinsics: &[(usize, IntrinsicSizes)],
732
13112
    ) -> Result<IntrinsicSizes> {
733
13112
        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
734
13112
        let writing_mode = if let Some(dom_id) = node.dom_node_id {
735
13112
            let node_state =
736
13112
                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
737
13112
            get_writing_mode(self.ctx.styled_dom, dom_id, node_state).unwrap_or_default()
738
        } else {
739
            LayoutWritingMode::default()
740
        };
741

            
742
        // NOTE: Text content detection is now handled in calculate_node_intrinsic_sizes
743
        // which calls calculate_ifc_root_intrinsic_sizes for blocks with inline content.
744
        // This function now only handles pure block containers (BFC roots).
745
        // +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)
746

            
747
13112
        let mut max_child_min_cross = 0.0f32;
748
13112
        let mut max_child_max_cross = 0.0f32;
749
13112
        let mut total_main_size = 0.0;
750
        // Track margins for CSS 2.2 §8.3.1 collapsing in the block direction.
751
        // Block margins collapse between siblings (max instead of sum) and
752
        // parent-child margins can escape (first/last child).
753
13112
        let mut last_margin_main_end = 0.0f32;
754
13112
        let mut is_first_child = true;
755

            
756
14784
        for &child_index in tree.children(node_index) {
757
23232
            if let Some(child_intrinsic) = child_intrinsics.iter().find(|(k, _)| k == &child_index).map(|(_, v)| v) {
758
                // +spec:intrinsic-sizing:ed72bb - intrinsic contributions based on outer size, auto margins as zero
759
14784
                let child_node = tree.get(child_index);
760
14784
                let (cross_extras, main_border_padding, main_margin_start, main_margin_end) =
761
14784
                    if let Some(cn) = child_node {
762
14784
                        let bp = cn.box_props.unpack();
763
14784
                        let h = bp.margin.left + bp.margin.right
764
14784
                              + bp.border.left + bp.border.right
765
14784
                              + bp.padding.left + bp.padding.right;
766
14784
                        let v_bp = bp.border.top + bp.border.bottom
767
14784
                              + bp.padding.top + bp.padding.bottom;
768
14784
                        match writing_mode {
769
14784
                            LayoutWritingMode::HorizontalTb => (h, v_bp, bp.margin.top, bp.margin.bottom),
770
                            _ => (v_bp, h, bp.margin.left, bp.margin.right),
771
                        }
772
                    } else {
773
                        (0.0, 0.0, 0.0, 0.0)
774
                    };
775

            
776
14784
                let (child_min_cross, child_max_cross, child_border_box_main) = match writing_mode {
777
14784
                    LayoutWritingMode::HorizontalTb => (
778
14784
                        child_intrinsic.min_content_width + cross_extras,
779
14784
                        child_intrinsic.max_content_width + cross_extras,
780
14784
                        child_intrinsic.max_content_height + main_border_padding,
781
14784
                    ),
782
                    _ => (
783
                        child_intrinsic.min_content_height + cross_extras,
784
                        child_intrinsic.max_content_height + cross_extras,
785
                        child_intrinsic.max_content_width + main_border_padding,
786
                    ),
787
                };
788

            
789
14784
                max_child_min_cross = max_child_min_cross.max(child_min_cross);
790
14784
                max_child_max_cross = max_child_max_cross.max(child_max_cross);
791

            
792
                // CSS 2.2 §8.3.1 margin collapsing for intrinsic sizing:
793
                // - First child's margin-start can escape (don't add to total)
794
                // - Between siblings: collapsed gap = max(prev_end, curr_start)
795
                // - Last child's margin-end can escape (don't add to total)
796
14784
                if is_first_child {
797
10340
                    is_first_child = false;
798
10340
                    // First child: top margin may escape, don't add it
799
10340
                } else {
800
4444
                    // Sibling gap: collapsed margin between prev bottom and current top
801
4444
                    let collapsed_gap = crate::solver3::fc::collapse_margins(
802
4444
                        last_margin_main_end, main_margin_start
803
4444
                    );
804
4444
                    total_main_size += collapsed_gap;
805
4444
                }
806

            
807
14784
                total_main_size += child_border_box_main;
808
14784
                last_margin_main_end = main_margin_end;
809
            }
810
        }
811
        // Last child's margin-end may escape — don't add it to total_main_size
812

            
813
13112
        let (min_width, max_width, min_height, max_height) = match writing_mode {
814
13112
            LayoutWritingMode::HorizontalTb => (
815
13112
                max_child_min_cross,
816
13112
                max_child_max_cross,
817
13112
                total_main_size,
818
13112
                total_main_size,
819
13112
            ),
820
            _ => (
821
                total_main_size,
822
                total_main_size,
823
                max_child_min_cross,
824
                max_child_max_cross,
825
            ),
826
        };
827

            
828
13112
        Ok(IntrinsicSizes {
829
13112
            min_content_width: min_width,
830
13112
            max_content_width: max_width,
831
13112
            preferred_width: None,
832
13112
            min_content_height: min_height,
833
13112
            max_content_height: max_height,
834
13112
            preferred_height: None,
835
13112
        })
836
13112
    }
837

            
838
    // The max-content main size is the sum of items' max-content contributions.
839
    // The min-content main size of a single-line flex container is the sum of items'
840
    // min-content contributions. For multi-line, it is the largest min-content contribution.
841
    // Auto margins on flex items are treated as 0 for this computation.
842
2728
    fn calculate_flex_intrinsic_sizes(
843
2728
        &mut self,
844
2728
        tree: &LayoutTree,
845
2728
        node_index: usize,
846
2728
        child_intrinsics: &[(usize, IntrinsicSizes)],
847
2728
    ) -> Result<IntrinsicSizes> {
848
2728
        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
849

            
850
        // Determine flex-direction to know if main axis is horizontal or vertical
851
2728
        let is_row = if let Some(dom_id) = node.dom_node_id {
852
2728
            let node_state =
853
2728
                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
854
2728
            match get_flex_direction(self.ctx.styled_dom, dom_id, &node_state) {
855
2728
                MultiValue::Exact(dir) => matches!(dir, LayoutFlexDirection::Row | LayoutFlexDirection::RowReverse),
856
                _ => true, // default is row
857
            }
858
        } else {
859
            true // default flex-direction is row
860
        };
861

            
862
2728
        let mut sum_main_min: f32 = 0.0;
863
2728
        let mut sum_main_max: f32 = 0.0;
864
2728
        let mut max_main_min: f32 = 0.0;
865
2728
        let mut max_cross_min: f32 = 0.0;
866
2728
        let mut max_cross_max: f32 = 0.0;
867

            
868
5720
        for &child_index in tree.children(node_index) {
869
16368
            if let Some(child_intrinsic) = child_intrinsics.iter().find(|(k, _)| k == &child_index).map(|(_, v)| v) {
870
5720
                let (child_main_min, child_main_max, child_cross_min, child_cross_max) = if is_row {
871
4488
                    (
872
4488
                        child_intrinsic.min_content_width,
873
4488
                        child_intrinsic.max_content_width,
874
4488
                        child_intrinsic.min_content_height,
875
4488
                        child_intrinsic.max_content_height,
876
4488
                    )
877
                } else {
878
1232
                    (
879
1232
                        child_intrinsic.min_content_height,
880
1232
                        child_intrinsic.max_content_height,
881
1232
                        child_intrinsic.min_content_width,
882
1232
                        child_intrinsic.max_content_width,
883
1232
                    )
884
                };
885

            
886
5720
                sum_main_max += child_main_max;
887
5720
                sum_main_min += child_main_min;
888
                // For multi-line min-content, track the largest single item
889
5720
                max_main_min = max_main_min.max(child_main_min);
890

            
891
                // Cross axis: largest child determines the container's cross size
892
5720
                max_cross_min = max_cross_min.max(child_cross_min);
893
5720
                max_cross_max = max_cross_max.max(child_cross_max);
894
            }
895
        }
896

            
897
        // For single-line (nowrap), min-content = sum; for multi-line (wrap), min-content = max
898
        // Default flex-wrap is nowrap (single-line)
899
2728
        let is_single_line = if let Some(dom_id) = node.dom_node_id {
900
2728
            let node_state =
901
2728
                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
902
2728
            let wrap_prop = crate::solver3::getters::get_flex_wrap_prop(
903
2728
                self.ctx.styled_dom, dom_id, &node_state,
904
            );
905
2728
            match wrap_prop {
906
                Some(val) => matches!(
907
                    val.get_property_or_default().unwrap_or_default(),
908
                    LayoutFlexWrap::NoWrap
909
                ),
910
2728
                None => true, // default is nowrap
911
            }
912
        } else {
913
            true
914
        };
915

            
916
2728
        let min_main = if is_single_line { sum_main_min } else { max_main_min };
917
2728
        let max_main = sum_main_max;
918

            
919
2728
        if is_row {
920
1892
            Ok(IntrinsicSizes {
921
1892
                min_content_width: min_main,
922
1892
                max_content_width: max_main,
923
1892
                preferred_width: None,
924
1892
                min_content_height: max_cross_min,
925
1892
                max_content_height: max_cross_max,
926
1892
                preferred_height: None,
927
1892
            })
928
        } else {
929
836
            Ok(IntrinsicSizes {
930
836
                min_content_width: max_cross_min,
931
836
                max_content_width: max_cross_max,
932
836
                preferred_width: None,
933
836
                min_content_height: min_main,
934
836
                max_content_height: max_main,
935
836
                preferred_height: None,
936
836
            })
937
        }
938
2728
    }
939

            
940
    /// Calculate intrinsic sizes for a table element by aggregating cell content
941
    /// widths per column and row heights.
942
    /// +spec:table-layout:93b13c - shrink-to-fit for tables uses intrinsic sizing
943
1452
    fn calculate_table_intrinsic_sizes(
944
1452
        &mut self,
945
1452
        tree: &LayoutTree,
946
1452
        node_index: usize,
947
1452
        child_intrinsics: &[(usize, IntrinsicSizes)],
948
1452
    ) -> Result<IntrinsicSizes> {
949
        // Collect per-column min/max widths and total row heights.
950
        // Table structure: table > row-group? > row > cell
951
1452
        let mut col_min: Vec<f32> = Vec::new();
952
1452
        let mut col_max: Vec<f32> = Vec::new();
953
1452
        let mut total_height = 0.0f32;
954

            
955
        // Iterate rows — children may be row groups (thead/tbody/tfoot) or direct rows
956
1452
        let mut rows: Vec<usize> = Vec::new();
957
1496
        for &child_idx in tree.children(node_index) {
958
1496
            let child = match tree.get(child_idx) { Some(c) => c, None => continue };
959
1496
            match child.formatting_context {
960
1496
                FormattingContext::TableRow => rows.push(child_idx),
961
                FormattingContext::TableRowGroup => {
962
                    // Row group contains rows
963
                    for &row_idx in tree.children(child_idx) {
964
                        if let Some(row) = tree.get(row_idx) {
965
                            if matches!(row.formatting_context, FormattingContext::TableRow) {
966
                                rows.push(row_idx);
967
                            }
968
                        }
969
                    }
970
                }
971
                _ => {}
972
            }
973
        }
974

            
975
2948
        for &row_idx in &rows {
976
1496
            let mut row_height = 0.0f32;
977
1496
            let mut col = 0usize;
978
3740
            for &cell_idx in tree.children(row_idx) {
979
3828
                let cell_intrinsic = child_intrinsics.iter().find(|(k, _)| k == &cell_idx).map(|(_, v)| *v)
980
3740
                    .unwrap_or_default();
981
                // Also check if cell has IFC content we can measure
982
3740
                let cell_is = if cell_intrinsic.max_content_width > 0.0 {
983
                    cell_intrinsic
984
                } else {
985
                    // Try to measure cell content via IFC
986
3740
                    self.calculate_ifc_root_intrinsic_sizes(tree, cell_idx)
987
3740
                        .unwrap_or_default()
988
                };
989

            
990
                // Add cell box-model extras
991
3740
                let cell_node = tree.get(cell_idx);
992
3740
                let (h_extras, v_extras) = if let Some(cn) = cell_node {
993
3740
                    let bp = cn.box_props.unpack();
994
3740
                    (bp.padding.left + bp.padding.right + bp.border.left + bp.border.right,
995
3740
                     bp.padding.top + bp.padding.bottom + bp.border.top + bp.border.bottom)
996
                } else { (0.0, 0.0) };
997

            
998
3740
                let cell_min = cell_is.min_content_width + h_extras;
999
3740
                let cell_max = cell_is.max_content_width + h_extras;
3740
                let cell_h = cell_is.max_content_height + v_extras;
3740
                if col >= col_min.len() {
3696
                    col_min.push(cell_min);
3696
                    col_max.push(cell_max);
3696
                } else {
44
                    col_min[col] = col_min[col].max(cell_min);
44
                    col_max[col] = col_max[col].max(cell_max);
44
                }
3740
                row_height = row_height.max(cell_h);
3740
                col += 1;
            }
1496
            total_height += row_height;
        }
1452
        let min_width: f32 = col_min.iter().sum();
1452
        let max_width: f32 = col_max.iter().sum();
1452
        Ok(IntrinsicSizes {
1452
            min_content_width: min_width,
1452
            max_content_width: max_width,
1452
            min_content_height: total_height,
1452
            max_content_height: total_height,
1452
            preferred_width: None,
1452
            preferred_height: None,
1452
        })
1452
    }
}
/// Gathers all inline content for the intrinsic sizing pass.
///
/// This function recursively collects text and inline-level content according to
/// CSS Sizing Level 3, Section 4.1: "Intrinsic Sizes"
/// https://www.w3.org/TR/css-sizing-3/#intrinsic-sizes
///
/// For inline formatting contexts, we need to gather:
/// 1. Text nodes (inline content)
/// 2. Inline-level boxes (display: inline, inline-block, etc.)
/// 3. Atomic inline-level elements (replaced elements like images)
///
/// The key difference from `collect_and_measure_inline_content` in fc.rs is that
/// this version is used for intrinsic sizing (calculating min/max-content widths)
/// before the actual layout pass, so it must recursively gather content from
/// inline descendants without laying them out first.
20812
fn collect_inline_content_for_sizing<T: ParsedFontTrait>(
20812
    ctx: &mut LayoutContext<'_, T>,
20812
    tree: &LayoutTree,
20812
    ifc_root_index: usize,
20812
    out: &mut Vec<InlineContent>,
20812
) -> Result<()> {
20812
    ctx.debug_log(&format!(
20812
        "Collecting inline content from node {} for intrinsic sizing",
20812
        ifc_root_index
20812
    ));
    // [g78] fill the caller's out-param (was a local Vec returned by value → Ok→Err mis-lift).
    // Recursively collect inline content from this node and its inline descendants
20812
    collect_inline_content_recursive(ctx, tree, ifc_root_index, out)?;
    // [g73] B8 = top-level recursion returned Ok (collect_inline_content complete).
20812
    unsafe { crate::az_mark((0x6071C) as u32, (0xB8u32) as u32); }
20812
    ctx.debug_log(&format!(
20812
        "Collected {} inline content items from node {}",
20812
        out.len(),
20812
        ifc_root_index
20812
    ));
20812
    Ok(())
20812
}
/// Recursive helper for collecting inline content.
///
/// According to CSS Sizing Level 3, the intrinsic size of an inline formatting context
/// is based on all inline-level content, including text in nested inline elements.
///
/// This function:
/// - Collects text from the current node if it's a text node
/// - Collects text from DOM children (text nodes may not be in layout tree)
/// - Recursively collects from inline children (display: inline)
/// - Treats non-inline children as atomic inline-level boxes
38764
fn collect_inline_content_recursive<T: ParsedFontTrait>(
38764
    ctx: &mut LayoutContext<'_, T>,
38764
    tree: &LayoutTree,
38764
    node_index: usize,
38764
    content: &mut Vec<InlineContent>,
38764
) -> Result<()> {
    // [g75] capture node_index of EVERY recursion entry (0x60754) and mark the entry-tree.get
    // FAILURE distinctly (inline-phase=0xBAD) so a node_index that fails HERE (before B1) is
    // visible even though a PRIOR successful call already wrote B8. This is the suspected
    // InvalidTree site (phase stuck at 0xA0 + B8 reached ⇒ a 2nd IFC call fails at this get).
38764
    unsafe { crate::az_mark((0x60754) as u32, (node_index as u32) as u32); }
38764
    let node = match tree.get(node_index) {
38764
        Some(n) => n,
        None => {
            unsafe { crate::az_mark((0x6071C) as u32, (0xBADu32) as u32); }
            return Err(LayoutError::InvalidTree);
        }
    };
    // CRITICAL FIX: Text nodes may exist in the DOM but not as separate layout nodes!
    // We need to check the DOM children for text content.
38764
    let Some(dom_id) = node.dom_node_id else {
        // No DOM ID means this is a synthetic node, skip text extraction
        return process_layout_children(ctx, tree, node_index, content);
    };
    // First check if THIS node is a text node
38764
    if let Some(text) = extract_text_from_node(ctx.styled_dom, dom_id) {
20504
        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)));
20504
        ctx.debug_log(&format!("Found text in node {}: '{}'", node_index, text));
20504
        // Use split_text_for_whitespace to correctly handle white-space: pre with \n
20504
        let text_items = split_text_for_whitespace(
20504
            ctx.styled_dom,
20504
            dom_id,
20504
            &text,
20504
            style_props,
20504
        );
20504
        content.extend(text_items);
20504
    }
    // CRITICAL: Also check DOM children for text nodes!
    // Text nodes are often not represented as separate layout nodes.
    // However, we must SKIP children that already have a layout tree entry,
    // because those will be handled by process_layout_children() below.
    // Without this guard, text nodes present in both DOM and layout tree
    // get collected twice, causing inline-block containers to be ~2x too wide.
38764
    let node_hierarchy = &ctx.styled_dom.node_hierarchy.as_container();
38764
    for child_id in dom_id.az_children(node_hierarchy) {
        // Skip DOM children that have layout tree nodes - they will be
        // processed via process_layout_children -> collect_inline_content_recursive
18260
        if tree.dom_to_layout.contains_key(&child_id) {
18260
            continue;
        }
        // Check if this DOM child is a text node
        let child_dom_node = &ctx.styled_dom.node_data.as_container()[child_id];
        if let NodeType::Text(text_data) = child_dom_node.get_node_type() {
            let text = text_data.as_str().to_string();
            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)));
            ctx.debug_log(&format!(
                "Found text in DOM child of node {}: '{}'",
                node_index, text
            ));
            // Use split_text_for_whitespace to correctly handle white-space: pre with \n
            let text_items = split_text_for_whitespace(
                ctx.styled_dom,
                child_id,
                &text,
                style_props,
            );
            content.extend(text_items);
        }
    }
    // [g73] B6 = DOM-children loop done (about to process_layout_children).
38764
    unsafe { crate::az_mark((0x6071C) as u32, (0xB6u32) as u32); }
38764
    process_layout_children(ctx, tree, node_index, content)
38764
}
/// Helper to process layout tree children for inline content collection
38764
fn process_layout_children<T: ParsedFontTrait>(
38764
    ctx: &mut LayoutContext<'_, T>,
38764
    tree: &LayoutTree,
38764
    node_index: usize,
38764
    content: &mut Vec<InlineContent>,
38764
) -> Result<()> {
    use azul_css::props::basic::SizeMetric;
    use azul_css::props::layout::{LayoutHeight, LayoutWidth};
    // [g73] PLC entry: 0x60708 = 0xC0<<24 | node_index (which node's children we process).
38764
    unsafe { crate::az_mark((0x60708) as u32, (0xC0000000u32 | (node_index as u32 & 0xFFFFFF)) as u32); }
    // Process layout tree children (these are elements with layout properties)
38764
    for &child_index in tree.children(node_index) {
        // [g73] PLC loop: 0x6070C = current child_index being processed.
18260
        unsafe { crate::az_mark((0x6070C) as u32, (child_index as u32) as u32); }
        // 2026-06-02: was `.ok_or(LayoutError::InvalidTree)?` — a stray/invalid child_index in
        // tree.children (likely a Text node mis-listed during reconcile, since Text is INLINE
        // content not a layout-tree node) aborted the WHOLE intrinsic-sizing pass with
        // InvalidTree BEFORE the inline text got measured → label height 0. Skip gracefully so
        // measurement continues (the inline text is collected separately above, at the
        // collect_inline_content_recursive DOM-children loop). REAL fix = reconcile not listing it.
18260
        let Some(child_node) = tree.get(child_index) else { continue; };
18260
        let Some(child_dom_id) = child_node.dom_node_id else {
            continue;
        };
18260
        let display = get_display_property(ctx.styled_dom, Some(child_dom_id));
        // CSS Sizing Level 3: Inline-level boxes participate in the IFC
18260
        if display.unwrap_or_default() == LayoutDisplay::Inline {
            // Recursively collect content from inline children
            // This is CRITICAL for proper intrinsic width calculation!
17952
            ctx.debug_log(&format!(
17952
                "Recursing into inline child at node {}",
17952
                child_index
17952
            ));
17952
            collect_inline_content_recursive(ctx, tree, child_index, content)?;
        } else {
            // Non-inline children are treated as atomic inline-level boxes
            // (e.g., inline-block, images, floats)
            // Their intrinsic size must have been calculated in the bottom-up pass
308
            let intrinsic_sizes = tree.warm(child_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
            // CSS 2.2 § 10.3.9: For inline-block elements with explicit CSS width/height,
            // use the CSS-defined values instead of intrinsic sizes.
308
            let node_state =
308
                &ctx.styled_dom.styled_nodes.as_container()[child_dom_id].styled_node_state;
308
            let css_width = get_css_width(ctx.styled_dom, child_dom_id, node_state);
308
            let css_height = get_css_height(ctx.styled_dom, child_dom_id, node_state);
            // Resolve CSS width - use explicit value if set, otherwise fall back to intrinsic
308
            let used_width = match css_width {
88
                MultiValue::Exact(LayoutWidth::Px(px)) => {
                    // Convert PixelValue to f32
                    use azul_css::props::basic::pixel::{DEFAULT_FONT_SIZE, PT_TO_PX};
88
                    match px.metric {
88
                        SizeMetric::Px => px.number.get(),
                        SizeMetric::Pt => px.number.get() * PT_TO_PX,
                        SizeMetric::In => px.number.get() * 96.0,
                        SizeMetric::Cm => px.number.get() * 96.0 / 2.54,
                        SizeMetric::Mm => px.number.get() * 96.0 / 25.4,
                        SizeMetric::Em | SizeMetric::Rem => px.number.get() * DEFAULT_FONT_SIZE,
                        // +spec:containing-block:495930 - percentages in intrinsic sizing fall back to intrinsic contribution (css-sizing-3 §5.2.1)
                        // For percentages and viewport units, fall back to intrinsic
                        // +spec:containing-block:5246c0 - cyclic percentage: when containing block size depends on this box's intrinsic contribution, percentages fall back to intrinsic size
                        // +spec:containing-block:598124 - cyclic percentage contributions use intrinsic size
                        // +spec:height-calculation:ca9f19 - percentage-sized boxes use intrinsic size as contribution during intrinsic sizing
                        // +spec:width-calculation:7a384a - percentage-sized boxes behave as width:auto for intrinsic contributions (cyclic percentage)
                        _ => intrinsic_sizes.max_content_width,
                    }
                }
                MultiValue::Exact(LayoutWidth::MinContent) => intrinsic_sizes.min_content_width,
                MultiValue::Exact(LayoutWidth::MaxContent) => intrinsic_sizes.max_content_width,
                MultiValue::Exact(LayoutWidth::FitContent(_)) => {
                    // During intrinsic sizing, fit-content resolves to max-content
                    intrinsic_sizes.max_content_width
                }
                // For Auto or other values, use intrinsic size
220
                _ => intrinsic_sizes.max_content_width,
            };
            // +spec:containing-block:5145c5 - percentage block-size ignored in content-sized containing blocks during intrinsic sizing
            // Resolve CSS height - use explicit value if set, otherwise fall back to intrinsic
308
            let used_height = match css_height {
88
                MultiValue::Exact(LayoutHeight::Px(px)) => {
                    use azul_css::props::basic::pixel::{DEFAULT_FONT_SIZE, PT_TO_PX};
88
                    match px.metric {
88
                        SizeMetric::Px => px.number.get(),
                        SizeMetric::Pt => px.number.get() * PT_TO_PX,
                        SizeMetric::In => px.number.get() * 96.0,
                        SizeMetric::Cm => px.number.get() * 96.0 / 2.54,
                        SizeMetric::Mm => px.number.get() * 96.0 / 25.4,
                        SizeMetric::Em | SizeMetric::Rem => px.number.get() * DEFAULT_FONT_SIZE,
                        // +spec:containing-block:7d5e79 - percentages behave as auto when containing block height is auto (cyclic percentage contribution)
                        // +spec:height-calculation:7d807b - css-sizing-3 §5.2.1: percentage heights behave as auto during intrinsic sizing (cyclic percentage contribution)
                        // Percentages and viewport units fall back to intrinsic (treated as auto)
                        _ => intrinsic_sizes.max_content_height,
                    }
                }
                // is equivalent to automatic size
                MultiValue::Exact(LayoutHeight::MinContent) => intrinsic_sizes.max_content_height,
                // is equivalent to automatic size
                MultiValue::Exact(LayoutHeight::MaxContent) => intrinsic_sizes.max_content_height,
                MultiValue::Exact(LayoutHeight::FitContent(_)) => intrinsic_sizes.max_content_height,
220
                _ => intrinsic_sizes.max_content_height,
            };
308
            ctx.debug_log(&format!(
308
                "Found atomic inline child at node {}: display={:?}, intrinsic_width={}, used_width={}, css_width={:?}",
308
                child_index, display, intrinsic_sizes.max_content_width, used_width, css_width
308
            ));
            // Represent as a rectangular shape with the resolved dimensions
308
            content.push(InlineContent::Shape(InlineShape {
308
                shape_def: ShapeDefinition::Rectangle {
308
                    size: crate::text3::cache::Size {
308
                        width: used_width,
308
                        height: used_height,
308
                    },
308
                    corner_radius: None,
308
                },
308
                fill: None,
308
                stroke: None,
308
                baseline_offset: used_height,
308
                alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, child_dom_id),
308
                source_node_id: Some(child_dom_id),
308
            }));
        }
    }
38764
    Ok(())
38764
}
// Keep old name as an alias for backward compatibility
20812
pub fn collect_inline_content<T: ParsedFontTrait>(
20812
    ctx: &mut LayoutContext<'_, T>,
20812
    tree: &LayoutTree,
20812
    ifc_root_index: usize,
20812
) -> Result<Vec<InlineContent>> {
20812
    let mut out = Vec::new();
20812
    collect_inline_content_for_sizing(ctx, tree, ifc_root_index, &mut out)?;
20812
    Ok(out)
20812
}
// +spec:height-calculation:1c899b - width and height properties specify the preferred size of the box
/// Calculates the used size of a single node based on its CSS properties and
/// the available space provided by its containing block.
///
/// // +spec:display-contents:71ccde - extrinsic sizing: size determined by context (containing block), not contents
///
/// This implementation correctly handles writing modes and percentage-based sizes
/// according to the CSS specification:
/// 1. `width` and `height` CSS properties are resolved to pixel values. Percentages are calculated
///    based on the containing block's PHYSICAL dimensions (`width` for `width`, `height` for
///    `height`), regardless of writing mode.
/// 2. The resolved physical `width` is then mapped to the node's logical CROSS size.
/// 3. The resolved physical `height` is then mapped to the node's logical MAIN size.
/// 4. A final `LogicalSize` is constructed from these logical dimensions.
// +spec:overflow:3c4f25 - auto box sizes: four auto-determined size types resolved here
// +spec:width-calculation:fb0629 - width/margin used values depend on box type, auto replaced by suitable value
/// M12.7: out-of-line auto-width-block inline size — `(cb.width - margins - borders -
/// padding).max(0.0)`. Extracted from calc_used_size's auto-width Block arm so the
/// `.max(0.0)` runs in a small fn (proven to lift correctly), with a FRESH pointer
/// deref (the huge calc_used_size body hoists/spills cb.width and the remill lift then
/// reads it back 0). Returns by f32 (D0/V0 — the standard scalar return), NOT an out-ptr:
/// the out-ptr version computed 800 correctly but the caller's reload was opt-forwarded
/// to the init 0.0 across the opaque call (the helper's `*out` lowers to a direct
/// linear-mem store not modeled as aliasing the caller's slot). The f32 return is the
/// call's SSA result, which opt cannot replace. (The earlier "f32-return mis-lift" worry
/// was the 2×f32 *struct* HFA — a single scalar f32 return is fine.)
#[inline(never)]
11968
fn auto_block_inline_size(cb: &LogicalSize, bp: &BoxProps) -> f32 {
11968
    let aw = cb.width
11968
        - bp.margin.left
11968
        - bp.margin.right
11968
        - bp.border.left
11968
        - bp.border.right
11968
        - bp.padding.left
11968
        - bp.padding.right;
11968
    aw.max(0.0)
11968
}
54956
pub fn calculate_used_size_for_node(
54956
    styled_dom: &StyledDom,
54956
    dom_id: Option<NodeId>,
54956
    // M12.7: by-reference (GP-register pointer). A by-value LogicalSize is an HFA
54956
    // (2×f32) the remill lift stages as an 8-byte double into a V register, and that
54956
    // f64/d-register copyload mis-tracks to 0 in the wasm lift (single-f32 reads work,
54956
    // the 64-bit one doesn't) — so cb + viewport arrived 0 and every width came out 0.
54956
    // A pointer arg lifts cleanly; the body reads only .width/.height (auto-deref).
54956
    containing_block_size: &LogicalSize,
54956
    intrinsic: IntrinsicSizes,
54956
    _box_props: &BoxProps,
54956
    viewport_size: &LogicalSize,
54956
) -> Result<LogicalSize> {
54956
    let Some(id) = dom_id else {
        // Anonymous boxes:
        // CSS 2.2 § 9.2.1.1: Anonymous boxes inherit from their enclosing box.
        // The inline dimension fills the containing block's inline size,
        // and the block dimension is auto (content-based).
        // In horizontal-tb: inline=width, block=height.
        // In vertical modes: inline=height, block=width.
        //
        // Since anonymous boxes don't have a DOM node, we default to horizontal-tb.
        // The parent's writing mode is already reflected in containing_block_size.
264
        return Ok(LogicalSize::new(
264
            containing_block_size.width,
264
            if intrinsic.max_content_height > 0.0 {
                intrinsic.max_content_height
            } else {
                // Auto height - will be resolved from content
264
                0.0
            },
        ));
    };
54692
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
54692
    let css_width = get_css_width(styled_dom, id, node_state);
54692
    let css_height = get_css_height(styled_dom, id, node_state);
54692
    let writing_mode = get_writing_mode(styled_dom, id, node_state);
54692
    let display = get_display_property(styled_dom, Some(id));
54692
    let position = get_position_type(styled_dom, dom_id);
    // Construct the full WritingModeContext from resolved styles.
    // This determines how logical dimensions (inline/block) map to physical (width/height).
54692
    let wm_ctx = WritingModeContext::new(
54692
        writing_mode.unwrap_or_default(),
54692
        get_direction_property(styled_dom, id, node_state).unwrap_or_default(),
54692
        get_text_orientation_property(styled_dom, id, node_state).unwrap_or_default(),
    );
54692
    let is_vertical = !wm_ctx.is_horizontal();
    // +spec:display-property:06e0b1 - form controls (non-image) treated as non-replaced
    // Determine if this element is a replaced element (images, virtual views)
54692
    let node_data = &styled_dom.node_data.as_container()[id];
54692
    let is_replaced = matches!(node_data.get_node_type(), NodeType::Image(_))
54208
        || node_data.is_virtual_view_node();
    // +spec:width-calculation:79cdf8 - inline non-replaced: width property does not apply
    // +spec:width-calculation:972e86 - §10.3.1: width property does not apply to inline non-replaced elements
    // For inline non-replaced elements, override any explicit width to Auto.
54692
    let css_width = if display.unwrap_or_default() == LayoutDisplay::Inline
15884
        && !is_replaced
    {
15884
        MultiValue::Exact(LayoutWidth::Auto)
    } else {
38808
        css_width
    };
    // +spec:box-model:1197a5 - height does not apply to non-replaced inline elements
    // +spec:display-property:9cb33d - height does not apply to inline boxes
    // +spec:height-calculation:c03717 - height does not apply to inline non-replaced elements
    // CSS 2.2 §10.6.1 / CSS Inline 3 §6.4: height property does not apply to
    // inline, non-replaced elements. Override any explicit height to Auto.
54692
    let css_height = if display.unwrap_or_default() == LayoutDisplay::Inline
15884
        && !is_replaced
    {
15884
        MultiValue::Exact(LayoutHeight::Auto)
    } else {
38808
        css_height
    };
    // Remember if width/height were auto before consuming them
54692
    let width_is_auto = css_width.is_auto() || matches!(&css_width, MultiValue::Exact(LayoutWidth::Auto));
54692
    let height_is_auto = css_height.is_auto() || matches!(&css_height, MultiValue::Exact(LayoutHeight::Auto));
    // +spec:intrinsic-sizing:9e1c9d - non-quantitative values (auto, min-content, max-content) are not influenced by box-sizing
54692
    let width_is_quantitative = matches!(
28424
        &css_width,
        MultiValue::Exact(LayoutWidth::Px(_) | LayoutWidth::FitContent(_) | LayoutWidth::Calc(_))
    );
54692
    let height_is_quantitative = matches!(
29436
        &css_height,
        MultiValue::Exact(LayoutHeight::Px(_) | LayoutHeight::FitContent(_) | LayoutHeight::Calc(_))
    );
    // +spec:width-calculation:50d67a - automatic sizing concepts (width/height auto resolution)
    // +spec:width-calculation:564315 - §10.3 width calculation dispatch for all box types
    // Step 1: Resolve the CSS `width` property into a concrete pixel value.
    // CSS `width` always refers to the physical horizontal dimension, regardless of writing mode.
    // Percentage values resolve against the containing block's physical width.
    // In horizontal-tb: width = inline size. In vertical modes: width = block size.
    // The physical-to-logical mapping happens in Step 5 below.
    // Percentage values for `width` are resolved against the containing block's width.
    // +spec:width-calculation:febf0c - width/height "behaves as auto" when computed auto or percentage resolves against indefinite
54692
    let resolved_width = match css_width.unwrap_or_default() {
        LayoutWidth::Auto => {
            // +spec:width-calculation:ed6a34 - auto width on replaced element uses intrinsic width
            // CSS 2.2 §10.3.2: If 'width' has a computed value of 'auto', and the element
            // has an intrinsic width, then that intrinsic width is the used value of 'width'.
            // +spec:replaced-elements:992ea5 - block-level replaced elements use inline replaced width rules
            // §10.3.4: "The used value of 'width' is determined as for inline replaced elements."
            // +spec:replaced-elements:36de3e - §10.3.2/§10.3.4: auto width for inline/block replaced elements uses intrinsic width
            // +spec:replaced-elements:b9a780 - §10.3.2: inline replaced auto width = intrinsic width (conditions resolved during intrinsic size calc)
42152
            if is_replaced {
                // +spec:width-calculation:b41dbe - floating/inline replaced: auto width = intrinsic width
                // +spec:width-calculation:c62d35 - §10.3.2: auto width for replaced elements uses intrinsic width
                // +spec:width-calculation:d87ca4 - abs-replaced: auto width+height uses intrinsic width
                // For replaced elements (inline or block-level), auto width = intrinsic width.
                // The intrinsic sizes were already computed with the 300px fallback per §10.3.2.
1188
                intrinsic.max_content_width
            }
            // +spec:intrinsic-sizing:560697 - shrink-to-fit = clamp(min-content, stretch-fit, max-content)
40964
            else if get_float(styled_dom, id, node_state).unwrap_or(LayoutFloat::None) != LayoutFloat::None {
                // +spec:width-calculation:8d7047 - shrink-to-fit width per CSS2.1§10.3.5
                // +spec:width-calculation:0bb038 - shrink-to-fit for floating non-replaced elements (§10.3.5)
                // shrink-to-fit = min(max(preferred minimum width, available width), preferred width)
                // +spec:table-layout:93b13c - shrink-to-fit for floats, inline-blocks, table-cells;
                // orthogonal flows would require child block size as input (not yet implemented)
                // +spec:width-calculation:a6fd29 - shrink-to-fit width for floats: min(max(preferred minimum, available), preferred)
                // CSS 2.2 §10.3.5: For floats, auto width = shrink-to-fit
                let available_width = (containing_block_size.width
                    - _box_props.margin.left
                    - _box_props.margin.right
                    - _box_props.border.left
                    - _box_props.border.right
                    - _box_props.padding.left
                    - _box_props.padding.right)
                    .max(0.0);
                let preferred_minimum = intrinsic.min_content_width;
                let preferred = intrinsic.max_content_width;
                preferred_minimum.max(available_width).min(preferred).max(0.0)
            }
40964
            else if matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed) {
                // +spec:intrinsic-sizing:12a531 - abspos auto size = fit-content (shrink-to-fit)
                // +spec:width-calculation:0bb038 - shrink-to-fit width for abs-pos non-replaced elements
                // §10.3.7: abs-pos elements with auto width use shrink-to-fit
                // +spec:intrinsic-sizing:087b57 - abspos automatic size is fit-content (shrink-to-fit)
                // +spec:width-calculation:1661b4 - abs-pos non-replaced auto width uses shrink-to-fit (§10.3.7)
                // shrink-to-fit = min(max(preferred_minimum, available), preferred)
6336
                let available_width = (containing_block_size.width
6336
                    - _box_props.margin.left
6336
                    - _box_props.margin.right
6336
                    - _box_props.border.left
6336
                    - _box_props.border.right
6336
                    - _box_props.padding.left
6336
                    - _box_props.padding.right)
6336
                    .max(0.0);
6336
                let preferred_minimum = intrinsic.min_content_width;
6336
                let preferred = intrinsic.max_content_width;
6336
                preferred_minimum.max(available_width).min(preferred).max(0.0)
            } else {
            // +spec:width-calculation:472065 - orthogonal flow auto inline size: if this block
            // container establishes an orthogonal flow (child writing mode axis differs from
            // parent), its auto inline size should use the parent's block-axis size as available
            // space, falling back to the initial containing block size. Currently not implemented;
            // auto width always resolves against the containing block's width.
            // 'auto' width resolution depends on the display type.
34628
            match display.unwrap_or_default() {
                LayoutDisplay::Block
                | LayoutDisplay::FlowRoot
                | LayoutDisplay::ListItem
                | LayoutDisplay::Flex
                | LayoutDisplay::Grid => {
                    // +spec:box-model:503ea3 - margin + border + padding + width = containing block width
                    // +spec:box-model:5ed651 - stretch fit: size minus margins (auto=0), border, padding, floored at 0
                    // +spec:box-model:33b951 - stretch-fit inline size: available space minus margins/border/padding, floored at zero
                    // +spec:box-model:30b4d0 - stretch fit: available size minus margins (auto as zero), border, padding, floored at zero
                    // +spec:width-calculation:e2c8f6 - auto width for non-replaced blocks in normal flow per CSS2.1§10.3.3
                    // For block-level non-replaced elements,
                    // 'auto' width fills the containing block (minus margins, borders, padding).
                    // CSS 2.2 §10.3.3: width = containing_block_width - margin_left -
                    // margin_right - border_left - border_right - padding_left - padding_right
                    // +spec:width-calculation:aef2da - auto width: other auto values become 0, width follows from constraint equality
                    // M12.7: compute in a small #[inline(never)] helper with by-ref/out-ptr
                    // args. calc_used_size is a ~6KB fn (38 maxnum, heavy SROA); the remill
                    // lift spills + diverges the available_width copyload feeding `.max`
                    // (a marker read sees 800, the maxnum's copyload reads 0 → width 0). A
                    // small fn has clean register allocation; out-ptr avoids the f32-return
                    // mis-lift. cb/bp are already &-refs (GP-pointer args lift cleanly).
                    // M12.7: compute the auto-width in a small f32-RETURNING helper.
                    // Inline-in-calc reads cb.width back 0 (huge-fn lift divergence); the
                    // out-ptr helper's readback was opt-forwarded to init 0. The f32
                    // return comes back in D0 as the call's SSA result (opt can't forward
                    // the init over it), and with D8-D15 preserved across calc's later
                    // calls the value survives to the return.
11968
                    auto_block_inline_size(containing_block_size, _box_props)
                }
                LayoutDisplay::InlineBlock | LayoutDisplay::InlineGrid | LayoutDisplay::InlineFlex => {
                    // +spec:width-calculation:c01de8 - inline-block auto width uses shrink-to-fit (§10.3.9)
                    // shrink-to-fit = min(max(preferred_minimum, available), preferred)
176
                    let available_width = (containing_block_size.width
176
                        - _box_props.margin.left
176
                        - _box_props.margin.right
176
                        - _box_props.border.left
176
                        - _box_props.border.right
176
                        - _box_props.padding.left
176
                        - _box_props.padding.right)
176
                        .max(0.0);
176
                    let preferred_minimum = intrinsic.min_content_width;
176
                    let preferred = intrinsic.max_content_width;
176
                    preferred_minimum.max(available_width).min(preferred).max(0.0)
                }
                LayoutDisplay::Inline => {
                    // For inline elements, 'auto' width is the intrinsic/max-content width
9724
                    intrinsic.max_content_width
                }
2728
                LayoutDisplay::Table | LayoutDisplay::InlineTable => intrinsic.max_content_width,
                // Table cells: during intrinsic measurement, intrinsic sizes
                // aren't known yet (0). Use containing block width so content
                // can expand and be measured. The table layout algorithm sets
                // the final cell width from computed column widths.
                LayoutDisplay::TableCell => {
10032
                    if intrinsic.max_content_width > 0.0 {
9768
                        intrinsic.max_content_width
                    } else {
264
                        (containing_block_size.width
264
                            - _box_props.margin.left
264
                            - _box_props.margin.right
264
                            - _box_props.border.left
264
                            - _box_props.border.right
264
                            - _box_props.padding.left
264
                            - _box_props.padding.right)
264
                            .max(0.0)
                    }
                }
                // Other display types use intrinsic sizing
                _ => intrinsic.max_content_width,
            }
            }
        }
12540
        LayoutWidth::Px(px) => {
            // Resolve percentage or absolute pixel value
            use azul_css::props::basic::{
                pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
                SizeMetric,
            };
12540
            let pixels_opt = match px.metric {
11616
                SizeMetric::Px => Some(px.number.get()),
                SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
                SizeMetric::In => Some(px.number.get() * 96.0),
                SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
                SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
                SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
                SizeMetric::Vw => Some(px.number.get() / 100.0 * viewport_size.width),
                SizeMetric::Vh => Some(px.number.get() / 100.0 * viewport_size.height),
                SizeMetric::Vmin => Some(px.number.get() / 100.0 * viewport_size.width.min(viewport_size.height)),
                SizeMetric::Vmax => Some(px.number.get() / 100.0 * viewport_size.width.max(viewport_size.height)),
924
                SizeMetric::Percent => None,
            };
12540
            match pixels_opt {
11616
                Some(pixels) => pixels,
924
                None => match px.to_percent() {
924
                    Some(p) => {
924
                        let result = resolve_percentage_with_box_model(
924
                            containing_block_size.width,
924
                            p.get(),
924
                            (_box_props.margin.left, _box_props.margin.right),
924
                            (_box_props.border.left, _box_props.border.right),
924
                            (_box_props.padding.left, _box_props.padding.right),
                        );
924
                        result
                    }
                    None => intrinsic.max_content_width,
                },
            }
        }
        // +spec:intrinsic-sizing:069c75 - min-content, max-content, fit-content() sizing value keywords
        // +spec:intrinsic-sizing:1ce4fa - §3.2 min-content/max-content/fit-content() sizing values
        LayoutWidth::MinContent => intrinsic.min_content_width,
        LayoutWidth::MaxContent => intrinsic.max_content_width,
        // +spec:width-calculation:7b2128 - fit-content formula and non-negative inner size flooring (css-sizing-3 §3.2)
        // +spec:width-calculation:bf694a - min-content, max-content, fit-content() sizing values
        // css-sizing-3 §3.2: fit-content(<length-percentage>) = min(max-content, max(min-content, <length-percentage>))
        LayoutWidth::FitContent(px) => {
            use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
            let arg = super::calc::resolve_pixel_value_with_viewport(
                &px, containing_block_size.width, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
                viewport_size.width, viewport_size.height,
            );
            intrinsic.max_content_width.min(intrinsic.min_content_width.max(arg))
        }
        LayoutWidth::Calc(items) => {
            use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
            let em = get_element_font_size(styled_dom, id, node_state);
            let calc_ctx = super::calc::CalcResolveContext {
                items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
            };
            super::calc::evaluate_calc(&calc_ctx, containing_block_size.width)
        }
    };
    // css-sizing-3: "the used value is floored to preserve a non-negative inner size"
54692
    let resolved_width = resolved_width.max(0.0);
    // +spec:height-calculation:7880e3 - Distinction between box types for height/margin calculation
    // +spec:height-calculation:753d8d - Height calculation for various box types (§10.6)
    // +spec:positioning:d5184e - percentage height resolved against containing block height
    // +spec:height-calculation:6a6cac - §10.5 content height resolution (auto, length, percentage)
    // +spec:height-calculation:d398e4 - §10.5/10.6 height property resolution for different box types
    // Step 2: Resolve the CSS `height` property into a concrete pixel value.
    // CSS `height` always refers to the physical vertical dimension, regardless of writing mode.
    // Percentage values resolve against the containing block's physical height.
    // In horizontal-tb: height = block size. In vertical modes: height = inline size.
    // The physical-to-logical mapping happens in Step 5 below.
    // Percentage values for `height` are resolved against the containing block's height.
    // +spec:height-calculation:0b5b0a - abs-pos replaced elements use intrinsic height for auto
54692
    let resolved_height = match css_height.unwrap_or_default() {
        LayoutHeight::Auto => {
            // +spec:width-calculation:be5eb1 - auto height means available block space is infinite (unconstrained)
            // +spec:replaced-elements:994ac6 - §10.6.2: auto height for replaced elements uses intrinsic height or (used width)/ratio
            //
            // For block-level non-replaced containers in normal flow, CSS 2.2 §10.6.3
            // says auto height is resolved from children after layout. We return 0.0
            // as a placeholder; `apply_content_based_height` (cache.rs) overwrites it
            // with the laid-out content size. Reading `intrinsic.max_content_height`
            // here is unsafe: when the intrinsic pass short-circuits (e.g. a non-STF
            // subtree whose intrinsics are never consumed), that field is zero anyway
            // — so any caller that "trusts" the pre-layout value is depending on an
            // estimate that isn't guaranteed to exist.
            //
            // Shrink-to-fit contexts (inline-block, float, abspos, table/table-cell)
            // genuinely need intrinsic for width sizing; auto-height for those is
            // still driven by content, but we keep the intrinsic fallback for
            // backwards compatibility with the existing paths.
            // CSS 2.2 §10.6.4: an absolutely/fixed-positioned non-replaced box with
            // `height:auto` and BOTH `top` and `bottom` specified has a STRETCH-FIT
            // height = cb_height − top − bottom − margins. `position_out_of_flow_
            // elements` also derives this, but it runs AFTER the subtree is laid out —
            // so resolving it HERE (a definite, computed height) lets percentage-height
            // CHILDREN resolve against the real box during their own layout instead of
            // collapsing against a 0 placeholder. (Root cause of the slippy-map
            // VirtualView blank-bounds bug: its container fills via abs inset:0.)
41140
            let abs_stretch_fit = if matches!(
41140
                position,
                LayoutPosition::Absolute | LayoutPosition::Fixed
6336
            ) && !is_replaced
            {
6336
                let off = crate::solver3::positioning::resolve_position_offsets(
6336
                    styled_dom, dom_id, *containing_block_size,
                );
6336
                match (off.top, off.bottom) {
176
                    (Some(t), Some(b)) => Some(
176
                        (containing_block_size.height
176
                            - t
176
                            - b
176
                            - _box_props.margin.top
176
                            - _box_props.margin.bottom)
176
                            .max(0.0),
176
                    ),
6160
                    _ => None,
                }
            } else {
34804
                None
            };
40964
            match abs_stretch_fit {
176
                Some(h) => h,
                // §10.6.2: auto height for a replaced element (image / VirtualView)
                // uses its intrinsic height — mirrors the auto-WIDTH replaced branch
                // above. Without this, replaced nodes (no flow content) get 0 height
                // (the blank-image / "300x0" bug).
1188
                None if is_replaced => intrinsic.max_content_height,
39776
                None => match display.unwrap_or_default() {
                    LayoutDisplay::Block
                    | LayoutDisplay::FlowRoot
                    | LayoutDisplay::ListItem
                    | LayoutDisplay::Flex
10780
                    | LayoutDisplay::Grid => 0.0,
                    // Inline: height property does not apply (§10.6.1), handled earlier
                    // via css_height override, but be explicit anyway.
15884
                    LayoutDisplay::Inline => 0.0,
                    // Shrink-to-fit and intrinsically-sized: keep using intrinsic pre-layout.
13112
                    _ => intrinsic.max_content_height,
                },
            }
        }
13552
        LayoutHeight::Px(px) => {
            // Resolve percentage or absolute pixel value
            use azul_css::props::basic::{
                pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
                SizeMetric,
            };
13552
            let pixels_opt = match px.metric {
12320
                SizeMetric::Px => Some(px.number.get()),
                SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
                SizeMetric::In => Some(px.number.get() * 96.0),
                SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
                SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
                SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
                SizeMetric::Vw => Some(px.number.get() / 100.0 * viewport_size.width),
                SizeMetric::Vh => Some(px.number.get() / 100.0 * viewport_size.height),
                SizeMetric::Vmin => Some(px.number.get() / 100.0 * viewport_size.width.min(viewport_size.height)),
                SizeMetric::Vmax => Some(px.number.get() / 100.0 * viewport_size.width.max(viewport_size.height)),
1232
                SizeMetric::Percent => None,
            };
13552
            match pixels_opt {
12320
                Some(pixels) => pixels,
                // +spec:height-calculation:37bc8c - percentage heights resolve against definite containing block height
1232
                None => match px.to_percent() {
1232
                    Some(p) => resolve_percentage_with_box_model(
1232
                        containing_block_size.height,
1232
                        p.get(),
1232
                        (_box_props.margin.top, _box_props.margin.bottom),
1232
                        (_box_props.border.top, _box_props.border.bottom),
1232
                        (_box_props.padding.top, _box_props.padding.bottom),
                    ),
                    None => intrinsic.max_content_height,
                },
            }
        }
        // equivalent to automatic size (not min_content_height which is height at min-content width)
        LayoutHeight::MinContent => intrinsic.max_content_height,
        // equivalent to automatic size
        LayoutHeight::MaxContent => intrinsic.max_content_height,
        // css-sizing-3 §3.2: fit-content(<length-percentage>) = min(max-content, max(min-content, <length-percentage>))
        // For block axis, both min-content and max-content equal auto height
        LayoutHeight::FitContent(px) => {
            use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
            let arg = super::calc::resolve_pixel_value_with_viewport(
                &px, containing_block_size.height, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
                viewport_size.width, viewport_size.height,
            );
            let auto_height = intrinsic.max_content_height;
            auto_height.min(auto_height.max(arg))
        }
        LayoutHeight::Calc(items) => {
            use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
            let em = get_element_font_size(styled_dom, id, node_state);
            let calc_ctx = super::calc::CalcResolveContext {
                items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
            };
            super::calc::evaluate_calc(&calc_ctx, containing_block_size.height)
        }
    };
    // css-sizing-3: "the used value is floored to preserve a non-negative inner size"
54692
    let resolved_height = resolved_height.max(0.0);
    // +spec:replaced-elements:5a85ce - abs-pos replaced: derive auto width from height × intrinsic ratio
    // +spec:replaced-elements:aedb26 - abs-pos replaced: both auto, ratio but no intrinsic w/h → block constraint
    // CSS Position 3 §6.2 (abs-replaced-width): For absolutely positioned replaced elements,
    // if width is auto and the element has an intrinsic ratio, width may be derived from height.
54692
    let (resolved_width, resolved_height) = if is_replaced
1408
        && width_is_auto
1188
        && matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed)
    {
        let has_intrinsic_width = intrinsic.preferred_width.map_or(false, |w| w > 0.0);
        let has_intrinsic_height = intrinsic.preferred_height.map_or(false, |h| h > 0.0);
        let intrinsic_ratio = match (intrinsic.preferred_width, intrinsic.preferred_height) {
            (Some(iw), Some(ih)) if ih > 0.0 => Some(iw / ih),
            _ => None,
        };
        if let Some(ratio) = intrinsic_ratio {
            if height_is_auto && !has_intrinsic_width && has_intrinsic_height {
                // §6.2 case: both auto, no intrinsic width, has intrinsic height + ratio
                // → width = used height × ratio
                (resolved_height * ratio, resolved_height)
            } else if !height_is_auto {
                // §6.2 case: width auto, height not auto, has intrinsic ratio
                // → width = used height × ratio
                (resolved_height * ratio, resolved_height)
            } else if height_is_auto && !has_intrinsic_width && !has_intrinsic_height {
                // §6.2 case: both auto, has ratio but no intrinsic width or height
                // → use block-level non-replaced constraint equation for width
                let block_width = (containing_block_size.width
                    - _box_props.margin.left
                    - _box_props.margin.right
                    - _box_props.border.left
                    - _box_props.border.right
                    - _box_props.padding.left
                    - _box_props.padding.right)
                    .max(0.0);
                (block_width, block_width / ratio)
            } else {
                (resolved_width, resolved_height)
            }
        } else {
            (resolved_width, resolved_height)
        }
    } else {
54692
        (resolved_width, resolved_height)
    };
    // +spec:min-max-sizing:58869e - sizing properties width/height/min-width/min-height/max-width/max-height applied here
    // +spec:min-max-sizing:2e2414 - max-width/max-height specify maximum box dimensions, applied here
    // +spec:min-max-sizing:73f51a - tentative width clamped by max-width then min-width per §10.4
    // +spec:min-max-sizing:e98c4e - preferred size clamped by min/max, box-sizing handled
    // Step 3: Apply min/max constraints (CSS 2.2 § 10.4 and § 10.7)
    // "The tentative used width is calculated (without 'min-width' and 'max-width')
    // ...If the tentative used width is greater than 'max-width', the rules above are
    // applied again using the computed value of 'max-width' as the computed value for 'width'.
    // If the resulting width is smaller than 'min-width', the rules above are applied again
    // using the value of 'min-width' as the computed value for 'width'."
    // use the constraint violation table to coordinate width+height together;
    // for non-replaced elements, apply width and height constraints independently
54692
    let has_intrinsic_ratio = intrinsic.preferred_width.is_some()
132
        && intrinsic.preferred_height.is_some()
132
        && intrinsic.preferred_width.unwrap_or(0.0) > 0.0
132
        && intrinsic.preferred_height.unwrap_or(0.0) > 0.0;
    // +spec:margin-collapsing:840eb6 - aspect ratio transfers size constraints across dimensions
54692
    let (constrained_width, constrained_height) = if has_intrinsic_ratio {
        // +spec:width-calculation:ef71c4 - replaced elements with both width/height auto use constraint violation table
        // Replaced element with intrinsic ratio: use §10.4 constraint violation table
132
        apply_constraint_violation_table(
132
            styled_dom,
132
            id,
132
            node_state,
132
            resolved_width,
132
            resolved_height,
132
            containing_block_size.width,
132
            containing_block_size.height,
132
            _box_props,
        )
    } else {
        // Non-replaced element: apply width and height constraints independently
54560
        let cw = apply_width_constraints(
54560
            styled_dom,
54560
            id,
54560
            node_state,
54560
            resolved_width,
54560
            containing_block_size.width,
54560
            _box_props,
        );
54560
        let ch = apply_height_constraints(
54560
            styled_dom,
54560
            id,
54560
            node_state,
54560
            resolved_height,
54560
            containing_block_size.height,
54560
            _box_props,
        );
54560
        (cw, ch)
    };
    // +spec:box-model:cc170b - box-sizing: border-box includes padding+border in specified size; content-box adds them outside; content size floored at zero
    // +spec:box-model:d9d797 - box-sizing: content-box vs border-box dimension interpretation
    // +spec:box-model:e2a773 - box-sizing: border-box includes padding+border in width/height; content-box adds them outside
    // +spec:box-sizing:8159a8 - box-sizing property indicates whether content-box or border-box is measured
    // +spec:box-sizing:b0ff05 - border-box sets border-box to specified size, content-box calculated from it
    // +spec:box-sizing:aefeb2 - box-sizing: content-box vs border-box width/height interpretation
    // +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
    // Step 4: Convert to border-box dimensions, respecting box-sizing property
    // CSS box-sizing:
    // - content-box (default): width/height set content size, border+padding are added
    // - border-box: width/height set border-box size, border+padding are included
54692
    let box_sizing = match get_css_box_sizing(styled_dom, id, node_state) {
54692
        MultiValue::Exact(bs) => bs,
        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
            azul_css::props::layout::LayoutBoxSizing::ContentBox
        }
    };
54692
    let (border_box_width, border_box_height) = match box_sizing {
        azul_css::props::layout::LayoutBoxSizing::BorderBox => {
            // +spec:box-sizing:cdfe09 - box-sizing: border-box makes width/height set the border box
            // +spec:box-sizing:3ba6d3 - content-box floors at 0px, so border-box can't be less than padding+border
748
            let min_border_box_w = _box_props.padding.left
748
                + _box_props.padding.right
748
                + _box_props.border.left
748
                + _box_props.border.right;
748
            let min_border_box_h = _box_props.padding.top
748
                + _box_props.padding.bottom
748
                + _box_props.border.top
748
                + _box_props.border.bottom;
            // +spec:box-model:4f423b - used values refer to the border box when box-sizing: border-box
            // border-box: The width/height values already include border and padding
            // CSS Box Sizing Level 3: "the specified width and height (and respective min/max
            // properties) on this element determine the border box of the element"
            // However, non-quantitative values (auto, min-content, max-content) are not
            // influenced by box-sizing, so they still need border+padding added.
            // Floor: content-box cannot go negative, so border-box >= padding+border
748
            let bw = if width_is_quantitative {
748
                constrained_width.max(min_border_box_w)
            } else {
                constrained_width
                    + _box_props.padding.left
                    + _box_props.padding.right
                    + _box_props.border.left
                    + _box_props.border.right
            };
748
            let bh = if height_is_quantitative {
396
                constrained_height.max(min_border_box_h)
            } else {
352
                constrained_height
352
                    + _box_props.padding.top
352
                    + _box_props.padding.bottom
352
                    + _box_props.border.top
352
                    + _box_props.border.bottom
            };
748
            (bw, bh)
        }
        azul_css::props::layout::LayoutBoxSizing::ContentBox => {
            // +spec:box-sizing:fead70 - content-box: width/height set content size, border+padding added outside
53944
            let border_box_width = constrained_width
53944
                + _box_props.padding.left
53944
                + _box_props.padding.right
53944
                + _box_props.border.left
53944
                + _box_props.border.right;
53944
            let border_box_height = constrained_height
53944
                + _box_props.padding.top
53944
                + _box_props.padding.bottom
53944
                + _box_props.border.top
53944
                + _box_props.border.bottom;
53944
            (border_box_width, border_box_height)
        }
    };
    // +spec:block-formatting-context:c6fb58 - vertical writing modes swap layout dimensions
    // +spec:min-max-sizing:d97870 - width/height/min/max refer to physical dimensions; layout rules are logical
    // Step 5: Map the resolved physical dimensions to logical dimensions.
    //
    // CSS Writing Modes Level 4:
    // - In horizontal-tb: width = inline (cross) size, height = block (main) size.
    // - In vertical-rl/lr: width = block (main) size, height = inline (cross) size.
    //
    // `from_main_cross` handles this mapping: given (main, cross) and writing mode,
    // it produces the correct LogicalSize with physical (width, height).
54692
    let (main_size, cross_size) = if is_vertical {
        // Vertical writing mode: width is the block (main) dimension,
        // height is the inline (cross) dimension.
        (border_box_width, border_box_height)
    } else {
        // Horizontal writing mode (default): width is cross, height is main.
54692
        (border_box_height, border_box_width)
    };
    // Step 6: Construct the final LogicalSize from the logical dimensions.
    // +spec:min-max-sizing:2f66a6 - direction-dependent layout rules abstracted to logical start/end via writing mode
54692
    let result =
54692
        LogicalSize::from_main_cross(main_size, cross_size, writing_mode.unwrap_or_default());
54692
    Ok(result)
54956
}
// +spec:min-max-sizing:b02ebc - sizing properties min-width/max-width/min-height/max-height and preferred aspect ratio
// +spec:replaced-elements:740f3e - constraint violation table for replaced elements with intrinsic ratio and both width/height auto
// +spec:min-max-sizing:939f2c - use min-width/min-height <length> with aspect ratio for replaced elements
// with intrinsic ratios. Implements all 10 cases from the spec table, coordinating
// +spec:min-max-sizing:07620d - CSS 2.2 §10.4 constraint violation table for replaced elements with intrinsic ratios
// Implements all 11 cases from the spec table, coordinating
// width and height together to preserve the aspect ratio while respecting min/max constraints.
132
fn apply_constraint_violation_table(
132
    styled_dom: &StyledDom,
132
    id: NodeId,
132
    node_state: &StyledNodeState,
132
    w: f32,  // tentative width (ignoring min/max)
132
    h: f32,  // tentative height (ignoring min/max)
132
    containing_block_width: f32,
132
    containing_block_height: f32,
132
    box_props: &BoxProps,
132
) -> (f32, f32) {
    use azul_css::props::basic::{
        pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
        SizeMetric,
    };
    use crate::solver3::getters::{
        get_css_min_width, get_css_max_width, get_css_min_height, get_css_max_height, MultiValue,
    };
    // Helper to resolve a pixel value to f32
    fn resolve_px(px: &azul_css::props::basic::pixel::PixelValue, containing: f32, box_props: &BoxProps, is_horizontal: bool) -> Option<f32> {
        let pixels_opt = match px.metric {
            SizeMetric::Px => Some(px.number.get()),
            SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
            SizeMetric::In => Some(px.number.get() * 96.0),
            SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
            SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
            SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
            SizeMetric::Percent => None,
            _ => None,
        };
        match pixels_opt {
            Some(v) => Some(v),
            None => {
                px.to_percent().map(|p| {
                    let (m1, m2, b1, b2, p1, p2) = if is_horizontal {
                        (box_props.margin.left, box_props.margin.right,
                         box_props.border.left, box_props.border.right,
                         box_props.padding.left, box_props.padding.right)
                    } else {
                        (box_props.margin.top, box_props.margin.bottom,
                         box_props.border.top, box_props.border.bottom,
                         box_props.padding.top, box_props.padding.bottom)
                    };
                    resolve_percentage_with_box_model(containing, p.get(), (m1, m2), (b1, b2), (p1, p2))
                })
            }
        }
    }
    // +spec:min-max-sizing:92ab8d - constraint violation table for replaced elements with intrinsic ratio (cyclic percentage contributions use auto fallback)
    // +spec:min-max-sizing:ad8605 - min-height/max-height interact with percentage heights; percentages behave as auto in intrinsic contribution calc
    // +spec:positioning:c0af55 - automatic minimum size of abspos box is always zero (default 0.0)
    // Resolve min-width (default 0)
132
    let min_w = match get_css_min_width(styled_dom, id, node_state) {
        MultiValue::Exact(mw) => resolve_px(&mw.inner, containing_block_width, box_props, true).unwrap_or(0.0),
132
        _ => 0.0,
    };
    // Resolve max-width (default infinity)
132
    let max_w = match get_css_max_width(styled_dom, id, node_state) {
        MultiValue::Exact(mw) => {
            if mw.inner.number.get() >= core::f32::MAX - 1.0 {
                f32::MAX
            } else {
                resolve_px(&mw.inner, containing_block_width, box_props, true).unwrap_or(f32::MAX)
            }
        }
132
        _ => f32::MAX,
    };
    // Resolve min-height (default 0)
132
    let min_h = match get_css_min_height(styled_dom, id, node_state) {
        MultiValue::Exact(mh) => resolve_px(&mh.inner, containing_block_height, box_props, false).unwrap_or(0.0),
132
        _ => 0.0,
    };
    // Resolve max-height (default infinity)
132
    let max_h = match get_css_max_height(styled_dom, id, node_state) {
        MultiValue::Exact(mh) => {
            if mh.inner.number.get() >= core::f32::MAX - 1.0 {
                f32::MAX
            } else {
                resolve_px(&mh.inner, containing_block_height, box_props, false).unwrap_or(f32::MAX)
            }
        }
132
        _ => f32::MAX,
    };
    // max(min, max) so that min ≤ max holds true."
132
    let max_w = max_w.max(min_w);
132
    let max_h = max_h.max(min_h);
    // Guard against zero dimensions (avoid division by zero)
132
    if w <= 0.0 || h <= 0.0 {
        return (w.max(min_w).min(max_w), h.max(min_h).min(max_h));
132
    }
132
    let w_over = w > max_w;
132
    let w_under = w < min_w;
132
    let h_over = h > max_h;
132
    let h_under = h < min_h;
    // +spec:min-max-sizing:713560 - constraint violation table for replaced elements with intrinsic ratio
132
    match (w_over, w_under, h_over, h_under) {
        // Row 1: no constraint violation
132
        (false, false, false, false) => (w, h),
        // Row 2: w > max-width only
        (true, false, false, false) => {
            (max_w, (max_w * h / w).max(min_h))
        }
        // Row 3: w < min-width only
        (false, true, false, false) => {
            (min_w, (min_w * h / w).min(max_h))
        }
        // Row 4: h > max-height only
        (false, false, true, false) => {
            ((max_h * w / h).max(min_w), max_h)
        }
        // Row 5: h < min-height only
        (false, false, false, true) => {
            ((min_h * w / h).min(max_w), min_h)
        }
        // Row 6+7: (w > max-width) and (h > max-height)
        (true, false, true, false) => {
            if max_w / w <= max_h / h {
                (max_w, (max_w * h / w).max(min_h))
            } else {
                ((max_h * w / h).max(min_w), max_h)
            }
        }
        // Row 8+9: (w < min-width) and (h < min-height)
        (false, true, false, true) => {
            if min_w / w <= min_h / h {
                ((min_h * w / h).min(max_w), min_h)
            } else {
                (min_w, (min_w * h / w).min(max_h))
            }
        }
        // Row 10: (w < min-width) and (h > max-height)
        (false, true, true, false) => (min_w, max_h),
        // Row 11: (w > max-width) and (h < min-height)
        (true, false, false, true) => (max_w, min_h),
        // Fallback (impossible combinations like w_over && w_under)
        _ => (w.max(min_w).min(max_w), h.max(min_h).min(max_h)),
    }
132
}
// +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
// +spec:min-max-sizing:12667d - width/height/min-width/min-height/max-width/max-height properties from CSS Sizing 3
/// +spec:min-max-sizing:205e9e - intrinsic size constraints (min/max-content contributions, min/max sizing properties)
// +spec:min-max-sizing:cac146 - min-width/min-height specify minimum box dimensions; max overridden by min
// +spec:width-calculation:e77d58 - min/max-width clamping algorithm per CSS 2.2 § 10.4
// +spec:width-calculation:1d63f0 - min-width/max-width property resolution and value meanings
/// Apply min-width and max-width constraints to tentative width
/// Per CSS 2.2 § 10.4: min-width overrides max-width if min > max
54560
fn apply_width_constraints(
54560
    styled_dom: &StyledDom,
54560
    id: NodeId,
54560
    node_state: &StyledNodeState,
54560
    tentative_width: f32,
54560
    containing_block_width: f32,
54560
    box_props: &BoxProps,
54560
) -> f32 {
    use azul_css::props::basic::{
        pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
        SizeMetric,
    };
    use crate::solver3::getters::{get_css_max_width, get_css_min_width, MultiValue};
    // +spec:display-property:0c55e5 - auto min-width resolves to 0 for CSS2 display types
    // Resolve min-width (default is 0)
54560
    let min_width = match get_css_min_width(styled_dom, id, node_state) {
        MultiValue::Exact(mw) => {
            let px = &mw.inner;
            let pixels_opt = match px.metric {
                SizeMetric::Px => Some(px.number.get()),
                SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
                SizeMetric::In => Some(px.number.get() * 96.0),
                SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
                SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
                SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
                SizeMetric::Percent => None,
                _ => None,
            };
            match pixels_opt {
                Some(pixels) => pixels,
                None => px
                    .to_percent()
                    .map(|p| {
                        resolve_percentage_with_box_model(
                            containing_block_width,
                            p.get(),
                            (box_props.margin.left, box_props.margin.right),
                            (box_props.border.left, box_props.border.right),
                            (box_props.padding.left, box_props.padding.right),
                        )
                    })
                    .unwrap_or(0.0),
            }
        }
54560
        _ => 0.0,
    };
    // Resolve max-width (default is infinity/none)
54560
    let max_width = match get_css_max_width(styled_dom, id, node_state) {
44
        MultiValue::Exact(mw) => {
44
            let px = &mw.inner;
            // Check if it's the default "max" value (f32::MAX)
44
            if px.number.get() >= core::f32::MAX - 1.0 {
                None
            } else {
44
                let pixels_opt = match px.metric {
44
                    SizeMetric::Px => Some(px.number.get()),
                    SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
                    SizeMetric::In => Some(px.number.get() * 96.0),
                    SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
                    SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
                    SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
                    SizeMetric::Percent => None,
                    _ => None,
                };
44
                match pixels_opt {
44
                    Some(pixels) => Some(pixels),
                    None => px.to_percent().map(|p| {
                        resolve_percentage_with_box_model(
                            containing_block_width,
                            p.get(),
                            (box_props.margin.left, box_props.margin.right),
                            (box_props.border.left, box_props.border.right),
                            (box_props.padding.left, box_props.padding.right),
                        )
                    }),
                }
            }
        }
54516
        _ => None,
    };
    // Apply constraints: max(min_width, min(tentative, max_width))
    // If min > max, min wins per CSS spec
54560
    let mut result = tentative_width;
54560
    if let Some(max) = max_width {
44
        result = result.min(max);
54516
    }
54560
    result = result.max(min_width);
54560
    result
54560
}
/// Apply min-height and max-height constraints to tentative height
/// Per CSS 2.2 § 10.7: min-height overrides max-height if min > max
// +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)
// +spec:height-calculation:982aaf - min-height/max-height constrain box heights to a range
// +spec:height-calculation:c6c33a - min-height and max-height property resolution and application
54560
fn apply_height_constraints(
54560
    styled_dom: &StyledDom,
54560
    id: NodeId,
54560
    node_state: &StyledNodeState,
54560
    tentative_height: f32,
54560
    containing_block_height: f32,
54560
    box_props: &BoxProps,
54560
) -> f32 {
    use azul_css::props::basic::{
        pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
        SizeMetric,
    };
    use crate::solver3::getters::{get_css_max_height, get_css_min_height, MultiValue};
    // for backwards-compat with CSS2 display types (block, inline, inline-block, table)
    // Resolve min-height (default is 0)
54560
    let min_height = match get_css_min_height(styled_dom, id, node_state) {
352
        MultiValue::Exact(mh) => {
352
            let px = &mh.inner;
352
            let pixels_opt = match px.metric {
352
                SizeMetric::Px => Some(px.number.get()),
                SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
                SizeMetric::In => Some(px.number.get() * 96.0),
                SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
                SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
                SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
                SizeMetric::Percent => None,
                _ => None,
            };
352
            match pixels_opt {
352
                Some(pixels) => pixels,
                None => px
                    .to_percent()
                    .map(|p| {
                        resolve_percentage_with_box_model(
                            containing_block_height,
                            p.get(),
                            (box_props.margin.top, box_props.margin.bottom),
                            (box_props.border.top, box_props.border.bottom),
                            (box_props.padding.top, box_props.padding.bottom),
                        )
                    })
                    .unwrap_or(0.0),
            }
        }
54208
        _ => 0.0,
    };
    // Resolve max-height (default is infinity/none)
54560
    let max_height = match get_css_max_height(styled_dom, id, node_state) {
        MultiValue::Exact(mh) => {
            let px = &mh.inner;
            // Check if it's the default "max" value (f32::MAX)
            if px.number.get() >= core::f32::MAX - 1.0 {
                None
            } else {
                let pixels_opt = match px.metric {
                    SizeMetric::Px => Some(px.number.get()),
                    SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
                    SizeMetric::In => Some(px.number.get() * 96.0),
                    SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
                    SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
                    SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
                    SizeMetric::Percent => None,
                    _ => None,
                };
                match pixels_opt {
                    Some(pixels) => Some(pixels),
                    None => px.to_percent().map(|p| {
                        resolve_percentage_with_box_model(
                            containing_block_height,
                            p.get(),
                            (box_props.margin.top, box_props.margin.bottom),
                            (box_props.border.top, box_props.border.bottom),
                            (box_props.padding.top, box_props.padding.bottom),
                        )
                    }),
                }
            }
        }
54560
        _ => None,
    };
    // +spec:height-calculation:297001 - min/max height constraint algorithm per CSS 2.2 §10.7
    // Apply constraints: max(min_height, min(tentative, max_height))
    // If min > max, min wins per CSS spec
54560
    let mut result = tentative_height;
54560
    if let Some(max) = max_height {
        result = result.min(max);
54560
    }
54560
    result = result.max(min_height);
54560
    result
54560
}
38764
pub fn extract_text_from_node(styled_dom: &StyledDom, node_id: NodeId) -> Option<String> {
38764
    match &styled_dom.node_data.as_container()[node_id].get_node_type() {
20504
        NodeType::Text(text_data) => {
20504
            Some(text_data.as_str().to_string())
        }
18260
        _ => None,
    }
38764
}