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
2240
pub fn resolve_percentage_with_box_model(
62
2240
    containing_block_dimension: f32,
63
2240
    percentage: f32,
64
2240
    _margins: (f32, f32),
65
2240
    _borders: (f32, f32),
66
2240
    _paddings: (f32, f32),
67
2240
) -> 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
2240
    (containing_block_dimension * percentage).max(0.0)
72
2240
}
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
14525
fn subtree_contains_text(styled_dom: &StyledDom, dom_id: NodeId) -> bool {
80
14525
    let node_hierarchy = styled_dom.node_hierarchy.as_container();
81
14525
    let node_data = styled_dom.node_data.as_container();
82
14525
    if matches!(node_data[dom_id].get_node_type(), NodeType::Text(_)) {
83
8260
        return true;
84
6265
    }
85
6265
    dom_id
86
6265
        .az_children(&node_hierarchy)
87
6265
        .any(|child| subtree_contains_text(styled_dom, child))
88
14525
}
89

            
90
/// Phase 2a: Calculate intrinsic sizes (bottom-up pass)
91
/// // +spec:display-contents:f12d4e - intrinsic sizing: size determined by contents, not context
92
2785
pub fn calculate_intrinsic_sizes<T: ParsedFontTrait>(
93
2785
    ctx: &mut LayoutContext<'_, T>,
94
2785
    tree: &mut LayoutTree,
95
2785
    text_cache: &mut LayoutCache,
96
2785
    dirty_nodes: &BTreeSet<usize>,
97
2785
) -> Result<()> {
98
2785
    if dirty_nodes.is_empty() {
99
        return Ok(());
100
2785
    }
101

            
102
2785
    ctx.debug_log("Starting intrinsic size calculation");
103
    // Pre-compute the "ancestor closure" of dirty_nodes: every dirty
104
    // node AND each of its ancestors up to root. A node not in this
105
    // set (and whose `intrinsic_sizes` is already populated) can
106
    // reuse its cached intrinsic — we skip its entire subtree walk.
107
    // Before this, `calculate_intrinsic_recursive` walked the full
108
    // tree from root regardless, costing ~2 ms per warm render on
109
    // excel.html even when only 3 nodes were actually dirty.
110
2785
    let dirty_closure = compute_dirty_ancestor_closure(tree, dirty_nodes);
111

            
112
2785
    let mut calculator = IntrinsicSizeCalculator::new(ctx, text_cache);
113
2785
    calculator.dirty_closure = Some(dirty_closure);
114
    // Fix C (re-enabled §58 Win #3): skip intrinsic computation for subtrees
115
    // whose values will never be consumed. `tree.subtree_needs_intrinsic` is a
116
    // static-DOM bitmap precomputed at tree-build time — true if this node or
117
    // any descendant establishes a shrink-to-fit context. When both the
118
    // caller and the subtree are non-STF, no one reads the intrinsic, so the
119
    // whole descent is pure waste.
120
    //
121
    // The previous attempt (7667d13e, reverted in bd9ad36d) wrote default
122
    // (zero) intrinsics and broke auto-height rendering because
123
    // calculate_used_size_for_node read intrinsic.max_content_height as the
124
    // height:auto fallback. 97c3d3db refactored that dependency away: for
125
    // block-level auto-height, used_size.height is 0 pre-layout and
126
    // apply_content_based_height fills it from the laid-out content size.
127
    // With that gone, skipping intrinsic is safe.
128
2785
    calculator.calculate_intrinsic_recursive(tree, tree.root, false)?;
129
2785
    ctx.debug_log("Finished intrinsic size calculation");
130
2785
    Ok(())
131
2785
}
132

            
133
4655
fn compute_dirty_ancestor_closure(
134
4655
    tree: &LayoutTree,
135
4655
    dirty_nodes: &BTreeSet<usize>,
136
4655
) -> std::collections::HashSet<usize> {
137
4655
    let mut closure: std::collections::HashSet<usize> = std::collections::HashSet::new();
138
30065
    for &dirty in dirty_nodes {
139
25410
        let mut cur = Some(dirty);
140
50855
        while let Some(idx) = cur {
141
46200
            if !closure.insert(idx) {
142
20755
                break;
143
25445
            }
144
25445
            cur = tree.get(idx).and_then(|n| n.parent);
145
        }
146
    }
147
4655
    closure
148
4655
}
149

            
150
struct IntrinsicSizeCalculator<'a, 'b, 'c, T: ParsedFontTrait> {
151
    ctx: &'a mut LayoutContext<'b, T>,
152
    /// Shared text shaping cache, threaded through from the caller so
153
    /// stages 1–3 of the inline layout pipeline (logical / BiDi / shaping)
154
    /// are cache-hits across the sizing pass's min/max-content measurements
155
    /// AND the subsequent real layout pass. Previously each pass held its
156
    /// own `LayoutCache`, so identical text was shaped three times per
157
    /// root_layout_pass — once per min-content measurement, once per
158
    /// max-content measurement, once at final layout.
159
    text_cache: &'c mut LayoutCache,
160
    /// If `Some`, only nodes in this set (the ancestor-closure of
161
    /// dirty nodes) need recomputation. A clean node whose
162
    /// `warm.intrinsic_sizes` is already populated reuses the
163
    /// cached value and skips its entire subtree descent.
164
    dirty_closure: Option<std::collections::HashSet<usize>>,
165
}
166

            
167
impl<'a, 'b, 'c, T: ParsedFontTrait> IntrinsicSizeCalculator<'a, 'b, 'c, T> {
168
2785
    fn new(ctx: &'a mut LayoutContext<'b, T>, text_cache: &'c mut LayoutCache) -> Self {
169
2785
        Self {
170
2785
            ctx,
171
2785
            text_cache,
172
2785
            dirty_closure: None,
173
2785
        }
174
2785
    }
175

            
176
15995
    fn calculate_intrinsic_recursive(
177
15995
        &mut self,
178
15995
        tree: &mut LayoutTree,
179
15995
        node_index: usize,
180
15995
        ancestor_is_stf: bool,
181
15995
    ) -> Result<IntrinsicSizes> {
182
        // Fast path: if this subtree has no dirty nodes AND we
183
        // already have a cached intrinsic, return the cached value
184
        // and skip the whole descent. Caller is the ancestor-closure
185
        // computation in `calculate_intrinsic_sizes` — anything not
186
        // in that set is guaranteed clean through every descendant.
187
15995
        if let Some(closure) = self.dirty_closure.as_ref() {
188
15995
            if !closure.contains(&node_index) {
189
                if let Some(cached) = tree
190
                    .warm(node_index)
191
                    .and_then(|w| w.intrinsic_sizes.clone())
192
                {
193
                    return Ok(cached);
194
                }
195
15995
            }
196
        }
197

            
198
        // Fix C static-DOM short-circuit: if no ancestor needs this intrinsic
199
        // (none are STF) AND no descendant in this subtree is STF, nobody
200
        // will ever read the value. Write a default and skip the recursion.
201
        // `subtree_needs_intrinsic` is precomputed at tree-build time from
202
        // the DOM's display/position/float properties, so this is a constant
203
        // lookup with no per-pass work.
204
15995
        if !ancestor_is_stf
205
5031
            && tree
206
5031
                .subtree_needs_intrinsic
207
5031
                .get(node_index)
208
5031
                .copied()
209
5031
                .map(|v| !v)
210
5031
                .unwrap_or(false)
211
        {
212
47
            let default = IntrinsicSizes::default();
213
47
            if let Some(n) = tree.warm_mut(node_index) {
214
47
                n.intrinsic_sizes = Some(default);
215
47
            }
216
47
            return Ok(default);
217
15948
        }
218

            
219
        // Previously cloned the full LayoutNode to sidestep borrow conflicts
220
        // with the `&mut tree` recursive calls below, but we only need the
221
        // DOM id here — a `Copy` scalar. The clone was allocating a
222
        // Vec<usize> for children and a TaffyCache on every recursion
223
        // (~300x on excel.html).
224
15948
        let dom_node_id = tree
225
15948
            .get(node_index)
226
15948
            .ok_or(LayoutError::InvalidTree)?
227
            .dom_node_id;
228

            
229
        // Out-of-flow elements do not contribute to their parent's intrinsic size.
230
15948
        let position = get_position_type(self.ctx.styled_dom, dom_node_id);
231
15948
        if position == LayoutPosition::Absolute || position == LayoutPosition::Fixed {
232
35
            if let Some(n) = tree.warm_mut(node_index) {
233
35
                n.intrinsic_sizes = Some(IntrinsicSizes::default());
234
35
            }
235
35
            return Ok(IntrinsicSizes::default());
236
15913
        }
237

            
238
        // Copy child indices before recursive calls (which need &mut tree).
239
        // Stack buffer for the common case (≤32 children); heap only for huge nodes.
240
15913
        let children_slice = tree.children(node_index);
241
15913
        let n = children_slice.len();
242
15913
        let mut stack_buf = [0usize; 32];
243
        let heap_buf: Vec<usize>;
244
15913
        let children: &[usize] = if n <= 32 {
245
15913
            stack_buf[..n].copy_from_slice(children_slice);
246
15913
            &stack_buf[..n]
247
        } else {
248
            heap_buf = children_slice.to_vec();
249
            &heap_buf
250
        };
251
        // Propagate STF flag: children inherit `ancestor_is_stf=true` if any
252
        // ancestor up to and including self is STF.
253
15913
        let self_is_stf = tree
254
15913
            .get(node_index)
255
15913
            .map(|n| {
256
15913
                crate::solver3::layout_tree::is_shrink_to_fit_context(
257
15913
                    self.ctx.styled_dom,
258
15913
                    n.dom_node_id,
259
15913
                    &n.formatting_context,
260
                )
261
15913
            })
262
15913
            .unwrap_or(false);
263
15913
        let child_ancestor_is_stf = ancestor_is_stf || self_is_stf;
264

            
265
15913
        let mut child_intrinsics = Vec::with_capacity(n);
266
29123
        for &child_index in children {
267
13210
            let child_intrinsic =
268
13210
                self.calculate_intrinsic_recursive(tree, child_index, child_ancestor_is_stf)?;
269
13210
            child_intrinsics.push((child_index, child_intrinsic));
270
        }
271

            
272
        // Then calculate this node's intrinsic size based on its children
273
15913
        let mut intrinsic = self.calculate_node_intrinsic_sizes(tree, node_index, &child_intrinsics)?;
274

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

            
280
15878
            let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
281

            
282
15878
            if let MultiValue::Exact(mw) = get_css_min_width(self.ctx.styled_dom, dom_id, node_state) {
283
                let px = &mw.inner;
284
                let resolved = match px.metric {
285
                    SizeMetric::Px => Some(px.number.get()),
286
                    SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
287
                    SizeMetric::In => Some(px.number.get() * 96.0),
288
                    SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
289
                    SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
290
                    SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
291
                    _ => None, // percentages are not <length>
292
                };
293
                if let Some(min_w) = resolved {
294
                    intrinsic.min_content_width = intrinsic.min_content_width.max(min_w);
295
                    intrinsic.max_content_width = intrinsic.max_content_width.max(min_w);
296
                }
297
15878
            }
298

            
299
15878
            if let MultiValue::Exact(mh) = get_css_min_height(self.ctx.styled_dom, dom_id, node_state) {
300
280
                let px = &mh.inner;
301
280
                let resolved = match px.metric {
302
280
                    SizeMetric::Px => Some(px.number.get()),
303
                    SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
304
                    SizeMetric::In => Some(px.number.get() * 96.0),
305
                    SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
306
                    SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
307
                    SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
308
                    _ => None,
309
                };
310
280
                if let Some(min_h) = resolved {
311
280
                    intrinsic.min_content_height = intrinsic.min_content_height.max(min_h);
312
280
                    intrinsic.max_content_height = intrinsic.max_content_height.max(min_h);
313
280
                }
314
15598
            }
315
35
        }
316

            
317
15913
        if let Some(n) = tree.warm_mut(node_index) {
318
15913
            n.intrinsic_sizes = Some(intrinsic);
319
15913
        }
320

            
321
15913
        Ok(intrinsic)
322
15995
    }
323

            
324
15913
    fn calculate_node_intrinsic_sizes(
325
15913
        &mut self,
326
15913
        tree: &LayoutTree,
327
15913
        node_index: usize,
328
15913
        child_intrinsics: &[(usize, IntrinsicSizes)],
329
15913
    ) -> Result<IntrinsicSizes> {
330
15913
        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
331

            
332
        // +spec:block-formatting-context:30def2 - replaced elements use physical 300x150 default, not re-oriented by writing-mode
333
        // +spec:display-property:015c41 - replaced elements default to 300x150 intrinsic size per css-sizing-3 §5.1
334
        // +spec:display-property:2c6af3 - replaced elements with auto width/height use max-content size
335
        // +spec:replaced-elements:6d6030 - Intrinsic sizes for replaced elements (images, virtual views)
336
        // VirtualViews are replaced elements with a default intrinsic size of 300x150px
337
        // (same as virtualized view elements)
338
15913
        if let Some(dom_id) = node.dom_node_id {
339
15878
            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
340
15878
            if node_data.is_virtual_view_node() {
341
                return Ok(IntrinsicSizes {
342
                    min_content_width: 300.0,
343
                    max_content_width: 300.0,
344
                    preferred_width: None, // Will be determined by CSS or flex-grow
345
                    min_content_height: 150.0,
346
                    max_content_height: 150.0,
347
                    preferred_height: None, // Will be determined by CSS or flex-grow
348
                });
349
15878
            }
350
            
351
            // +spec:containing-block:bb5a12 - replaced element intrinsic sizes using initial containing block
352
            // +spec:display-property:7127f9 - intrinsic sizes of replaced elements without natural sizes (300x150 fallback, aspect ratio)
353
            // +spec:display-property:f9cede - replaced elements derive intrinsic size from natural dimensions
354
            // +spec:writing-modes:b18121 - stretch fit inline size from available space, calculate block size via aspect ratio
355
15878
            if let NodeType::Image(image_ref) = node_data.get_node_type() {
356
                let size = image_ref.get_size();
357
                // +spec:containing-block:1da6dc - use initial CB inline size for replaced elements with aspect ratio but no intrinsic size
358
                // Per css-sizing-3 §5.1: "use an inline size matching the corresponding dimension
359
                // of the initial containing block and calculate the other dimension using the aspect ratio"
360
                let (width, height) = if size.width > 0.0 && size.height > 0.0 {
361
                    (size.width, size.height)
362
                } else if size.width > 0.0 {
363
                    (size.width, size.width / 2.0)
364
                } else if size.height > 0.0 {
365
                    // Has intrinsic height but no width — use initial CB inline dimension
366
                    (self.ctx.viewport_size.width, size.height)
367
                } else {
368
                    // +spec:replaced-elements:43376b - 300px fallback with 2:1 ratio for replaced elements
369
                    // No intrinsic dimensions — cap at 300x150 per CSS 2.2 §10.3.2
370
                    // +spec:width-calculation:3b0efe - auto width fallback: 300px capped to device width
371
                    // +spec:width-calculation:16c305 - auto height fallback: 2:1 ratio, max 150px
372
                    let w = self.ctx.viewport_size.width.min(300.0);
373
                    (w, w / 2.0)
374
                };
375
                return Ok(IntrinsicSizes {
376
                    min_content_width: width,
377
                    max_content_width: width,
378
                    preferred_width: Some(width),
379
                    min_content_height: height,
380
                    max_content_height: height,
381
                    preferred_height: Some(height),
382
                });
383
15878
            }
384
35
        }
385

            
386
15913
        match node.formatting_context {
387
            FormattingContext::Block { .. } => {
388
                // Check if this block establishes an Inline Formatting Context (IFC).
389
                // Per CSS 2.2 §9.2.1.1: A block container with mixed block-level and
390
                // inline-level children creates anonymous block boxes to wrap the inline
391
                // content. So we only treat as IFC root if there are NO block-level children.
392
                //
393
                // We check the actual CSS display property, NOT formatting_context,
394
                // because a display:block element with only inline children gets
395
                // FormattingContext::Inline (meaning "establishes IFC for its children"),
396
                // which is different from being an inline element itself.
397
2287
                let has_block_child = tree.children(node_index).iter().any(|&child_idx| {
398
1107
                    tree.get(child_idx)
399
1107
                        .and_then(|c| c.dom_node_id)
400
1107
                        .map(|dom_id| {
401
1072
                            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
402
                            // Text nodes are inline-level
403
1072
                            if matches!(node_data.get_node_type(), NodeType::Text(_)) {
404
                                return false;
405
1072
                            }
406
1072
                            let display = get_display_type(self.ctx.styled_dom, dom_id);
407
1072
                            display.creates_block_context()
408
1072
                        })
409
1107
                        .unwrap_or(false)
410
1107
                });
411

            
412
2287
                let has_inline_child = tree.children(node_index).iter().any(|&child_idx| {
413
1407
                    tree.get(child_idx)
414
1407
                        .and_then(|c| c.dom_node_id)
415
1407
                        .map(|dom_id| {
416
1372
                            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
417
1372
                            if matches!(node_data.get_node_type(), NodeType::Text(_)) {
418
                                return true;
419
1372
                            }
420
1372
                            let display = get_display_type(self.ctx.styled_dom, dom_id);
421
1372
                            matches!(display,
422
                                LayoutDisplay::Inline
423
                                | LayoutDisplay::InlineBlock
424
                                | LayoutDisplay::InlineFlex
425
                                | LayoutDisplay::InlineGrid
426
                                | LayoutDisplay::InlineTable
427
                            )
428
1372
                        })
429
1407
                        .unwrap_or(false)
430
1407
                });
431

            
432
                // IFC root only if there are inline children and NO block children.
433
                // If there are block children, text nodes get anonymous block wrappers.
434
2287
                let is_ifc_root = has_inline_child && !has_block_child;
435
                
436
                // Also check if this block has direct text content (text nodes in DOM)
437
                // but ONLY if there are no block-level layout children
438
2287
                let has_direct_text = if !has_block_child {
439
1215
                    if let Some(dom_id) = node.dom_node_id {
440
1215
                        let node_hierarchy = &self.ctx.styled_dom.node_hierarchy.as_container();
441
1215
                        dom_id.az_children(node_hierarchy).any(|child_id| {
442
                            let child_node_data = &self.ctx.styled_dom.node_data.as_container()[child_id];
443
                            matches!(child_node_data.get_node_type(), NodeType::Text(_))
444
                        })
445
                    } else {
446
                        false
447
                    }
448
                } else {
449
1072
                    false
450
                };
451
                
452
2287
                if is_ifc_root || has_direct_text {
453
                    // This block is an IFC root - measure all inline content ONCE
454
                    self.calculate_ifc_root_intrinsic_sizes(tree, node_index)
455
                } else {
456
                    // This is a BFC root (only block children) - aggregate child sizes
457
2287
                    self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics)
458
                }
459
            }
460
            FormattingContext::Inline => {
461
                // There are THREE cases for FormattingContext::Inline:
462
                // 1. A Text node (NodeType::Text) - this IS the text content itself
463
                //    -> Needs to measure itself as an atomic inline unit
464
                // 2. An IFC root - a block with only inline children (has text child nodes)
465
                //    -> Should measure its inline content
466
                // 3. A true inline element (display: inline, e.g., <span>) with no text
467
                //    -> Returns default(0,0), measured by parent IFC root
468
                //
469
                // We distinguish by:
470
                // - Checking if THIS node is a Text node (case 1)
471
                // - Checking if this subtree contains any text (case 2)
472
                //
473
                // Why descendants, not just direct children: for `<span><a>text</a></span>`,
474
                // the `<span>` is a layout-tree IFC root (layout_ifc is called on it), but
475
                // its direct DOM children are inline elements, not text. Restricting the
476
                // check to direct text children would zero out the span's intrinsic width
477
                // even though the cell content width depends on it.
478
8023
                let is_text_node = if let Some(dom_id) = node.dom_node_id {
479
7988
                    let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
480
7988
                    matches!(node_data.get_node_type(), NodeType::Text(_))
481
                } else {
482
35
                    false
483
                };
484

            
485
8023
                let has_text_in_subtree = if let Some(dom_id) = node.dom_node_id {
486
7988
                    subtree_contains_text(self.ctx.styled_dom, dom_id)
487
                } else {
488
35
                    false
489
                };
490

            
491
8023
                if is_text_node || has_text_in_subtree {
492
                    // Case 1 or 2: Text node or IFC root - measure inline content
493
7988
                    self.calculate_ifc_root_intrinsic_sizes(tree, node_index)
494
                } else {
495
                    // Case 3: True inline element - measured by parent IFC root
496
35
                    Ok(IntrinsicSizes::default())
497
                }
498
            }
499
            FormattingContext::InlineBlock => {
500
                // Inline-block IS an atomic inline - it needs its own intrinsic size.
501
                // Check layout tree children AND direct DOM text children (text nodes
502
                // are not in the layout tree, only in the DOM).
503
70
                let has_inline_children = tree.children(node_index).iter().any(|&child_idx| {
504
70
                    tree.get(child_idx)
505
70
                        .map(|c| matches!(c.formatting_context, FormattingContext::Inline))
506
70
                        .unwrap_or(false)
507
70
                });
508

            
509
70
                let has_direct_text = if let Some(dom_id) = node.dom_node_id {
510
70
                    let node_hierarchy = &self.ctx.styled_dom.node_hierarchy.as_container();
511
70
                    dom_id.az_children(node_hierarchy).any(|child_id| {
512
70
                        let child_node_data = &self.ctx.styled_dom.node_data.as_container()[child_id];
513
70
                        matches!(child_node_data.get_node_type(), NodeType::Text(_))
514
70
                    })
515
                } else {
516
                    false
517
                };
518

            
519
70
                if has_inline_children || has_direct_text {
520
                    // InlineBlock with inline children - measure as IFC root.
521
                    // Returns content-level intrinsic sizes (no margin/padding/border).
522
                    // The parent adds box-model extras via calculate_block_intrinsic_sizes,
523
                    // and calculate_used_size_for_node adds padding+border for border-box.
524
70
                    let intrinsic = self.calculate_ifc_root_intrinsic_sizes(tree, node_index)?;
525

            
526
70
                    Ok(intrinsic)
527
                } else {
528
                    // InlineBlock with block children - aggregate like block
529
                    self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics)
530
                }
531
            }
532
            FormattingContext::Table => {
533
1155
                self.calculate_table_intrinsic_sizes(tree, node_index, child_intrinsics)
534
            }
535
            FormattingContext::Flex => {
536
213
                self.calculate_flex_intrinsic_sizes(tree, node_index, child_intrinsics)
537
            }
538
4165
            _ => self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics),
539
        }
540
15913
    }
541
    
542
    // +spec:intrinsic-sizing:ea2c2c - §5.1 min-content size = size as float with auto; max-content = no wrapping
543
    /// Calculate intrinsic sizes for an IFC root (a block containing inline content).
544
    /// This collects ALL inline descendants' text and measures it ONCE.
545
    // +spec:intrinsic-sizing:8f3c0c - hanging glyphs must be excluded from intrinsic size measurement
546
11033
    fn calculate_ifc_root_intrinsic_sizes(
547
11033
        &mut self,
548
11033
        tree: &LayoutTree,
549
11033
        node_index: usize,
550
11033
    ) -> Result<IntrinsicSizes> {
551
        // Collect all inline content from this IFC root and its inline descendants
552
11033
        let inline_content = collect_inline_content(&mut self.ctx, tree, node_index)?;
553

            
554

            
555

            
556
11033
        if inline_content.is_empty() {
557
            return Ok(IntrinsicSizes::default());
558
11033
        }
559

            
560
        // Get pre-loaded fonts from font manager
561
11033
        let loaded_fonts = self.ctx.font_manager.get_loaded_fonts();
562

            
563
        // +spec:intrinsic-sizing:ae8beb - min-content = zero-width CB, max-content = infinite-width CB
564
        // +spec:intrinsic-sizing:8c94e2 - min-content/max-content intrinsic size determination via constrained layout
565
        // Use `measure_intrinsic_widths` instead of two `layout_flow` passes (fix B):
566
        // it runs stages 1–4 of the pipeline once (logical → BiDi → shape → orient)
567
        // and derives min/max-content by scanning the shaped items directly. This
568
        // avoids the BreakCursor line-breaking loop entirely — that loop clones
569
        // every ShapedCluster it inspects via `peek_next_unit` and accounted for
570
        // 24% of total CPU on the text_2000 stress fixture. Shaping is cached
571
        // at the per-item level (keyed on text+style), so the subsequent real
572
        // layout_flow call for this content gets pure cache hits for stages 1–3.
573
11033
        let constraints = UnifiedConstraints::default();
574
11033
        let intrinsic_text = match self.text_cache.measure_intrinsic_widths(
575
11033
            &inline_content,
576
11033
            &[],
577
11033
            &constraints,
578
11033
            &self.ctx.font_manager.font_chain_cache,
579
11033
            &self.ctx.font_manager.fc_cache,
580
11033
            &loaded_fonts,
581
11033
            self.ctx.debug_messages,
582
11033
        ) {
583
11033
            Ok(r) => r,
584
            Err(_) => {
585
                return Ok(IntrinsicSizes {
586
                    min_content_width: 100.0,
587
                    max_content_width: 300.0,
588
                    preferred_width: None,
589
                    min_content_height: 20.0,
590
                    max_content_height: 20.0,
591
                    preferred_height: None,
592
                });
593
            }
594
        };
595

            
596
11033
        let min_width = intrinsic_text.min_content_width;
597
11033
        let max_width = intrinsic_text.max_content_width;
598

            
599
        // +spec:display-property:c587fd - min-content block size equals max-content block size for block containers, tables, inline boxes
600
        // +spec:intrinsic-sizing:02eedc - min-content block size equals max-content block size for block containers
601
        // For a single-line max-content layout the height is one line box;
602
        // `measure_intrinsic_widths` returns exactly that.
603
11033
        let max_content_height = intrinsic_text.max_content_height;
604

            
605
        // NOTE(writing-modes): min_content_width / max_content_width are named for
606
        // the physical axis. In vertical writing modes the "inline" axis is vertical,
607
        // so these are swapped by calculate_block_intrinsic_sizes when computing
608
        // the parent's intrinsic sizes. The physical naming is intentional here.
609
11033
        Ok(IntrinsicSizes {
610
11033
            min_content_width: min_width,
611
11033
            max_content_width: max_width,
612
11033
            preferred_width: None,
613
11033
            min_content_height: max_content_height,
614
11033
            max_content_height,
615
11033
            preferred_height: None,
616
11033
        })
617
11033
    }
618

            
619
    // +spec:containing-block:bb0658 - percentage block-sizes behave as auto during intrinsic computation (no CSS height resolution here)
620
    // +spec:display-contents:84fe7f - cyclic percentage contributions: percentage-sized children use auto during intrinsic sizing
621
    // +spec:min-max-sizing:411904 - percentage block-sizes treated as auto during intrinsic sizing (content-sized CB)
622
    // +spec:min-max-sizing:737e62 - percentage heights don't resolve inside content-sized containing blocks
623
6452
    fn calculate_block_intrinsic_sizes(
624
6452
        &mut self,
625
6452
        tree: &LayoutTree,
626
6452
        node_index: usize,
627
6452
        child_intrinsics: &[(usize, IntrinsicSizes)],
628
6452
    ) -> Result<IntrinsicSizes> {
629
6452
        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
630
6452
        let writing_mode = if let Some(dom_id) = node.dom_node_id {
631
6452
            let node_state =
632
6452
                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
633
6452
            get_writing_mode(self.ctx.styled_dom, dom_id, node_state).unwrap_or_default()
634
        } else {
635
            LayoutWritingMode::default()
636
        };
637

            
638
        // NOTE: Text content detection is now handled in calculate_node_intrinsic_sizes
639
        // which calls calculate_ifc_root_intrinsic_sizes for blocks with inline content.
640
        // This function now only handles pure block containers (BFC roots).
641
        // +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)
642

            
643
6452
        let mut max_child_min_cross = 0.0f32;
644
6452
        let mut max_child_max_cross = 0.0f32;
645
6452
        let mut total_main_size = 0.0;
646
        // Track margins for CSS 2.2 §8.3.1 collapsing in the block direction.
647
        // Block margins collapse between siblings (max instead of sum) and
648
        // parent-child margins can escape (first/last child).
649
6452
        let mut last_margin_main_end = 0.0f32;
650
6452
        let mut is_first_child = true;
651

            
652
7357
        for &child_index in tree.children(node_index) {
653
10361
            if let Some(child_intrinsic) = child_intrinsics.iter().find(|(k, _)| k == &child_index).map(|(_, v)| v) {
654
                // +spec:intrinsic-sizing:ed72bb - intrinsic contributions based on outer size, auto margins as zero
655
7357
                let child_node = tree.get(child_index);
656
7357
                let (cross_extras, main_border_padding, main_margin_start, main_margin_end) =
657
7357
                    if let Some(cn) = child_node {
658
7357
                        let bp = cn.box_props.unpack();
659
7357
                        let h = bp.margin.left + bp.margin.right
660
7357
                              + bp.border.left + bp.border.right
661
7357
                              + bp.padding.left + bp.padding.right;
662
7357
                        let v_bp = bp.border.top + bp.border.bottom
663
7357
                              + bp.padding.top + bp.padding.bottom;
664
7357
                        match writing_mode {
665
7357
                            LayoutWritingMode::HorizontalTb => (h, v_bp, bp.margin.top, bp.margin.bottom),
666
                            _ => (v_bp, h, bp.margin.left, bp.margin.right),
667
                        }
668
                    } else {
669
                        (0.0, 0.0, 0.0, 0.0)
670
                    };
671

            
672
7357
                let (child_min_cross, child_max_cross, child_border_box_main) = match writing_mode {
673
7357
                    LayoutWritingMode::HorizontalTb => (
674
7357
                        child_intrinsic.min_content_width + cross_extras,
675
7357
                        child_intrinsic.max_content_width + cross_extras,
676
7357
                        child_intrinsic.max_content_height + main_border_padding,
677
7357
                    ),
678
                    _ => (
679
                        child_intrinsic.min_content_height + cross_extras,
680
                        child_intrinsic.max_content_height + cross_extras,
681
                        child_intrinsic.max_content_width + main_border_padding,
682
                    ),
683
                };
684

            
685
7357
                max_child_min_cross = max_child_min_cross.max(child_min_cross);
686
7357
                max_child_max_cross = max_child_max_cross.max(child_max_cross);
687

            
688
                // CSS 2.2 §8.3.1 margin collapsing for intrinsic sizing:
689
                // - First child's margin-start can escape (don't add to total)
690
                // - Between siblings: collapsed gap = max(prev_end, curr_start)
691
                // - Last child's margin-end can escape (don't add to total)
692
7357
                if is_first_child {
693
5237
                    is_first_child = false;
694
5237
                    // First child: top margin may escape, don't add it
695
5237
                } else {
696
2120
                    // Sibling gap: collapsed margin between prev bottom and current top
697
2120
                    let collapsed_gap = crate::solver3::fc::collapse_margins(
698
2120
                        last_margin_main_end, main_margin_start
699
2120
                    );
700
2120
                    total_main_size += collapsed_gap;
701
2120
                }
702

            
703
7357
                total_main_size += child_border_box_main;
704
7357
                last_margin_main_end = main_margin_end;
705
            }
706
        }
707
        // Last child's margin-end may escape — don't add it to total_main_size
708

            
709
6452
        let (min_width, max_width, min_height, max_height) = match writing_mode {
710
6452
            LayoutWritingMode::HorizontalTb => (
711
6452
                max_child_min_cross,
712
6452
                max_child_max_cross,
713
6452
                total_main_size,
714
6452
                total_main_size,
715
6452
            ),
716
            _ => (
717
                total_main_size,
718
                total_main_size,
719
                max_child_min_cross,
720
                max_child_max_cross,
721
            ),
722
        };
723

            
724
6452
        Ok(IntrinsicSizes {
725
6452
            min_content_width: min_width,
726
6452
            max_content_width: max_width,
727
6452
            preferred_width: None,
728
6452
            min_content_height: min_height,
729
6452
            max_content_height: max_height,
730
6452
            preferred_height: None,
731
6452
        })
732
6452
    }
733

            
734
    // The max-content main size is the sum of items' max-content contributions.
735
    // The min-content main size of a single-line flex container is the sum of items'
736
    // min-content contributions. For multi-line, it is the largest min-content contribution.
737
    // Auto margins on flex items are treated as 0 for this computation.
738
213
    fn calculate_flex_intrinsic_sizes(
739
213
        &mut self,
740
213
        tree: &LayoutTree,
741
213
        node_index: usize,
742
213
        child_intrinsics: &[(usize, IntrinsicSizes)],
743
213
    ) -> Result<IntrinsicSizes> {
744
213
        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
745

            
746
        // Determine flex-direction to know if main axis is horizontal or vertical
747
213
        let is_row = if let Some(dom_id) = node.dom_node_id {
748
213
            let node_state =
749
213
                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
750
213
            match get_flex_direction(self.ctx.styled_dom, dom_id, &node_state) {
751
213
                MultiValue::Exact(dir) => matches!(dir, LayoutFlexDirection::Row | LayoutFlexDirection::RowReverse),
752
                _ => true, // default is row
753
            }
754
        } else {
755
            true // default flex-direction is row
756
        };
757

            
758
213
        let mut sum_main_min: f32 = 0.0;
759
213
        let mut sum_main_max: f32 = 0.0;
760
213
        let mut max_main_min: f32 = 0.0;
761
213
        let mut max_cross_min: f32 = 0.0;
762
213
        let mut max_cross_max: f32 = 0.0;
763

            
764
249
        for &child_index in tree.children(node_index) {
765
355
            if let Some(child_intrinsic) = child_intrinsics.iter().find(|(k, _)| k == &child_index).map(|(_, v)| v) {
766
249
                let (child_main_min, child_main_max, child_cross_min, child_cross_max) = if is_row {
767
142
                    (
768
142
                        child_intrinsic.min_content_width,
769
142
                        child_intrinsic.max_content_width,
770
142
                        child_intrinsic.min_content_height,
771
142
                        child_intrinsic.max_content_height,
772
142
                    )
773
                } else {
774
107
                    (
775
107
                        child_intrinsic.min_content_height,
776
107
                        child_intrinsic.max_content_height,
777
107
                        child_intrinsic.min_content_width,
778
107
                        child_intrinsic.max_content_width,
779
107
                    )
780
                };
781

            
782
249
                sum_main_max += child_main_max;
783
249
                sum_main_min += child_main_min;
784
                // For multi-line min-content, track the largest single item
785
249
                max_main_min = max_main_min.max(child_main_min);
786

            
787
                // Cross axis: largest child determines the container's cross size
788
249
                max_cross_min = max_cross_min.max(child_cross_min);
789
249
                max_cross_max = max_cross_max.max(child_cross_max);
790
            }
791
        }
792

            
793
        // For single-line (nowrap), min-content = sum; for multi-line (wrap), min-content = max
794
        // Default flex-wrap is nowrap (single-line)
795
213
        let is_single_line = if let Some(dom_id) = node.dom_node_id {
796
213
            let node_state =
797
213
                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
798
213
            let wrap_prop = crate::solver3::getters::get_flex_wrap_prop(
799
213
                self.ctx.styled_dom, dom_id, &node_state,
800
            );
801
213
            match wrap_prop {
802
                Some(val) => matches!(
803
                    val.get_property_or_default().unwrap_or_default(),
804
                    LayoutFlexWrap::NoWrap
805
                ),
806
213
                None => true, // default is nowrap
807
            }
808
        } else {
809
            true
810
        };
811

            
812
213
        let min_main = if is_single_line { sum_main_min } else { max_main_min };
813
213
        let max_main = sum_main_max;
814

            
815
213
        if is_row {
816
72
            Ok(IntrinsicSizes {
817
72
                min_content_width: min_main,
818
72
                max_content_width: max_main,
819
72
                preferred_width: None,
820
72
                min_content_height: max_cross_min,
821
72
                max_content_height: max_cross_max,
822
72
                preferred_height: None,
823
72
            })
824
        } else {
825
141
            Ok(IntrinsicSizes {
826
141
                min_content_width: max_cross_min,
827
141
                max_content_width: max_cross_max,
828
141
                preferred_width: None,
829
141
                min_content_height: min_main,
830
141
                max_content_height: max_main,
831
141
                preferred_height: None,
832
141
            })
833
        }
834
213
    }
835

            
836
    /// Calculate intrinsic sizes for a table element by aggregating cell content
837
    /// widths per column and row heights.
838
    /// +spec:table-layout:93b13c - shrink-to-fit for tables uses intrinsic sizing
839
1155
    fn calculate_table_intrinsic_sizes(
840
1155
        &mut self,
841
1155
        tree: &LayoutTree,
842
1155
        node_index: usize,
843
1155
        child_intrinsics: &[(usize, IntrinsicSizes)],
844
1155
    ) -> Result<IntrinsicSizes> {
845
        // Collect per-column min/max widths and total row heights.
846
        // Table structure: table > row-group? > row > cell
847
1155
        let mut col_min: Vec<f32> = Vec::new();
848
1155
        let mut col_max: Vec<f32> = Vec::new();
849
1155
        let mut total_height = 0.0f32;
850

            
851
        // Iterate rows — children may be row groups (thead/tbody/tfoot) or direct rows
852
1155
        let mut rows: Vec<usize> = Vec::new();
853
1190
        for &child_idx in tree.children(node_index) {
854
1190
            let child = match tree.get(child_idx) { Some(c) => c, None => continue };
855
1190
            match child.formatting_context {
856
1190
                FormattingContext::TableRow => rows.push(child_idx),
857
                FormattingContext::TableRowGroup => {
858
                    // Row group contains rows
859
                    for &row_idx in tree.children(child_idx) {
860
                        if let Some(row) = tree.get(row_idx) {
861
                            if matches!(row.formatting_context, FormattingContext::TableRow) {
862
                                rows.push(row_idx);
863
                            }
864
                        }
865
                    }
866
                }
867
                _ => {}
868
            }
869
        }
870

            
871
2345
        for &row_idx in &rows {
872
1190
            let mut row_height = 0.0f32;
873
1190
            let mut col = 0usize;
874
2975
            for &cell_idx in tree.children(row_idx) {
875
3045
                let cell_intrinsic = child_intrinsics.iter().find(|(k, _)| k == &cell_idx).map(|(_, v)| *v)
876
2975
                    .unwrap_or_default();
877
                // Also check if cell has IFC content we can measure
878
2975
                let cell_is = if cell_intrinsic.max_content_width > 0.0 {
879
                    cell_intrinsic
880
                } else {
881
                    // Try to measure cell content via IFC
882
2975
                    self.calculate_ifc_root_intrinsic_sizes(tree, cell_idx)
883
2975
                        .unwrap_or_default()
884
                };
885

            
886
                // Add cell box-model extras
887
2975
                let cell_node = tree.get(cell_idx);
888
2975
                let (h_extras, v_extras) = if let Some(cn) = cell_node {
889
2975
                    let bp = cn.box_props.unpack();
890
2975
                    (bp.padding.left + bp.padding.right + bp.border.left + bp.border.right,
891
2975
                     bp.padding.top + bp.padding.bottom + bp.border.top + bp.border.bottom)
892
                } else { (0.0, 0.0) };
893

            
894
2975
                let cell_min = cell_is.min_content_width + h_extras;
895
2975
                let cell_max = cell_is.max_content_width + h_extras;
896
2975
                let cell_h = cell_is.max_content_height + v_extras;
897

            
898
2975
                if col >= col_min.len() {
899
2940
                    col_min.push(cell_min);
900
2940
                    col_max.push(cell_max);
901
2940
                } else {
902
35
                    col_min[col] = col_min[col].max(cell_min);
903
35
                    col_max[col] = col_max[col].max(cell_max);
904
35
                }
905
2975
                row_height = row_height.max(cell_h);
906
2975
                col += 1;
907
            }
908
1190
            total_height += row_height;
909
        }
910

            
911
1155
        let min_width: f32 = col_min.iter().sum();
912
1155
        let max_width: f32 = col_max.iter().sum();
913

            
914
1155
        Ok(IntrinsicSizes {
915
1155
            min_content_width: min_width,
916
1155
            max_content_width: max_width,
917
1155
            min_content_height: total_height,
918
1155
            max_content_height: total_height,
919
1155
            preferred_width: None,
920
1155
            preferred_height: None,
921
1155
        })
922
1155
    }
923
}
924

            
925
/// Gathers all inline content for the intrinsic sizing pass.
926
///
927
/// This function recursively collects text and inline-level content according to
928
/// CSS Sizing Level 3, Section 4.1: "Intrinsic Sizes"
929
/// https://www.w3.org/TR/css-sizing-3/#intrinsic-sizes
930
///
931
/// For inline formatting contexts, we need to gather:
932
/// 1. Text nodes (inline content)
933
/// 2. Inline-level boxes (display: inline, inline-block, etc.)
934
/// 3. Atomic inline-level elements (replaced elements like images)
935
///
936
/// The key difference from `collect_and_measure_inline_content` in fc.rs is that
937
/// this version is used for intrinsic sizing (calculating min/max-content widths)
938
/// before the actual layout pass, so it must recursively gather content from
939
/// inline descendants without laying them out first.
940
11033
fn collect_inline_content_for_sizing<T: ParsedFontTrait>(
941
11033
    ctx: &mut LayoutContext<'_, T>,
942
11033
    tree: &LayoutTree,
943
11033
    ifc_root_index: usize,
944
11033
) -> Result<Vec<InlineContent>> {
945
11033
    ctx.debug_log(&format!(
946
11033
        "Collecting inline content from node {} for intrinsic sizing",
947
11033
        ifc_root_index
948
11033
    ));
949

            
950
11033
    let mut content = Vec::new();
951

            
952
    // Recursively collect inline content from this node and its inline descendants
953
11033
    collect_inline_content_recursive(ctx, tree, ifc_root_index, &mut content)?;
954

            
955
11033
    ctx.debug_log(&format!(
956
11033
        "Collected {} inline content items from node {}",
957
11033
        content.len(),
958
11033
        ifc_root_index
959
11033
    ));
960

            
961
11033
    Ok(content)
962
11033
}
963

            
964
/// Recursive helper for collecting inline content.
965
///
966
/// According to CSS Sizing Level 3, the intrinsic size of an inline formatting context
967
/// is based on all inline-level content, including text in nested inline elements.
968
///
969
/// This function:
970
/// - Collects text from the current node if it's a text node
971
/// - Collects text from DOM children (text nodes may not be in layout tree)
972
/// - Recursively collects from inline children (display: inline)
973
/// - Treats non-inline children as atomic inline-level boxes
974
23077
fn collect_inline_content_recursive<T: ParsedFontTrait>(
975
23077
    ctx: &mut LayoutContext<'_, T>,
976
23077
    tree: &LayoutTree,
977
23077
    node_index: usize,
978
23077
    content: &mut Vec<InlineContent>,
979
23077
) -> Result<()> {
980
23077
    let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
981

            
982
    // CRITICAL FIX: Text nodes may exist in the DOM but not as separate layout nodes!
983
    // We need to check the DOM children for text content.
984
23077
    let Some(dom_id) = node.dom_node_id else {
985
        // No DOM ID means this is a synthetic node, skip text extraction
986
        return process_layout_children(ctx, tree, node_index, content);
987
    };
988

            
989
    // First check if THIS node is a text node
990
23077
    if let Some(text) = extract_text_from_node(ctx.styled_dom, dom_id) {
991
10788
        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)));
992
10788
        ctx.debug_log(&format!("Found text in node {}: '{}'", node_index, text));
993
10788
        // Use split_text_for_whitespace to correctly handle white-space: pre with \n
994
10788
        let text_items = split_text_for_whitespace(
995
10788
            ctx.styled_dom,
996
10788
            dom_id,
997
10788
            &text,
998
10788
            style_props,
999
10788
        );
10788
        content.extend(text_items);
12289
    }
    // 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.
23077
    let node_hierarchy = &ctx.styled_dom.node_hierarchy.as_container();
23077
    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
12289
        if tree.dom_to_layout.contains_key(&child_id) {
12289
            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);
        }
    }
23077
    process_layout_children(ctx, tree, node_index, content)
23077
}
/// Helper to process layout tree children for inline content collection
23077
fn process_layout_children<T: ParsedFontTrait>(
23077
    ctx: &mut LayoutContext<'_, T>,
23077
    tree: &LayoutTree,
23077
    node_index: usize,
23077
    content: &mut Vec<InlineContent>,
23077
) -> Result<()> {
    use azul_css::props::basic::SizeMetric;
    use azul_css::props::layout::{LayoutHeight, LayoutWidth};
    // Process layout tree children (these are elements with layout properties)
23077
    for &child_index in tree.children(node_index) {
12289
        let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
12289
        let Some(child_dom_id) = child_node.dom_node_id else {
            continue;
        };
12289
        let display = get_display_property(ctx.styled_dom, Some(child_dom_id));
        // CSS Sizing Level 3: Inline-level boxes participate in the IFC
12289
        if display.unwrap_or_default() == LayoutDisplay::Inline {
            // Recursively collect content from inline children
            // This is CRITICAL for proper intrinsic width calculation!
12044
            ctx.debug_log(&format!(
12044
                "Recursing into inline child at node {}",
12044
                child_index
12044
            ));
12044
            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
245
            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.
245
            let node_state =
245
                &ctx.styled_dom.styled_nodes.as_container()[child_dom_id].styled_node_state;
245
            let css_width = get_css_width(ctx.styled_dom, child_dom_id, node_state);
245
            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
245
            let used_width = match css_width {
70
                MultiValue::Exact(LayoutWidth::Px(px)) => {
                    // Convert PixelValue to f32
                    use azul_css::props::basic::pixel::{DEFAULT_FONT_SIZE, PT_TO_PX};
70
                    match px.metric {
70
                        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
175
                _ => 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
245
            let used_height = match css_height {
70
                MultiValue::Exact(LayoutHeight::Px(px)) => {
                    use azul_css::props::basic::pixel::{DEFAULT_FONT_SIZE, PT_TO_PX};
70
                    match px.metric {
70
                        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,
175
                _ => intrinsic_sizes.max_content_height,
            };
245
            ctx.debug_log(&format!(
245
                "Found atomic inline child at node {}: display={:?}, intrinsic_width={}, used_width={}, css_width={:?}",
245
                child_index, display, intrinsic_sizes.max_content_width, used_width, css_width
245
            ));
            // Represent as a rectangular shape with the resolved dimensions
245
            content.push(InlineContent::Shape(InlineShape {
245
                shape_def: ShapeDefinition::Rectangle {
245
                    size: crate::text3::cache::Size {
245
                        width: used_width,
245
                        height: used_height,
245
                    },
245
                    corner_radius: None,
245
                },
245
                fill: None,
245
                stroke: None,
245
                baseline_offset: used_height,
245
                alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, child_dom_id),
245
                source_node_id: Some(child_dom_id),
245
            }));
        }
    }
23077
    Ok(())
23077
}
// Keep old name as an alias for backward compatibility
11033
pub fn collect_inline_content<T: ParsedFontTrait>(
11033
    ctx: &mut LayoutContext<'_, T>,
11033
    tree: &LayoutTree,
11033
    ifc_root_index: usize,
11033
) -> Result<Vec<InlineContent>> {
11033
    collect_inline_content_for_sizing(ctx, tree, ifc_root_index)
11033
}
// +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)]
7350
fn auto_block_inline_size(cb: &LogicalSize, bp: &BoxProps) -> f32 {
7350
    let aw = cb.width
7350
        - bp.margin.left
7350
        - bp.margin.right
7350
        - bp.border.left
7350
        - bp.border.right
7350
        - bp.padding.left
7350
        - bp.padding.right;
7350
    aw.max(0.0)
7350
}
29820
pub fn calculate_used_size_for_node(
29820
    styled_dom: &StyledDom,
29820
    dom_id: Option<NodeId>,
29820
    // M12.7: by-reference (GP-register pointer). A by-value LogicalSize is an HFA
29820
    // (2×f32) the remill lift stages as an 8-byte double into a V register, and that
29820
    // f64/d-register copyload mis-tracks to 0 in the wasm lift (single-f32 reads work,
29820
    // the 64-bit one doesn't) — so cb + viewport arrived 0 and every width came out 0.
29820
    // A pointer arg lifts cleanly; the body reads only .width/.height (auto-deref).
29820
    containing_block_size: &LogicalSize,
29820
    intrinsic: IntrinsicSizes,
29820
    _box_props: &BoxProps,
29820
    viewport_size: &LogicalSize,
29820
) -> Result<LogicalSize> {
29820
    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.
175
        return Ok(LogicalSize::new(
175
            containing_block_size.width,
175
            if intrinsic.max_content_height > 0.0 {
                intrinsic.max_content_height
            } else {
                // Auto height - will be resolved from content
175
                0.0
            },
        ));
    };
29645
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
29645
    let css_width = get_css_width(styled_dom, id, node_state);
29645
    let css_height = get_css_height(styled_dom, id, node_state);
29645
    let writing_mode = get_writing_mode(styled_dom, id, node_state);
29645
    let display = get_display_property(styled_dom, Some(id));
29645
    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).
29645
    let wm_ctx = WritingModeContext::new(
29645
        writing_mode.unwrap_or_default(),
29645
        get_direction_property(styled_dom, id, node_state).unwrap_or_default(),
29645
        get_text_orientation_property(styled_dom, id, node_state).unwrap_or_default(),
    );
29645
    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)
29645
    let node_data = &styled_dom.node_data.as_container()[id];
29645
    let is_replaced = matches!(node_data.get_node_type(), NodeType::Image(_))
29645
        || 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.
29645
    let css_width = if display.unwrap_or_default() == LayoutDisplay::Inline
7735
        && !is_replaced
    {
7735
        MultiValue::Exact(LayoutWidth::Auto)
    } else {
21910
        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.
29645
    let css_height = if display.unwrap_or_default() == LayoutDisplay::Inline
7735
        && !is_replaced
    {
7735
        MultiValue::Exact(LayoutHeight::Auto)
    } else {
21910
        css_height
    };
    // Remember if width/height were auto before consuming them
29645
    let width_is_auto = css_width.is_auto() || matches!(&css_width, MultiValue::Exact(LayoutWidth::Auto));
29645
    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
29645
    let width_is_quantitative = matches!(
12005
        &css_width,
        MultiValue::Exact(LayoutWidth::Px(_) | LayoutWidth::FitContent(_) | LayoutWidth::Calc(_))
    );
29645
    let height_is_quantitative = matches!(
14420
        &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
29645
    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)
25375
            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.
                intrinsic.max_content_width
            }
            // +spec:intrinsic-sizing:560697 - shrink-to-fit = clamp(min-content, stretch-fit, max-content)
25375
            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)
            }
25375
            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)
                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)
            } 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.
25375
            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.
7350
                    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)
140
                    let available_width = (containing_block_size.width
140
                        - _box_props.margin.left
140
                        - _box_props.margin.right
140
                        - _box_props.border.left
140
                        - _box_props.border.right
140
                        - _box_props.padding.left
140
                        - _box_props.padding.right)
140
                        .max(0.0);
140
                    let preferred_minimum = intrinsic.min_content_width;
140
                    let preferred = intrinsic.max_content_width;
140
                    preferred_minimum.max(available_width).min(preferred).max(0.0)
                }
                LayoutDisplay::Inline => {
                    // For inline elements, 'auto' width is the intrinsic/max-content width
7735
                    intrinsic.max_content_width
                }
2170
                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 => {
7980
                    if intrinsic.max_content_width > 0.0 {
7770
                        intrinsic.max_content_width
                    } else {
210
                        (containing_block_size.width
210
                            - _box_props.margin.left
210
                            - _box_props.margin.right
210
                            - _box_props.border.left
210
                            - _box_props.border.right
210
                            - _box_props.padding.left
210
                            - _box_props.padding.right)
210
                            .max(0.0)
                    }
                }
                // Other display types use intrinsic sizing
                _ => intrinsic.max_content_width,
            }
            }
        }
4270
        LayoutWidth::Px(px) => {
            // Resolve percentage or absolute pixel value
            use azul_css::props::basic::{
                pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
                SizeMetric,
            };
4270
            let pixels_opt = match px.metric {
4130
                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)),
140
                SizeMetric::Percent => None,
            };
4270
            match pixels_opt {
4130
                Some(pixels) => pixels,
140
                None => match px.to_percent() {
140
                    Some(p) => {
140
                        let result = resolve_percentage_with_box_model(
140
                            containing_block_size.width,
140
                            p.get(),
140
                            (_box_props.margin.left, _box_props.margin.right),
140
                            (_box_props.border.left, _box_props.border.right),
140
                            (_box_props.padding.left, _box_props.padding.right),
                        );
140
                        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"
29645
    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
29645
    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.
22960
            match display.unwrap_or_default() {
                LayoutDisplay::Block
                | LayoutDisplay::FlowRoot
                | LayoutDisplay::ListItem
                | LayoutDisplay::Flex
4795
                | LayoutDisplay::Grid => 0.0,
                // Inline: height property does not apply (§10.6.1), handled earlier
                // via css_height override, but be explicit anyway.
7735
                LayoutDisplay::Inline => 0.0,
                // Shrink-to-fit and intrinsically-sized: keep using intrinsic pre-layout.
10430
                _ => intrinsic.max_content_height,
            }
        }
6685
        LayoutHeight::Px(px) => {
            // Resolve percentage or absolute pixel value
            use azul_css::props::basic::{
                pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
                SizeMetric,
            };
6685
            let pixels_opt = match px.metric {
4585
                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)),
2100
                SizeMetric::Percent => None,
            };
6685
            match pixels_opt {
4585
                Some(pixels) => pixels,
                // +spec:height-calculation:37bc8c - percentage heights resolve against definite containing block height
2100
                None => match px.to_percent() {
2100
                    Some(p) => resolve_percentage_with_box_model(
2100
                        containing_block_size.height,
2100
                        p.get(),
2100
                        (_box_props.margin.top, _box_props.margin.bottom),
2100
                        (_box_props.border.top, _box_props.border.bottom),
2100
                        (_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"
29645
    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.
29645
    let (resolved_width, resolved_height) = if is_replaced
        && width_is_auto
        && 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 {
29645
        (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
29645
    let has_intrinsic_ratio = intrinsic.preferred_width.is_some()
        && intrinsic.preferred_height.is_some()
        && intrinsic.preferred_width.unwrap_or(0.0) > 0.0
        && intrinsic.preferred_height.unwrap_or(0.0) > 0.0;
    // +spec:margin-collapsing:840eb6 - aspect ratio transfers size constraints across dimensions
29645
    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
        apply_constraint_violation_table(
            styled_dom,
            id,
            node_state,
            resolved_width,
            resolved_height,
            containing_block_size.width,
            containing_block_size.height,
            _box_props,
        )
    } else {
        // Non-replaced element: apply width and height constraints independently
29645
        let cw = apply_width_constraints(
29645
            styled_dom,
29645
            id,
29645
            node_state,
29645
            resolved_width,
29645
            containing_block_size.width,
29645
            _box_props,
        );
29645
        let ch = apply_height_constraints(
29645
            styled_dom,
29645
            id,
29645
            node_state,
29645
            resolved_height,
29645
            containing_block_size.height,
29645
            _box_props,
        );
29645
        (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
29645
    let box_sizing = match get_css_box_sizing(styled_dom, id, node_state) {
29645
        MultiValue::Exact(bs) => bs,
        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
            azul_css::props::layout::LayoutBoxSizing::ContentBox
        }
    };
29645
    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
525
            let min_border_box_w = _box_props.padding.left
525
                + _box_props.padding.right
525
                + _box_props.border.left
525
                + _box_props.border.right;
525
            let min_border_box_h = _box_props.padding.top
525
                + _box_props.padding.bottom
525
                + _box_props.border.top
525
                + _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
525
            let bw = if width_is_quantitative {
525
                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
            };
525
            let bh = if height_is_quantitative {
245
                constrained_height.max(min_border_box_h)
            } else {
280
                constrained_height
280
                    + _box_props.padding.top
280
                    + _box_props.padding.bottom
280
                    + _box_props.border.top
280
                    + _box_props.border.bottom
            };
525
            (bw, bh)
        }
        azul_css::props::layout::LayoutBoxSizing::ContentBox => {
            // +spec:box-sizing:fead70 - content-box: width/height set content size, border+padding added outside
29120
            let border_box_width = constrained_width
29120
                + _box_props.padding.left
29120
                + _box_props.padding.right
29120
                + _box_props.border.left
29120
                + _box_props.border.right;
29120
            let border_box_height = constrained_height
29120
                + _box_props.padding.top
29120
                + _box_props.padding.bottom
29120
                + _box_props.border.top
29120
                + _box_props.border.bottom;
29120
            (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).
29645
    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.
29645
        (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
29645
    let result =
29645
        LogicalSize::from_main_cross(main_size, cross_size, writing_mode.unwrap_or_default());
29645
    Ok(result)
29820
}
// +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.
fn apply_constraint_violation_table(
    styled_dom: &StyledDom,
    id: NodeId,
    node_state: &StyledNodeState,
    w: f32,  // tentative width (ignoring min/max)
    h: f32,  // tentative height (ignoring min/max)
    containing_block_width: f32,
    containing_block_height: f32,
    box_props: &BoxProps,
) -> (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)
    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),
        _ => 0.0,
    };
    // Resolve max-width (default infinity)
    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)
            }
        }
        _ => f32::MAX,
    };
    // Resolve min-height (default 0)
    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),
        _ => 0.0,
    };
    // Resolve max-height (default infinity)
    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)
            }
        }
        _ => f32::MAX,
    };
    // max(min, max) so that min ≤ max holds true."
    let max_w = max_w.max(min_w);
    let max_h = max_h.max(min_h);
    // Guard against zero dimensions (avoid division by zero)
    if w <= 0.0 || h <= 0.0 {
        return (w.max(min_w).min(max_w), h.max(min_h).min(max_h));
    }
    let w_over = w > max_w;
    let w_under = w < min_w;
    let h_over = h > max_h;
    let h_under = h < min_h;
    // +spec:min-max-sizing:713560 - constraint violation table for replaced elements with intrinsic ratio
    match (w_over, w_under, h_over, h_under) {
        // Row 1: no constraint violation
        (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)),
    }
}
// +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
29645
fn apply_width_constraints(
29645
    styled_dom: &StyledDom,
29645
    id: NodeId,
29645
    node_state: &StyledNodeState,
29645
    tentative_width: f32,
29645
    containing_block_width: f32,
29645
    box_props: &BoxProps,
29645
) -> 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)
29645
    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),
            }
        }
29645
        _ => 0.0,
    };
    // Resolve max-width (default is infinity/none)
29645
    let max_width = match get_css_max_width(styled_dom, id, node_state) {
35
        MultiValue::Exact(mw) => {
35
            let px = &mw.inner;
            // Check if it's the default "max" value (f32::MAX)
35
            if px.number.get() >= core::f32::MAX - 1.0 {
                None
            } else {
35
                let pixels_opt = match px.metric {
35
                    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,
                };
35
                match pixels_opt {
35
                    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),
                        )
                    }),
                }
            }
        }
29610
        _ => None,
    };
    // Apply constraints: max(min_width, min(tentative, max_width))
    // If min > max, min wins per CSS spec
29645
    let mut result = tentative_width;
29645
    if let Some(max) = max_width {
35
        result = result.min(max);
29610
    }
29645
    result = result.max(min_width);
29645
    result
29645
}
/// 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
29645
fn apply_height_constraints(
29645
    styled_dom: &StyledDom,
29645
    id: NodeId,
29645
    node_state: &StyledNodeState,
29645
    tentative_height: f32,
29645
    containing_block_height: f32,
29645
    box_props: &BoxProps,
29645
) -> 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)
29645
    let min_height = match get_css_min_height(styled_dom, id, node_state) {
280
        MultiValue::Exact(mh) => {
280
            let px = &mh.inner;
280
            let pixels_opt = match px.metric {
280
                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,
            };
280
            match pixels_opt {
280
                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),
            }
        }
29365
        _ => 0.0,
    };
    // Resolve max-height (default is infinity/none)
29645
    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),
                        )
                    }),
                }
            }
        }
29645
        _ => 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
29645
    let mut result = tentative_height;
29645
    if let Some(max) = max_height {
        result = result.min(max);
29645
    }
29645
    result = result.max(min_height);
29645
    result
29645
}
23485
pub fn extract_text_from_node(styled_dom: &StyledDom, node_id: NodeId) -> Option<String> {
23485
    match &styled_dom.node_data.as_container()[node_id].get_node_type() {
11060
        NodeType::Text(text_data) => Some(text_data.as_str().to_string()),
12425
        _ => None,
    }
23485
}