Skip to main content

azul_layout/solver3/
sizing.rs

1//! Intrinsic and used size calculations for layout nodes
2
3use std::{
4    collections::BTreeSet,
5    sync::Arc,
6};
7
8use azul_core::{
9    dom::{FormattingContext, NodeId, NodeType},
10    geom::LogicalSize,
11    resources::RendererResources,
12    styled_dom::{StyledDom, StyledNodeState},
13};
14use 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};
23use rust_fontconfig::FcFontCache;
24
25#[cfg(feature = "text_layout")]
26use crate::text3;
27use 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
61pub fn resolve_percentage_with_box_model(
62    containing_block_dimension: f32,
63    percentage: f32,
64    _margins: (f32, f32),
65    _borders: (f32, f32),
66    _paddings: (f32, f32),
67) -> 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    (containing_block_dimension * percentage).max(0.0)
72}
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).
79fn subtree_contains_text(styled_dom: &StyledDom, dom_id: NodeId) -> bool {
80    let node_hierarchy = styled_dom.node_hierarchy.as_container();
81    let node_data = styled_dom.node_data.as_container();
82    if matches!(node_data[dom_id].get_node_type(), NodeType::Text(_)) {
83        return true;
84    }
85    dom_id
86        .az_children(&node_hierarchy)
87        .any(|child| subtree_contains_text(styled_dom, child))
88}
89
90/// Phase 2a: Calculate intrinsic sizes (bottom-up pass)
91/// // +spec:display-contents:f12d4e - intrinsic sizing: size determined by contents, not context
92pub fn calculate_intrinsic_sizes<T: ParsedFontTrait>(
93    ctx: &mut LayoutContext<'_, T>,
94    tree: &mut LayoutTree,
95    text_cache: &mut LayoutCache,
96    dirty_nodes: &BTreeSet<usize>,
97) -> Result<()> {
98    if dirty_nodes.is_empty() {
99        return Ok(());
100    }
101
102    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    let dirty_closure = compute_dirty_ancestor_closure(tree, dirty_nodes);
111
112    let mut calculator = IntrinsicSizeCalculator::new(ctx, text_cache);
113    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    calculator.calculate_intrinsic_recursive(tree, tree.root, false)?;
129    ctx.debug_log("Finished intrinsic size calculation");
130    Ok(())
131}
132
133fn compute_dirty_ancestor_closure(
134    tree: &LayoutTree,
135    dirty_nodes: &BTreeSet<usize>,
136) -> std::collections::HashSet<usize> {
137    let mut closure: std::collections::HashSet<usize> = std::collections::HashSet::new();
138    for &dirty in dirty_nodes {
139        let mut cur = Some(dirty);
140        while let Some(idx) = cur {
141            if !closure.insert(idx) {
142                break;
143            }
144            cur = tree.get(idx).and_then(|n| n.parent);
145        }
146    }
147    closure
148}
149
150struct 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
167impl<'a, 'b, 'c, T: ParsedFontTrait> IntrinsicSizeCalculator<'a, 'b, 'c, T> {
168    fn new(ctx: &'a mut LayoutContext<'b, T>, text_cache: &'c mut LayoutCache) -> Self {
169        Self {
170            ctx,
171            text_cache,
172            dirty_closure: None,
173        }
174    }
175
176    fn calculate_intrinsic_recursive(
177        &mut self,
178        tree: &mut LayoutTree,
179        node_index: usize,
180        ancestor_is_stf: bool,
181    ) -> 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        if let Some(closure) = self.dirty_closure.as_ref() {
188            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            }
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        if !ancestor_is_stf
205            && tree
206                .subtree_needs_intrinsic
207                .get(node_index)
208                .copied()
209                .map(|v| !v)
210                .unwrap_or(false)
211        {
212            let default = IntrinsicSizes::default();
213            if let Some(n) = tree.warm_mut(node_index) {
214                n.intrinsic_sizes = Some(default);
215            }
216            return Ok(default);
217        }
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        let dom_node_id = tree
225            .get(node_index)
226            .ok_or(LayoutError::InvalidTree)?
227            .dom_node_id;
228
229        // Out-of-flow elements do not contribute to their parent's intrinsic size.
230        let position = get_position_type(self.ctx.styled_dom, dom_node_id);
231        if position == LayoutPosition::Absolute || position == LayoutPosition::Fixed {
232            if let Some(n) = tree.warm_mut(node_index) {
233                n.intrinsic_sizes = Some(IntrinsicSizes::default());
234            }
235            return Ok(IntrinsicSizes::default());
236        }
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        let children_slice = tree.children(node_index);
241        let n = children_slice.len();
242        let mut stack_buf = [0usize; 32];
243        let heap_buf: Vec<usize>;
244        let children: &[usize] = if n <= 32 {
245            stack_buf[..n].copy_from_slice(children_slice);
246            &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        let self_is_stf = tree
254            .get(node_index)
255            .map(|n| {
256                crate::solver3::layout_tree::is_shrink_to_fit_context(
257                    self.ctx.styled_dom,
258                    n.dom_node_id,
259                    &n.formatting_context,
260                )
261            })
262            .unwrap_or(false);
263        let child_ancestor_is_stf = ancestor_is_stf || self_is_stf;
264
265        let mut child_intrinsics = Vec::with_capacity(n);
266        for &child_index in children {
267            let child_intrinsic =
268                self.calculate_intrinsic_recursive(tree, child_index, child_ancestor_is_stf)?;
269            child_intrinsics.push((child_index, child_intrinsic));
270        }
271
272        // Then calculate this node's intrinsic size based on its children
273        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        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            let node_state = &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
281
282            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            }
298
299            if let MultiValue::Exact(mh) = get_css_min_height(self.ctx.styled_dom, dom_id, node_state) {
300                let px = &mh.inner;
301                let resolved = match px.metric {
302                    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                if let Some(min_h) = resolved {
311                    intrinsic.min_content_height = intrinsic.min_content_height.max(min_h);
312                    intrinsic.max_content_height = intrinsic.max_content_height.max(min_h);
313                }
314            }
315        }
316
317        if let Some(n) = tree.warm_mut(node_index) {
318            n.intrinsic_sizes = Some(intrinsic);
319        }
320
321        Ok(intrinsic)
322    }
323
324    fn calculate_node_intrinsic_sizes(
325        &mut self,
326        tree: &LayoutTree,
327        node_index: usize,
328        child_intrinsics: &[(usize, IntrinsicSizes)],
329    ) -> Result<IntrinsicSizes> {
330        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        if let Some(dom_id) = node.dom_node_id {
339            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
340            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            }
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            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            }
384        }
385
386        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                let has_block_child = tree.children(node_index).iter().any(|&child_idx| {
398                    tree.get(child_idx)
399                        .and_then(|c| c.dom_node_id)
400                        .map(|dom_id| {
401                            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
402                            // Text nodes are inline-level
403                            if matches!(node_data.get_node_type(), NodeType::Text(_)) {
404                                return false;
405                            }
406                            let display = get_display_type(self.ctx.styled_dom, dom_id);
407                            display.creates_block_context()
408                        })
409                        .unwrap_or(false)
410                });
411
412                let has_inline_child = tree.children(node_index).iter().any(|&child_idx| {
413                    tree.get(child_idx)
414                        .and_then(|c| c.dom_node_id)
415                        .map(|dom_id| {
416                            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
417                            if matches!(node_data.get_node_type(), NodeType::Text(_)) {
418                                return true;
419                            }
420                            let display = get_display_type(self.ctx.styled_dom, dom_id);
421                            matches!(display,
422                                LayoutDisplay::Inline
423                                | LayoutDisplay::InlineBlock
424                                | LayoutDisplay::InlineFlex
425                                | LayoutDisplay::InlineGrid
426                                | LayoutDisplay::InlineTable
427                            )
428                        })
429                        .unwrap_or(false)
430                });
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                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                let has_direct_text = if !has_block_child {
439                    if let Some(dom_id) = node.dom_node_id {
440                        let node_hierarchy = &self.ctx.styled_dom.node_hierarchy.as_container();
441                        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                    false
450                };
451                
452                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                    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                let is_text_node = if let Some(dom_id) = node.dom_node_id {
479                    let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
480                    matches!(node_data.get_node_type(), NodeType::Text(_))
481                } else {
482                    false
483                };
484
485                let has_text_in_subtree = if let Some(dom_id) = node.dom_node_id {
486                    subtree_contains_text(self.ctx.styled_dom, dom_id)
487                } else {
488                    false
489                };
490
491                if is_text_node || has_text_in_subtree {
492                    // Case 1 or 2: Text node or IFC root - measure inline content
493                    self.calculate_ifc_root_intrinsic_sizes(tree, node_index)
494                } else {
495                    // Case 3: True inline element - measured by parent IFC root
496                    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                let has_inline_children = tree.children(node_index).iter().any(|&child_idx| {
504                    tree.get(child_idx)
505                        .map(|c| matches!(c.formatting_context, FormattingContext::Inline))
506                        .unwrap_or(false)
507                });
508
509                let has_direct_text = if let Some(dom_id) = node.dom_node_id {
510                    let node_hierarchy = &self.ctx.styled_dom.node_hierarchy.as_container();
511                    dom_id.az_children(node_hierarchy).any(|child_id| {
512                        let child_node_data = &self.ctx.styled_dom.node_data.as_container()[child_id];
513                        matches!(child_node_data.get_node_type(), NodeType::Text(_))
514                    })
515                } else {
516                    false
517                };
518
519                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                    let intrinsic = self.calculate_ifc_root_intrinsic_sizes(tree, node_index)?;
525
526                    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                self.calculate_table_intrinsic_sizes(tree, node_index, child_intrinsics)
534            }
535            FormattingContext::Flex => {
536                self.calculate_flex_intrinsic_sizes(tree, node_index, child_intrinsics)
537            }
538            _ => self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics),
539        }
540    }
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    fn calculate_ifc_root_intrinsic_sizes(
547        &mut self,
548        tree: &LayoutTree,
549        node_index: usize,
550    ) -> Result<IntrinsicSizes> {
551        // Collect all inline content from this IFC root and its inline descendants
552        let inline_content = collect_inline_content(&mut self.ctx, tree, node_index)?;
553
554
555
556        if inline_content.is_empty() {
557            return Ok(IntrinsicSizes::default());
558        }
559
560        // Get pre-loaded fonts from font manager
561        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        let constraints = UnifiedConstraints::default();
574        let intrinsic_text = match self.text_cache.measure_intrinsic_widths(
575            &inline_content,
576            &[],
577            &constraints,
578            &self.ctx.font_manager.font_chain_cache,
579            &self.ctx.font_manager.fc_cache,
580            &loaded_fonts,
581            self.ctx.debug_messages,
582        ) {
583            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        let min_width = intrinsic_text.min_content_width;
597        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        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        Ok(IntrinsicSizes {
610            min_content_width: min_width,
611            max_content_width: max_width,
612            preferred_width: None,
613            min_content_height: max_content_height,
614            max_content_height,
615            preferred_height: None,
616        })
617    }
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    fn calculate_block_intrinsic_sizes(
624        &mut self,
625        tree: &LayoutTree,
626        node_index: usize,
627        child_intrinsics: &[(usize, IntrinsicSizes)],
628    ) -> Result<IntrinsicSizes> {
629        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
630        let writing_mode = if let Some(dom_id) = node.dom_node_id {
631            let node_state =
632                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
633            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        let mut max_child_min_cross = 0.0f32;
644        let mut max_child_max_cross = 0.0f32;
645        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        let mut last_margin_main_end = 0.0f32;
650        let mut is_first_child = true;
651
652        for &child_index in tree.children(node_index) {
653            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                let child_node = tree.get(child_index);
656                let (cross_extras, main_border_padding, main_margin_start, main_margin_end) =
657                    if let Some(cn) = child_node {
658                        let bp = cn.box_props.unpack();
659                        let h = bp.margin.left + bp.margin.right
660                              + bp.border.left + bp.border.right
661                              + bp.padding.left + bp.padding.right;
662                        let v_bp = bp.border.top + bp.border.bottom
663                              + bp.padding.top + bp.padding.bottom;
664                        match writing_mode {
665                            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                let (child_min_cross, child_max_cross, child_border_box_main) = match writing_mode {
673                    LayoutWritingMode::HorizontalTb => (
674                        child_intrinsic.min_content_width + cross_extras,
675                        child_intrinsic.max_content_width + cross_extras,
676                        child_intrinsic.max_content_height + main_border_padding,
677                    ),
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                max_child_min_cross = max_child_min_cross.max(child_min_cross);
686                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                if is_first_child {
693                    is_first_child = false;
694                    // First child: top margin may escape, don't add it
695                } else {
696                    // Sibling gap: collapsed margin between prev bottom and current top
697                    let collapsed_gap = crate::solver3::fc::collapse_margins(
698                        last_margin_main_end, main_margin_start
699                    );
700                    total_main_size += collapsed_gap;
701                }
702
703                total_main_size += child_border_box_main;
704                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        let (min_width, max_width, min_height, max_height) = match writing_mode {
710            LayoutWritingMode::HorizontalTb => (
711                max_child_min_cross,
712                max_child_max_cross,
713                total_main_size,
714                total_main_size,
715            ),
716            _ => (
717                total_main_size,
718                total_main_size,
719                max_child_min_cross,
720                max_child_max_cross,
721            ),
722        };
723
724        Ok(IntrinsicSizes {
725            min_content_width: min_width,
726            max_content_width: max_width,
727            preferred_width: None,
728            min_content_height: min_height,
729            max_content_height: max_height,
730            preferred_height: None,
731        })
732    }
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    fn calculate_flex_intrinsic_sizes(
739        &mut self,
740        tree: &LayoutTree,
741        node_index: usize,
742        child_intrinsics: &[(usize, IntrinsicSizes)],
743    ) -> Result<IntrinsicSizes> {
744        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        let is_row = if let Some(dom_id) = node.dom_node_id {
748            let node_state =
749                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
750            match get_flex_direction(self.ctx.styled_dom, dom_id, &node_state) {
751                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        let mut sum_main_min: f32 = 0.0;
759        let mut sum_main_max: f32 = 0.0;
760        let mut max_main_min: f32 = 0.0;
761        let mut max_cross_min: f32 = 0.0;
762        let mut max_cross_max: f32 = 0.0;
763
764        for &child_index in tree.children(node_index) {
765            if let Some(child_intrinsic) = child_intrinsics.iter().find(|(k, _)| k == &child_index).map(|(_, v)| v) {
766                let (child_main_min, child_main_max, child_cross_min, child_cross_max) = if is_row {
767                    (
768                        child_intrinsic.min_content_width,
769                        child_intrinsic.max_content_width,
770                        child_intrinsic.min_content_height,
771                        child_intrinsic.max_content_height,
772                    )
773                } else {
774                    (
775                        child_intrinsic.min_content_height,
776                        child_intrinsic.max_content_height,
777                        child_intrinsic.min_content_width,
778                        child_intrinsic.max_content_width,
779                    )
780                };
781
782                sum_main_max += child_main_max;
783                sum_main_min += child_main_min;
784                // For multi-line min-content, track the largest single item
785                max_main_min = max_main_min.max(child_main_min);
786
787                // Cross axis: largest child determines the container's cross size
788                max_cross_min = max_cross_min.max(child_cross_min);
789                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        let is_single_line = if let Some(dom_id) = node.dom_node_id {
796            let node_state =
797                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
798            let wrap_prop = crate::solver3::getters::get_flex_wrap_prop(
799                self.ctx.styled_dom, dom_id, &node_state,
800            );
801            match wrap_prop {
802                Some(val) => matches!(
803                    val.get_property_or_default().unwrap_or_default(),
804                    LayoutFlexWrap::NoWrap
805                ),
806                None => true, // default is nowrap
807            }
808        } else {
809            true
810        };
811
812        let min_main = if is_single_line { sum_main_min } else { max_main_min };
813        let max_main = sum_main_max;
814
815        if is_row {
816            Ok(IntrinsicSizes {
817                min_content_width: min_main,
818                max_content_width: max_main,
819                preferred_width: None,
820                min_content_height: max_cross_min,
821                max_content_height: max_cross_max,
822                preferred_height: None,
823            })
824        } else {
825            Ok(IntrinsicSizes {
826                min_content_width: max_cross_min,
827                max_content_width: max_cross_max,
828                preferred_width: None,
829                min_content_height: min_main,
830                max_content_height: max_main,
831                preferred_height: None,
832            })
833        }
834    }
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    fn calculate_table_intrinsic_sizes(
840        &mut self,
841        tree: &LayoutTree,
842        node_index: usize,
843        child_intrinsics: &[(usize, IntrinsicSizes)],
844    ) -> Result<IntrinsicSizes> {
845        // Collect per-column min/max widths and total row heights.
846        // Table structure: table > row-group? > row > cell
847        let mut col_min: Vec<f32> = Vec::new();
848        let mut col_max: Vec<f32> = Vec::new();
849        let mut total_height = 0.0f32;
850
851        // Iterate rows — children may be row groups (thead/tbody/tfoot) or direct rows
852        let mut rows: Vec<usize> = Vec::new();
853        for &child_idx in tree.children(node_index) {
854            let child = match tree.get(child_idx) { Some(c) => c, None => continue };
855            match child.formatting_context {
856                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        for &row_idx in &rows {
872            let mut row_height = 0.0f32;
873            let mut col = 0usize;
874            for &cell_idx in tree.children(row_idx) {
875                let cell_intrinsic = child_intrinsics.iter().find(|(k, _)| k == &cell_idx).map(|(_, v)| *v)
876                    .unwrap_or_default();
877                // Also check if cell has IFC content we can measure
878                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                    self.calculate_ifc_root_intrinsic_sizes(tree, cell_idx)
883                        .unwrap_or_default()
884                };
885
886                // Add cell box-model extras
887                let cell_node = tree.get(cell_idx);
888                let (h_extras, v_extras) = if let Some(cn) = cell_node {
889                    let bp = cn.box_props.unpack();
890                    (bp.padding.left + bp.padding.right + bp.border.left + bp.border.right,
891                     bp.padding.top + bp.padding.bottom + bp.border.top + bp.border.bottom)
892                } else { (0.0, 0.0) };
893
894                let cell_min = cell_is.min_content_width + h_extras;
895                let cell_max = cell_is.max_content_width + h_extras;
896                let cell_h = cell_is.max_content_height + v_extras;
897
898                if col >= col_min.len() {
899                    col_min.push(cell_min);
900                    col_max.push(cell_max);
901                } else {
902                    col_min[col] = col_min[col].max(cell_min);
903                    col_max[col] = col_max[col].max(cell_max);
904                }
905                row_height = row_height.max(cell_h);
906                col += 1;
907            }
908            total_height += row_height;
909        }
910
911        let min_width: f32 = col_min.iter().sum();
912        let max_width: f32 = col_max.iter().sum();
913
914        Ok(IntrinsicSizes {
915            min_content_width: min_width,
916            max_content_width: max_width,
917            min_content_height: total_height,
918            max_content_height: total_height,
919            preferred_width: None,
920            preferred_height: None,
921        })
922    }
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.
940fn collect_inline_content_for_sizing<T: ParsedFontTrait>(
941    ctx: &mut LayoutContext<'_, T>,
942    tree: &LayoutTree,
943    ifc_root_index: usize,
944) -> Result<Vec<InlineContent>> {
945    ctx.debug_log(&format!(
946        "Collecting inline content from node {} for intrinsic sizing",
947        ifc_root_index
948    ));
949
950    let mut content = Vec::new();
951
952    // Recursively collect inline content from this node and its inline descendants
953    collect_inline_content_recursive(ctx, tree, ifc_root_index, &mut content)?;
954
955    ctx.debug_log(&format!(
956        "Collected {} inline content items from node {}",
957        content.len(),
958        ifc_root_index
959    ));
960
961    Ok(content)
962}
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
974fn collect_inline_content_recursive<T: ParsedFontTrait>(
975    ctx: &mut LayoutContext<'_, T>,
976    tree: &LayoutTree,
977    node_index: usize,
978    content: &mut Vec<InlineContent>,
979) -> Result<()> {
980    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    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    if let Some(text) = extract_text_from_node(ctx.styled_dom, dom_id) {
991        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        ctx.debug_log(&format!("Found text in node {}: '{}'", node_index, text));
993        // Use split_text_for_whitespace to correctly handle white-space: pre with \n
994        let text_items = split_text_for_whitespace(
995            ctx.styled_dom,
996            dom_id,
997            &text,
998            style_props,
999        );
1000        content.extend(text_items);
1001    }
1002
1003    // CRITICAL: Also check DOM children for text nodes!
1004    // Text nodes are often not represented as separate layout nodes.
1005    // However, we must SKIP children that already have a layout tree entry,
1006    // because those will be handled by process_layout_children() below.
1007    // Without this guard, text nodes present in both DOM and layout tree
1008    // get collected twice, causing inline-block containers to be ~2x too wide.
1009    let node_hierarchy = &ctx.styled_dom.node_hierarchy.as_container();
1010    for child_id in dom_id.az_children(node_hierarchy) {
1011        // Skip DOM children that have layout tree nodes - they will be
1012        // processed via process_layout_children -> collect_inline_content_recursive
1013        if tree.dom_to_layout.contains_key(&child_id) {
1014            continue;
1015        }
1016        // Check if this DOM child is a text node
1017        let child_dom_node = &ctx.styled_dom.node_data.as_container()[child_id];
1018        if let NodeType::Text(text_data) = child_dom_node.get_node_type() {
1019            let text = text_data.as_str().to_string();
1020            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)));
1021            ctx.debug_log(&format!(
1022                "Found text in DOM child of node {}: '{}'",
1023                node_index, text
1024            ));
1025            // Use split_text_for_whitespace to correctly handle white-space: pre with \n
1026            let text_items = split_text_for_whitespace(
1027                ctx.styled_dom,
1028                child_id,
1029                &text,
1030                style_props,
1031            );
1032            content.extend(text_items);
1033        }
1034    }
1035
1036    process_layout_children(ctx, tree, node_index, content)
1037}
1038
1039/// Helper to process layout tree children for inline content collection
1040fn process_layout_children<T: ParsedFontTrait>(
1041    ctx: &mut LayoutContext<'_, T>,
1042    tree: &LayoutTree,
1043    node_index: usize,
1044    content: &mut Vec<InlineContent>,
1045) -> Result<()> {
1046    use azul_css::props::basic::SizeMetric;
1047    use azul_css::props::layout::{LayoutHeight, LayoutWidth};
1048
1049    // Process layout tree children (these are elements with layout properties)
1050    for &child_index in tree.children(node_index) {
1051        let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1052        let Some(child_dom_id) = child_node.dom_node_id else {
1053            continue;
1054        };
1055
1056        let display = get_display_property(ctx.styled_dom, Some(child_dom_id));
1057
1058        // CSS Sizing Level 3: Inline-level boxes participate in the IFC
1059        if display.unwrap_or_default() == LayoutDisplay::Inline {
1060            // Recursively collect content from inline children
1061            // This is CRITICAL for proper intrinsic width calculation!
1062            ctx.debug_log(&format!(
1063                "Recursing into inline child at node {}",
1064                child_index
1065            ));
1066            collect_inline_content_recursive(ctx, tree, child_index, content)?;
1067        } else {
1068            // Non-inline children are treated as atomic inline-level boxes
1069            // (e.g., inline-block, images, floats)
1070            // Their intrinsic size must have been calculated in the bottom-up pass
1071            let intrinsic_sizes = tree.warm(child_index).and_then(|w| w.intrinsic_sizes).unwrap_or_default();
1072
1073            // CSS 2.2 § 10.3.9: For inline-block elements with explicit CSS width/height,
1074            // use the CSS-defined values instead of intrinsic sizes.
1075            let node_state =
1076                &ctx.styled_dom.styled_nodes.as_container()[child_dom_id].styled_node_state;
1077            let css_width = get_css_width(ctx.styled_dom, child_dom_id, node_state);
1078            let css_height = get_css_height(ctx.styled_dom, child_dom_id, node_state);
1079
1080            // Resolve CSS width - use explicit value if set, otherwise fall back to intrinsic
1081            let used_width = match css_width {
1082                MultiValue::Exact(LayoutWidth::Px(px)) => {
1083                    // Convert PixelValue to f32
1084                    use azul_css::props::basic::pixel::{DEFAULT_FONT_SIZE, PT_TO_PX};
1085                    match px.metric {
1086                        SizeMetric::Px => px.number.get(),
1087                        SizeMetric::Pt => px.number.get() * PT_TO_PX,
1088                        SizeMetric::In => px.number.get() * 96.0,
1089                        SizeMetric::Cm => px.number.get() * 96.0 / 2.54,
1090                        SizeMetric::Mm => px.number.get() * 96.0 / 25.4,
1091                        SizeMetric::Em | SizeMetric::Rem => px.number.get() * DEFAULT_FONT_SIZE,
1092                        // +spec:containing-block:495930 - percentages in intrinsic sizing fall back to intrinsic contribution (css-sizing-3 §5.2.1)
1093                        // For percentages and viewport units, fall back to intrinsic
1094                        // +spec:containing-block:5246c0 - cyclic percentage: when containing block size depends on this box's intrinsic contribution, percentages fall back to intrinsic size
1095                        // +spec:containing-block:598124 - cyclic percentage contributions use intrinsic size
1096                        // +spec:height-calculation:ca9f19 - percentage-sized boxes use intrinsic size as contribution during intrinsic sizing
1097                        // +spec:width-calculation:7a384a - percentage-sized boxes behave as width:auto for intrinsic contributions (cyclic percentage)
1098                        _ => intrinsic_sizes.max_content_width,
1099                    }
1100                }
1101                MultiValue::Exact(LayoutWidth::MinContent) => intrinsic_sizes.min_content_width,
1102                MultiValue::Exact(LayoutWidth::MaxContent) => intrinsic_sizes.max_content_width,
1103                MultiValue::Exact(LayoutWidth::FitContent(_)) => {
1104                    // During intrinsic sizing, fit-content resolves to max-content
1105                    intrinsic_sizes.max_content_width
1106                }
1107                // For Auto or other values, use intrinsic size
1108                _ => intrinsic_sizes.max_content_width,
1109            };
1110
1111            // +spec:containing-block:5145c5 - percentage block-size ignored in content-sized containing blocks during intrinsic sizing
1112            // Resolve CSS height - use explicit value if set, otherwise fall back to intrinsic
1113            let used_height = match css_height {
1114                MultiValue::Exact(LayoutHeight::Px(px)) => {
1115                    use azul_css::props::basic::pixel::{DEFAULT_FONT_SIZE, PT_TO_PX};
1116                    match px.metric {
1117                        SizeMetric::Px => px.number.get(),
1118                        SizeMetric::Pt => px.number.get() * PT_TO_PX,
1119                        SizeMetric::In => px.number.get() * 96.0,
1120                        SizeMetric::Cm => px.number.get() * 96.0 / 2.54,
1121                        SizeMetric::Mm => px.number.get() * 96.0 / 25.4,
1122                        SizeMetric::Em | SizeMetric::Rem => px.number.get() * DEFAULT_FONT_SIZE,
1123                        // +spec:containing-block:7d5e79 - percentages behave as auto when containing block height is auto (cyclic percentage contribution)
1124                        // +spec:height-calculation:7d807b - css-sizing-3 §5.2.1: percentage heights behave as auto during intrinsic sizing (cyclic percentage contribution)
1125                        // Percentages and viewport units fall back to intrinsic (treated as auto)
1126                        _ => intrinsic_sizes.max_content_height,
1127                    }
1128                }
1129                // is equivalent to automatic size
1130                MultiValue::Exact(LayoutHeight::MinContent) => intrinsic_sizes.max_content_height,
1131                // is equivalent to automatic size
1132                MultiValue::Exact(LayoutHeight::MaxContent) => intrinsic_sizes.max_content_height,
1133                MultiValue::Exact(LayoutHeight::FitContent(_)) => intrinsic_sizes.max_content_height,
1134                _ => intrinsic_sizes.max_content_height,
1135            };
1136
1137            ctx.debug_log(&format!(
1138                "Found atomic inline child at node {}: display={:?}, intrinsic_width={}, used_width={}, css_width={:?}",
1139                child_index, display, intrinsic_sizes.max_content_width, used_width, css_width
1140            ));
1141
1142            // Represent as a rectangular shape with the resolved dimensions
1143            content.push(InlineContent::Shape(InlineShape {
1144                shape_def: ShapeDefinition::Rectangle {
1145                    size: crate::text3::cache::Size {
1146                        width: used_width,
1147                        height: used_height,
1148                    },
1149                    corner_radius: None,
1150                },
1151                fill: None,
1152                stroke: None,
1153                baseline_offset: used_height,
1154                alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, child_dom_id),
1155                source_node_id: Some(child_dom_id),
1156            }));
1157        }
1158    }
1159
1160    Ok(())
1161}
1162
1163// Keep old name as an alias for backward compatibility
1164pub fn collect_inline_content<T: ParsedFontTrait>(
1165    ctx: &mut LayoutContext<'_, T>,
1166    tree: &LayoutTree,
1167    ifc_root_index: usize,
1168) -> Result<Vec<InlineContent>> {
1169    collect_inline_content_for_sizing(ctx, tree, ifc_root_index)
1170}
1171
1172// +spec:height-calculation:1c899b - width and height properties specify the preferred size of the box
1173/// Calculates the used size of a single node based on its CSS properties and
1174/// the available space provided by its containing block.
1175///
1176/// // +spec:display-contents:71ccde - extrinsic sizing: size determined by context (containing block), not contents
1177///
1178/// This implementation correctly handles writing modes and percentage-based sizes
1179/// according to the CSS specification:
1180/// 1. `width` and `height` CSS properties are resolved to pixel values. Percentages are calculated
1181///    based on the containing block's PHYSICAL dimensions (`width` for `width`, `height` for
1182///    `height`), regardless of writing mode.
1183/// 2. The resolved physical `width` is then mapped to the node's logical CROSS size.
1184/// 3. The resolved physical `height` is then mapped to the node's logical MAIN size.
1185/// 4. A final `LogicalSize` is constructed from these logical dimensions.
1186// +spec:overflow:3c4f25 - auto box sizes: four auto-determined size types resolved here
1187// +spec:width-calculation:fb0629 - width/margin used values depend on box type, auto replaced by suitable value
1188pub fn calculate_used_size_for_node(
1189    styled_dom: &StyledDom,
1190    dom_id: Option<NodeId>,
1191    containing_block_size: LogicalSize,
1192    intrinsic: IntrinsicSizes,
1193    _box_props: &BoxProps,
1194    viewport_size: LogicalSize,
1195) -> Result<LogicalSize> {
1196    let Some(id) = dom_id else {
1197        // Anonymous boxes:
1198        // CSS 2.2 § 9.2.1.1: Anonymous boxes inherit from their enclosing box.
1199        // The inline dimension fills the containing block's inline size,
1200        // and the block dimension is auto (content-based).
1201        // In horizontal-tb: inline=width, block=height.
1202        // In vertical modes: inline=height, block=width.
1203        //
1204        // Since anonymous boxes don't have a DOM node, we default to horizontal-tb.
1205        // The parent's writing mode is already reflected in containing_block_size.
1206        return Ok(LogicalSize::new(
1207            containing_block_size.width,
1208            if intrinsic.max_content_height > 0.0 {
1209                intrinsic.max_content_height
1210            } else {
1211                // Auto height - will be resolved from content
1212                0.0
1213            },
1214        ));
1215    };
1216
1217    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
1218    let css_width = get_css_width(styled_dom, id, node_state);
1219    let css_height = get_css_height(styled_dom, id, node_state);
1220    let writing_mode = get_writing_mode(styled_dom, id, node_state);
1221    let display = get_display_property(styled_dom, Some(id));
1222    let position = get_position_type(styled_dom, dom_id);
1223
1224    // Construct the full WritingModeContext from resolved styles.
1225    // This determines how logical dimensions (inline/block) map to physical (width/height).
1226    let wm_ctx = WritingModeContext::new(
1227        writing_mode.unwrap_or_default(),
1228        get_direction_property(styled_dom, id, node_state).unwrap_or_default(),
1229        get_text_orientation_property(styled_dom, id, node_state).unwrap_or_default(),
1230    );
1231    let is_vertical = !wm_ctx.is_horizontal();
1232
1233    // +spec:display-property:06e0b1 - form controls (non-image) treated as non-replaced
1234    // Determine if this element is a replaced element (images, virtual views)
1235    let node_data = &styled_dom.node_data.as_container()[id];
1236    let is_replaced = matches!(node_data.get_node_type(), NodeType::Image(_))
1237        || node_data.is_virtual_view_node();
1238
1239    // +spec:width-calculation:79cdf8 - inline non-replaced: width property does not apply
1240    // +spec:width-calculation:972e86 - §10.3.1: width property does not apply to inline non-replaced elements
1241    // For inline non-replaced elements, override any explicit width to Auto.
1242    let css_width = if display.unwrap_or_default() == LayoutDisplay::Inline
1243        && !is_replaced
1244    {
1245        MultiValue::Exact(LayoutWidth::Auto)
1246    } else {
1247        css_width
1248    };
1249
1250    // +spec:box-model:1197a5 - height does not apply to non-replaced inline elements
1251    // +spec:display-property:9cb33d - height does not apply to inline boxes
1252    // +spec:height-calculation:c03717 - height does not apply to inline non-replaced elements
1253    // CSS 2.2 §10.6.1 / CSS Inline 3 §6.4: height property does not apply to
1254    // inline, non-replaced elements. Override any explicit height to Auto.
1255    let css_height = if display.unwrap_or_default() == LayoutDisplay::Inline
1256        && !is_replaced
1257    {
1258        MultiValue::Exact(LayoutHeight::Auto)
1259    } else {
1260        css_height
1261    };
1262
1263    // Remember if width/height were auto before consuming them
1264    let width_is_auto = css_width.is_auto() || matches!(&css_width, MultiValue::Exact(LayoutWidth::Auto));
1265    let height_is_auto = css_height.is_auto() || matches!(&css_height, MultiValue::Exact(LayoutHeight::Auto));
1266
1267    // +spec:intrinsic-sizing:9e1c9d - non-quantitative values (auto, min-content, max-content) are not influenced by box-sizing
1268    let width_is_quantitative = matches!(
1269        &css_width,
1270        MultiValue::Exact(LayoutWidth::Px(_) | LayoutWidth::FitContent(_) | LayoutWidth::Calc(_))
1271    );
1272    let height_is_quantitative = matches!(
1273        &css_height,
1274        MultiValue::Exact(LayoutHeight::Px(_) | LayoutHeight::FitContent(_) | LayoutHeight::Calc(_))
1275    );
1276
1277    // +spec:width-calculation:50d67a - automatic sizing concepts (width/height auto resolution)
1278    // +spec:width-calculation:564315 - §10.3 width calculation dispatch for all box types
1279    // Step 1: Resolve the CSS `width` property into a concrete pixel value.
1280    // CSS `width` always refers to the physical horizontal dimension, regardless of writing mode.
1281    // Percentage values resolve against the containing block's physical width.
1282    // In horizontal-tb: width = inline size. In vertical modes: width = block size.
1283    // The physical-to-logical mapping happens in Step 5 below.
1284    // Percentage values for `width` are resolved against the containing block's width.
1285    // +spec:width-calculation:febf0c - width/height "behaves as auto" when computed auto or percentage resolves against indefinite
1286    let resolved_width = match css_width.unwrap_or_default() {
1287        LayoutWidth::Auto => {
1288            // +spec:width-calculation:ed6a34 - auto width on replaced element uses intrinsic width
1289            // CSS 2.2 §10.3.2: If 'width' has a computed value of 'auto', and the element
1290            // has an intrinsic width, then that intrinsic width is the used value of 'width'.
1291            // +spec:replaced-elements:992ea5 - block-level replaced elements use inline replaced width rules
1292            // §10.3.4: "The used value of 'width' is determined as for inline replaced elements."
1293            // +spec:replaced-elements:36de3e - §10.3.2/§10.3.4: auto width for inline/block replaced elements uses intrinsic width
1294            // +spec:replaced-elements:b9a780 - §10.3.2: inline replaced auto width = intrinsic width (conditions resolved during intrinsic size calc)
1295            if is_replaced {
1296                // +spec:width-calculation:b41dbe - floating/inline replaced: auto width = intrinsic width
1297                // +spec:width-calculation:c62d35 - §10.3.2: auto width for replaced elements uses intrinsic width
1298                // +spec:width-calculation:d87ca4 - abs-replaced: auto width+height uses intrinsic width
1299                // For replaced elements (inline or block-level), auto width = intrinsic width.
1300                // The intrinsic sizes were already computed with the 300px fallback per §10.3.2.
1301                intrinsic.max_content_width
1302            }
1303            // +spec:intrinsic-sizing:560697 - shrink-to-fit = clamp(min-content, stretch-fit, max-content)
1304            else if get_float(styled_dom, id, node_state).unwrap_or(LayoutFloat::None) != LayoutFloat::None {
1305                // +spec:width-calculation:8d7047 - shrink-to-fit width per CSS2.1§10.3.5
1306                // +spec:width-calculation:0bb038 - shrink-to-fit for floating non-replaced elements (§10.3.5)
1307                // shrink-to-fit = min(max(preferred minimum width, available width), preferred width)
1308                // +spec:table-layout:93b13c - shrink-to-fit for floats, inline-blocks, table-cells;
1309                // orthogonal flows would require child block size as input (not yet implemented)
1310                // +spec:width-calculation:a6fd29 - shrink-to-fit width for floats: min(max(preferred minimum, available), preferred)
1311                // CSS 2.2 §10.3.5: For floats, auto width = shrink-to-fit
1312                let available_width = (containing_block_size.width
1313                    - _box_props.margin.left
1314                    - _box_props.margin.right
1315                    - _box_props.border.left
1316                    - _box_props.border.right
1317                    - _box_props.padding.left
1318                    - _box_props.padding.right)
1319                    .max(0.0);
1320                let preferred_minimum = intrinsic.min_content_width;
1321                let preferred = intrinsic.max_content_width;
1322                preferred_minimum.max(available_width).min(preferred).max(0.0)
1323            }
1324            else if matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed) {
1325                // +spec:intrinsic-sizing:12a531 - abspos auto size = fit-content (shrink-to-fit)
1326                // +spec:width-calculation:0bb038 - shrink-to-fit width for abs-pos non-replaced elements
1327                // §10.3.7: abs-pos elements with auto width use shrink-to-fit
1328                // +spec:intrinsic-sizing:087b57 - abspos automatic size is fit-content (shrink-to-fit)
1329                // +spec:width-calculation:1661b4 - abs-pos non-replaced auto width uses shrink-to-fit (§10.3.7)
1330                // shrink-to-fit = min(max(preferred_minimum, available), preferred)
1331                let available_width = (containing_block_size.width
1332                    - _box_props.margin.left
1333                    - _box_props.margin.right
1334                    - _box_props.border.left
1335                    - _box_props.border.right
1336                    - _box_props.padding.left
1337                    - _box_props.padding.right)
1338                    .max(0.0);
1339                let preferred_minimum = intrinsic.min_content_width;
1340                let preferred = intrinsic.max_content_width;
1341                preferred_minimum.max(available_width).min(preferred).max(0.0)
1342            } else {
1343            // +spec:width-calculation:472065 - orthogonal flow auto inline size: if this block
1344            // container establishes an orthogonal flow (child writing mode axis differs from
1345            // parent), its auto inline size should use the parent's block-axis size as available
1346            // space, falling back to the initial containing block size. Currently not implemented;
1347            // auto width always resolves against the containing block's width.
1348            // 'auto' width resolution depends on the display type.
1349            match display.unwrap_or_default() {
1350                LayoutDisplay::Block
1351                | LayoutDisplay::FlowRoot
1352                | LayoutDisplay::ListItem
1353                | LayoutDisplay::Flex
1354                | LayoutDisplay::Grid => {
1355                    // +spec:box-model:503ea3 - margin + border + padding + width = containing block width
1356                    // +spec:box-model:5ed651 - stretch fit: size minus margins (auto=0), border, padding, floored at 0
1357                    // +spec:box-model:33b951 - stretch-fit inline size: available space minus margins/border/padding, floored at zero
1358                    // +spec:box-model:30b4d0 - stretch fit: available size minus margins (auto as zero), border, padding, floored at zero
1359                    // +spec:width-calculation:e2c8f6 - auto width for non-replaced blocks in normal flow per CSS2.1§10.3.3
1360                    // For block-level non-replaced elements,
1361                    // 'auto' width fills the containing block (minus margins, borders, padding).
1362                    // CSS 2.2 §10.3.3: width = containing_block_width - margin_left -
1363                    // margin_right - border_left - border_right - padding_left - padding_right
1364                    // +spec:width-calculation:aef2da - auto width: other auto values become 0, width follows from constraint equality
1365                    let available_width = containing_block_size.width
1366                        - _box_props.margin.left
1367                        - _box_props.margin.right
1368                        - _box_props.border.left
1369                        - _box_props.border.right
1370                        - _box_props.padding.left
1371                        - _box_props.padding.right;
1372
1373                    available_width.max(0.0)
1374                }
1375                LayoutDisplay::InlineBlock | LayoutDisplay::InlineGrid | LayoutDisplay::InlineFlex => {
1376                    // +spec:width-calculation:c01de8 - inline-block auto width uses shrink-to-fit (§10.3.9)
1377                    // shrink-to-fit = min(max(preferred_minimum, available), preferred)
1378                    let available_width = (containing_block_size.width
1379                        - _box_props.margin.left
1380                        - _box_props.margin.right
1381                        - _box_props.border.left
1382                        - _box_props.border.right
1383                        - _box_props.padding.left
1384                        - _box_props.padding.right)
1385                        .max(0.0);
1386                    let preferred_minimum = intrinsic.min_content_width;
1387                    let preferred = intrinsic.max_content_width;
1388                    preferred_minimum.max(available_width).min(preferred).max(0.0)
1389                }
1390                LayoutDisplay::Inline => {
1391                    // For inline elements, 'auto' width is the intrinsic/max-content width
1392                    intrinsic.max_content_width
1393                }
1394                LayoutDisplay::Table | LayoutDisplay::InlineTable => intrinsic.max_content_width,
1395                // Table cells: during intrinsic measurement, intrinsic sizes
1396                // aren't known yet (0). Use containing block width so content
1397                // can expand and be measured. The table layout algorithm sets
1398                // the final cell width from computed column widths.
1399                LayoutDisplay::TableCell => {
1400                    if intrinsic.max_content_width > 0.0 {
1401                        intrinsic.max_content_width
1402                    } else {
1403                        (containing_block_size.width
1404                            - _box_props.margin.left
1405                            - _box_props.margin.right
1406                            - _box_props.border.left
1407                            - _box_props.border.right
1408                            - _box_props.padding.left
1409                            - _box_props.padding.right)
1410                            .max(0.0)
1411                    }
1412                }
1413                // Other display types use intrinsic sizing
1414                _ => intrinsic.max_content_width,
1415            }
1416            }
1417        }
1418        LayoutWidth::Px(px) => {
1419            // Resolve percentage or absolute pixel value
1420            use azul_css::props::basic::{
1421                pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
1422                SizeMetric,
1423            };
1424            let pixels_opt = match px.metric {
1425                SizeMetric::Px => Some(px.number.get()),
1426                SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
1427                SizeMetric::In => Some(px.number.get() * 96.0),
1428                SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
1429                SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
1430                SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
1431                SizeMetric::Vw => Some(px.number.get() / 100.0 * viewport_size.width),
1432                SizeMetric::Vh => Some(px.number.get() / 100.0 * viewport_size.height),
1433                SizeMetric::Vmin => Some(px.number.get() / 100.0 * viewport_size.width.min(viewport_size.height)),
1434                SizeMetric::Vmax => Some(px.number.get() / 100.0 * viewport_size.width.max(viewport_size.height)),
1435                SizeMetric::Percent => None,
1436            };
1437
1438            match pixels_opt {
1439                Some(pixels) => pixels,
1440                None => match px.to_percent() {
1441                    Some(p) => {
1442                        let result = resolve_percentage_with_box_model(
1443                            containing_block_size.width,
1444                            p.get(),
1445                            (_box_props.margin.left, _box_props.margin.right),
1446                            (_box_props.border.left, _box_props.border.right),
1447                            (_box_props.padding.left, _box_props.padding.right),
1448                        );
1449
1450                        result
1451                    }
1452                    None => intrinsic.max_content_width,
1453                },
1454            }
1455        }
1456        // +spec:intrinsic-sizing:069c75 - min-content, max-content, fit-content() sizing value keywords
1457        // +spec:intrinsic-sizing:1ce4fa - §3.2 min-content/max-content/fit-content() sizing values
1458        LayoutWidth::MinContent => intrinsic.min_content_width,
1459        LayoutWidth::MaxContent => intrinsic.max_content_width,
1460        // +spec:width-calculation:7b2128 - fit-content formula and non-negative inner size flooring (css-sizing-3 §3.2)
1461        // +spec:width-calculation:bf694a - min-content, max-content, fit-content() sizing values
1462        // css-sizing-3 §3.2: fit-content(<length-percentage>) = min(max-content, max(min-content, <length-percentage>))
1463        LayoutWidth::FitContent(px) => {
1464            use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
1465            let arg = super::calc::resolve_pixel_value_with_viewport(
1466                &px, containing_block_size.width, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
1467                viewport_size.width, viewport_size.height,
1468            );
1469            intrinsic.max_content_width.min(intrinsic.min_content_width.max(arg))
1470        }
1471        LayoutWidth::Calc(items) => {
1472            use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
1473            let em = get_element_font_size(styled_dom, id, node_state);
1474            let calc_ctx = super::calc::CalcResolveContext {
1475                items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
1476            };
1477            super::calc::evaluate_calc(&calc_ctx, containing_block_size.width)
1478        }
1479    };
1480    // css-sizing-3: "the used value is floored to preserve a non-negative inner size"
1481    let resolved_width = resolved_width.max(0.0);
1482
1483    // +spec:height-calculation:7880e3 - Distinction between box types for height/margin calculation
1484    // +spec:height-calculation:753d8d - Height calculation for various box types (§10.6)
1485    // +spec:positioning:d5184e - percentage height resolved against containing block height
1486    // +spec:height-calculation:6a6cac - §10.5 content height resolution (auto, length, percentage)
1487    // +spec:height-calculation:d398e4 - §10.5/10.6 height property resolution for different box types
1488    // Step 2: Resolve the CSS `height` property into a concrete pixel value.
1489    // CSS `height` always refers to the physical vertical dimension, regardless of writing mode.
1490    // Percentage values resolve against the containing block's physical height.
1491    // In horizontal-tb: height = block size. In vertical modes: height = inline size.
1492    // The physical-to-logical mapping happens in Step 5 below.
1493    // Percentage values for `height` are resolved against the containing block's height.
1494    // +spec:height-calculation:0b5b0a - abs-pos replaced elements use intrinsic height for auto
1495    let resolved_height = match css_height.unwrap_or_default() {
1496        LayoutHeight::Auto => {
1497            // +spec:width-calculation:be5eb1 - auto height means available block space is infinite (unconstrained)
1498            // +spec:replaced-elements:994ac6 - §10.6.2: auto height for replaced elements uses intrinsic height or (used width)/ratio
1499            //
1500            // For block-level non-replaced containers in normal flow, CSS 2.2 §10.6.3
1501            // says auto height is resolved from children after layout. We return 0.0
1502            // as a placeholder; `apply_content_based_height` (cache.rs) overwrites it
1503            // with the laid-out content size. Reading `intrinsic.max_content_height`
1504            // here is unsafe: when the intrinsic pass short-circuits (e.g. a non-STF
1505            // subtree whose intrinsics are never consumed), that field is zero anyway
1506            // — so any caller that "trusts" the pre-layout value is depending on an
1507            // estimate that isn't guaranteed to exist.
1508            //
1509            // Shrink-to-fit contexts (inline-block, float, abspos, table/table-cell)
1510            // genuinely need intrinsic for width sizing; auto-height for those is
1511            // still driven by content, but we keep the intrinsic fallback for
1512            // backwards compatibility with the existing paths.
1513            match display.unwrap_or_default() {
1514                LayoutDisplay::Block
1515                | LayoutDisplay::FlowRoot
1516                | LayoutDisplay::ListItem
1517                | LayoutDisplay::Flex
1518                | LayoutDisplay::Grid => 0.0,
1519                // Inline: height property does not apply (§10.6.1), handled earlier
1520                // via css_height override, but be explicit anyway.
1521                LayoutDisplay::Inline => 0.0,
1522                // Shrink-to-fit and intrinsically-sized: keep using intrinsic pre-layout.
1523                _ => intrinsic.max_content_height,
1524            }
1525        }
1526        LayoutHeight::Px(px) => {
1527            // Resolve percentage or absolute pixel value
1528            use azul_css::props::basic::{
1529                pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
1530                SizeMetric,
1531            };
1532            let pixels_opt = match px.metric {
1533                SizeMetric::Px => Some(px.number.get()),
1534                SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
1535                SizeMetric::In => Some(px.number.get() * 96.0),
1536                SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
1537                SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
1538                SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
1539                SizeMetric::Vw => Some(px.number.get() / 100.0 * viewport_size.width),
1540                SizeMetric::Vh => Some(px.number.get() / 100.0 * viewport_size.height),
1541                SizeMetric::Vmin => Some(px.number.get() / 100.0 * viewport_size.width.min(viewport_size.height)),
1542                SizeMetric::Vmax => Some(px.number.get() / 100.0 * viewport_size.width.max(viewport_size.height)),
1543                SizeMetric::Percent => None,
1544            };
1545
1546            match pixels_opt {
1547                Some(pixels) => pixels,
1548                // +spec:height-calculation:37bc8c - percentage heights resolve against definite containing block height
1549                None => match px.to_percent() {
1550                    Some(p) => resolve_percentage_with_box_model(
1551                        containing_block_size.height,
1552                        p.get(),
1553                        (_box_props.margin.top, _box_props.margin.bottom),
1554                        (_box_props.border.top, _box_props.border.bottom),
1555                        (_box_props.padding.top, _box_props.padding.bottom),
1556                    ),
1557                    None => intrinsic.max_content_height,
1558                },
1559            }
1560        }
1561        // equivalent to automatic size (not min_content_height which is height at min-content width)
1562        LayoutHeight::MinContent => intrinsic.max_content_height,
1563        // equivalent to automatic size
1564        LayoutHeight::MaxContent => intrinsic.max_content_height,
1565        // css-sizing-3 §3.2: fit-content(<length-percentage>) = min(max-content, max(min-content, <length-percentage>))
1566        // For block axis, both min-content and max-content equal auto height
1567        LayoutHeight::FitContent(px) => {
1568            use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
1569            let arg = super::calc::resolve_pixel_value_with_viewport(
1570                &px, containing_block_size.height, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE,
1571                viewport_size.width, viewport_size.height,
1572            );
1573            let auto_height = intrinsic.max_content_height;
1574            auto_height.min(auto_height.max(arg))
1575        }
1576        LayoutHeight::Calc(items) => {
1577            use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
1578            let em = get_element_font_size(styled_dom, id, node_state);
1579            let calc_ctx = super::calc::CalcResolveContext {
1580                items, em_size: em, rem_size: DEFAULT_FONT_SIZE,
1581            };
1582            super::calc::evaluate_calc(&calc_ctx, containing_block_size.height)
1583        }
1584    };
1585    // css-sizing-3: "the used value is floored to preserve a non-negative inner size"
1586    let resolved_height = resolved_height.max(0.0);
1587
1588    // +spec:replaced-elements:5a85ce - abs-pos replaced: derive auto width from height × intrinsic ratio
1589    // +spec:replaced-elements:aedb26 - abs-pos replaced: both auto, ratio but no intrinsic w/h → block constraint
1590    // CSS Position 3 §6.2 (abs-replaced-width): For absolutely positioned replaced elements,
1591    // if width is auto and the element has an intrinsic ratio, width may be derived from height.
1592    let (resolved_width, resolved_height) = if is_replaced
1593        && width_is_auto
1594        && matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed)
1595    {
1596        let has_intrinsic_width = intrinsic.preferred_width.map_or(false, |w| w > 0.0);
1597        let has_intrinsic_height = intrinsic.preferred_height.map_or(false, |h| h > 0.0);
1598        let intrinsic_ratio = match (intrinsic.preferred_width, intrinsic.preferred_height) {
1599            (Some(iw), Some(ih)) if ih > 0.0 => Some(iw / ih),
1600            _ => None,
1601        };
1602
1603        if let Some(ratio) = intrinsic_ratio {
1604            if height_is_auto && !has_intrinsic_width && has_intrinsic_height {
1605                // §6.2 case: both auto, no intrinsic width, has intrinsic height + ratio
1606                // → width = used height × ratio
1607                (resolved_height * ratio, resolved_height)
1608            } else if !height_is_auto {
1609                // §6.2 case: width auto, height not auto, has intrinsic ratio
1610                // → width = used height × ratio
1611                (resolved_height * ratio, resolved_height)
1612            } else if height_is_auto && !has_intrinsic_width && !has_intrinsic_height {
1613                // §6.2 case: both auto, has ratio but no intrinsic width or height
1614                // → use block-level non-replaced constraint equation for width
1615                let block_width = (containing_block_size.width
1616                    - _box_props.margin.left
1617                    - _box_props.margin.right
1618                    - _box_props.border.left
1619                    - _box_props.border.right
1620                    - _box_props.padding.left
1621                    - _box_props.padding.right)
1622                    .max(0.0);
1623                (block_width, block_width / ratio)
1624            } else {
1625                (resolved_width, resolved_height)
1626            }
1627        } else {
1628            (resolved_width, resolved_height)
1629        }
1630    } else {
1631        (resolved_width, resolved_height)
1632    };
1633
1634    // +spec:min-max-sizing:58869e - sizing properties width/height/min-width/min-height/max-width/max-height applied here
1635    // +spec:min-max-sizing:2e2414 - max-width/max-height specify maximum box dimensions, applied here
1636    // +spec:min-max-sizing:73f51a - tentative width clamped by max-width then min-width per §10.4
1637    // +spec:min-max-sizing:e98c4e - preferred size clamped by min/max, box-sizing handled
1638    // Step 3: Apply min/max constraints (CSS 2.2 § 10.4 and § 10.7)
1639    // "The tentative used width is calculated (without 'min-width' and 'max-width')
1640    // ...If the tentative used width is greater than 'max-width', the rules above are
1641    // applied again using the computed value of 'max-width' as the computed value for 'width'.
1642    // If the resulting width is smaller than 'min-width', the rules above are applied again
1643    // using the value of 'min-width' as the computed value for 'width'."
1644
1645    // use the constraint violation table to coordinate width+height together;
1646    // for non-replaced elements, apply width and height constraints independently
1647    let has_intrinsic_ratio = intrinsic.preferred_width.is_some()
1648        && intrinsic.preferred_height.is_some()
1649        && intrinsic.preferred_width.unwrap_or(0.0) > 0.0
1650        && intrinsic.preferred_height.unwrap_or(0.0) > 0.0;
1651
1652    // +spec:margin-collapsing:840eb6 - aspect ratio transfers size constraints across dimensions
1653    let (constrained_width, constrained_height) = if has_intrinsic_ratio {
1654        // +spec:width-calculation:ef71c4 - replaced elements with both width/height auto use constraint violation table
1655        // Replaced element with intrinsic ratio: use §10.4 constraint violation table
1656        apply_constraint_violation_table(
1657            styled_dom,
1658            id,
1659            node_state,
1660            resolved_width,
1661            resolved_height,
1662            containing_block_size.width,
1663            containing_block_size.height,
1664            _box_props,
1665        )
1666    } else {
1667        // Non-replaced element: apply width and height constraints independently
1668        let cw = apply_width_constraints(
1669            styled_dom,
1670            id,
1671            node_state,
1672            resolved_width,
1673            containing_block_size.width,
1674            _box_props,
1675        );
1676
1677        let ch = apply_height_constraints(
1678            styled_dom,
1679            id,
1680            node_state,
1681            resolved_height,
1682            containing_block_size.height,
1683            _box_props,
1684        );
1685        (cw, ch)
1686    };
1687
1688    // +spec:box-model:cc170b - box-sizing: border-box includes padding+border in specified size; content-box adds them outside; content size floored at zero
1689    // +spec:box-model:d9d797 - box-sizing: content-box vs border-box dimension interpretation
1690    // +spec:box-model:e2a773 - box-sizing: border-box includes padding+border in width/height; content-box adds them outside
1691    // +spec:box-sizing:8159a8 - box-sizing property indicates whether content-box or border-box is measured
1692    // +spec:box-sizing:b0ff05 - border-box sets border-box to specified size, content-box calculated from it
1693    // +spec:box-sizing:aefeb2 - box-sizing: content-box vs border-box width/height interpretation
1694    // +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
1695    // Step 4: Convert to border-box dimensions, respecting box-sizing property
1696    // CSS box-sizing:
1697    // - content-box (default): width/height set content size, border+padding are added
1698    // - border-box: width/height set border-box size, border+padding are included
1699    let box_sizing = match get_css_box_sizing(styled_dom, id, node_state) {
1700        MultiValue::Exact(bs) => bs,
1701        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
1702            azul_css::props::layout::LayoutBoxSizing::ContentBox
1703        }
1704    };
1705
1706    let (border_box_width, border_box_height) = match box_sizing {
1707        azul_css::props::layout::LayoutBoxSizing::BorderBox => {
1708            // +spec:box-sizing:cdfe09 - box-sizing: border-box makes width/height set the border box
1709            // +spec:box-sizing:3ba6d3 - content-box floors at 0px, so border-box can't be less than padding+border
1710            let min_border_box_w = _box_props.padding.left
1711                + _box_props.padding.right
1712                + _box_props.border.left
1713                + _box_props.border.right;
1714            let min_border_box_h = _box_props.padding.top
1715                + _box_props.padding.bottom
1716                + _box_props.border.top
1717                + _box_props.border.bottom;
1718            // +spec:box-model:4f423b - used values refer to the border box when box-sizing: border-box
1719            // border-box: The width/height values already include border and padding
1720            // CSS Box Sizing Level 3: "the specified width and height (and respective min/max
1721            // properties) on this element determine the border box of the element"
1722            // However, non-quantitative values (auto, min-content, max-content) are not
1723            // influenced by box-sizing, so they still need border+padding added.
1724            // Floor: content-box cannot go negative, so border-box >= padding+border
1725            let bw = if width_is_quantitative {
1726                constrained_width.max(min_border_box_w)
1727            } else {
1728                constrained_width
1729                    + _box_props.padding.left
1730                    + _box_props.padding.right
1731                    + _box_props.border.left
1732                    + _box_props.border.right
1733            };
1734            let bh = if height_is_quantitative {
1735                constrained_height.max(min_border_box_h)
1736            } else {
1737                constrained_height
1738                    + _box_props.padding.top
1739                    + _box_props.padding.bottom
1740                    + _box_props.border.top
1741                    + _box_props.border.bottom
1742            };
1743            (bw, bh)
1744        }
1745        azul_css::props::layout::LayoutBoxSizing::ContentBox => {
1746            // +spec:box-sizing:fead70 - content-box: width/height set content size, border+padding added outside
1747            let border_box_width = constrained_width
1748                + _box_props.padding.left
1749                + _box_props.padding.right
1750                + _box_props.border.left
1751                + _box_props.border.right;
1752            let border_box_height = constrained_height
1753                + _box_props.padding.top
1754                + _box_props.padding.bottom
1755                + _box_props.border.top
1756                + _box_props.border.bottom;
1757            (border_box_width, border_box_height)
1758        }
1759    };
1760
1761    // +spec:block-formatting-context:c6fb58 - vertical writing modes swap layout dimensions
1762    // +spec:min-max-sizing:d97870 - width/height/min/max refer to physical dimensions; layout rules are logical
1763    // Step 5: Map the resolved physical dimensions to logical dimensions.
1764    //
1765    // CSS Writing Modes Level 4:
1766    // - In horizontal-tb: width = inline (cross) size, height = block (main) size.
1767    // - In vertical-rl/lr: width = block (main) size, height = inline (cross) size.
1768    //
1769    // `from_main_cross` handles this mapping: given (main, cross) and writing mode,
1770    // it produces the correct LogicalSize with physical (width, height).
1771    let (main_size, cross_size) = if is_vertical {
1772        // Vertical writing mode: width is the block (main) dimension,
1773        // height is the inline (cross) dimension.
1774        (border_box_width, border_box_height)
1775    } else {
1776        // Horizontal writing mode (default): width is cross, height is main.
1777        (border_box_height, border_box_width)
1778    };
1779
1780    // Step 6: Construct the final LogicalSize from the logical dimensions.
1781    // +spec:min-max-sizing:2f66a6 - direction-dependent layout rules abstracted to logical start/end via writing mode
1782    let result =
1783        LogicalSize::from_main_cross(main_size, cross_size, writing_mode.unwrap_or_default());
1784
1785    Ok(result)
1786}
1787
1788// +spec:min-max-sizing:b02ebc - sizing properties min-width/max-width/min-height/max-height and preferred aspect ratio
1789// +spec:replaced-elements:740f3e - constraint violation table for replaced elements with intrinsic ratio and both width/height auto
1790// +spec:min-max-sizing:939f2c - use min-width/min-height <length> with aspect ratio for replaced elements
1791// with intrinsic ratios. Implements all 10 cases from the spec table, coordinating
1792// +spec:min-max-sizing:07620d - CSS 2.2 §10.4 constraint violation table for replaced elements with intrinsic ratios
1793// Implements all 11 cases from the spec table, coordinating
1794// width and height together to preserve the aspect ratio while respecting min/max constraints.
1795fn apply_constraint_violation_table(
1796    styled_dom: &StyledDom,
1797    id: NodeId,
1798    node_state: &StyledNodeState,
1799    w: f32,  // tentative width (ignoring min/max)
1800    h: f32,  // tentative height (ignoring min/max)
1801    containing_block_width: f32,
1802    containing_block_height: f32,
1803    box_props: &BoxProps,
1804) -> (f32, f32) {
1805    use azul_css::props::basic::{
1806        pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
1807        SizeMetric,
1808    };
1809    use crate::solver3::getters::{
1810        get_css_min_width, get_css_max_width, get_css_min_height, get_css_max_height, MultiValue,
1811    };
1812
1813    // Helper to resolve a pixel value to f32
1814    fn resolve_px(px: &azul_css::props::basic::pixel::PixelValue, containing: f32, box_props: &BoxProps, is_horizontal: bool) -> Option<f32> {
1815        let pixels_opt = match px.metric {
1816            SizeMetric::Px => Some(px.number.get()),
1817            SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
1818            SizeMetric::In => Some(px.number.get() * 96.0),
1819            SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
1820            SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
1821            SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
1822            SizeMetric::Percent => None,
1823            _ => None,
1824        };
1825        match pixels_opt {
1826            Some(v) => Some(v),
1827            None => {
1828                px.to_percent().map(|p| {
1829                    let (m1, m2, b1, b2, p1, p2) = if is_horizontal {
1830                        (box_props.margin.left, box_props.margin.right,
1831                         box_props.border.left, box_props.border.right,
1832                         box_props.padding.left, box_props.padding.right)
1833                    } else {
1834                        (box_props.margin.top, box_props.margin.bottom,
1835                         box_props.border.top, box_props.border.bottom,
1836                         box_props.padding.top, box_props.padding.bottom)
1837                    };
1838                    resolve_percentage_with_box_model(containing, p.get(), (m1, m2), (b1, b2), (p1, p2))
1839                })
1840            }
1841        }
1842    }
1843
1844    // +spec:min-max-sizing:92ab8d - constraint violation table for replaced elements with intrinsic ratio (cyclic percentage contributions use auto fallback)
1845    // +spec:min-max-sizing:ad8605 - min-height/max-height interact with percentage heights; percentages behave as auto in intrinsic contribution calc
1846
1847    // +spec:positioning:c0af55 - automatic minimum size of abspos box is always zero (default 0.0)
1848    // Resolve min-width (default 0)
1849    let min_w = match get_css_min_width(styled_dom, id, node_state) {
1850        MultiValue::Exact(mw) => resolve_px(&mw.inner, containing_block_width, box_props, true).unwrap_or(0.0),
1851        _ => 0.0,
1852    };
1853
1854    // Resolve max-width (default infinity)
1855    let max_w = match get_css_max_width(styled_dom, id, node_state) {
1856        MultiValue::Exact(mw) => {
1857            if mw.inner.number.get() >= core::f32::MAX - 1.0 {
1858                f32::MAX
1859            } else {
1860                resolve_px(&mw.inner, containing_block_width, box_props, true).unwrap_or(f32::MAX)
1861            }
1862        }
1863        _ => f32::MAX,
1864    };
1865
1866    // Resolve min-height (default 0)
1867    let min_h = match get_css_min_height(styled_dom, id, node_state) {
1868        MultiValue::Exact(mh) => resolve_px(&mh.inner, containing_block_height, box_props, false).unwrap_or(0.0),
1869        _ => 0.0,
1870    };
1871
1872    // Resolve max-height (default infinity)
1873    let max_h = match get_css_max_height(styled_dom, id, node_state) {
1874        MultiValue::Exact(mh) => {
1875            if mh.inner.number.get() >= core::f32::MAX - 1.0 {
1876                f32::MAX
1877            } else {
1878                resolve_px(&mh.inner, containing_block_height, box_props, false).unwrap_or(f32::MAX)
1879            }
1880        }
1881        _ => f32::MAX,
1882    };
1883
1884    // max(min, max) so that min ≤ max holds true."
1885    let max_w = max_w.max(min_w);
1886    let max_h = max_h.max(min_h);
1887
1888    // Guard against zero dimensions (avoid division by zero)
1889    if w <= 0.0 || h <= 0.0 {
1890        return (w.max(min_w).min(max_w), h.max(min_h).min(max_h));
1891    }
1892
1893    let w_over = w > max_w;
1894    let w_under = w < min_w;
1895    let h_over = h > max_h;
1896    let h_under = h < min_h;
1897
1898    // +spec:min-max-sizing:713560 - constraint violation table for replaced elements with intrinsic ratio
1899    match (w_over, w_under, h_over, h_under) {
1900        // Row 1: no constraint violation
1901        (false, false, false, false) => (w, h),
1902
1903        // Row 2: w > max-width only
1904        (true, false, false, false) => {
1905            (max_w, (max_w * h / w).max(min_h))
1906        }
1907
1908        // Row 3: w < min-width only
1909        (false, true, false, false) => {
1910            (min_w, (min_w * h / w).min(max_h))
1911        }
1912
1913        // Row 4: h > max-height only
1914        (false, false, true, false) => {
1915            ((max_h * w / h).max(min_w), max_h)
1916        }
1917
1918        // Row 5: h < min-height only
1919        (false, false, false, true) => {
1920            ((min_h * w / h).min(max_w), min_h)
1921        }
1922
1923        // Row 6+7: (w > max-width) and (h > max-height)
1924        (true, false, true, false) => {
1925            if max_w / w <= max_h / h {
1926                (max_w, (max_w * h / w).max(min_h))
1927            } else {
1928                ((max_h * w / h).max(min_w), max_h)
1929            }
1930        }
1931
1932        // Row 8+9: (w < min-width) and (h < min-height)
1933        (false, true, false, true) => {
1934            if min_w / w <= min_h / h {
1935                ((min_h * w / h).min(max_w), min_h)
1936            } else {
1937                (min_w, (min_w * h / w).min(max_h))
1938            }
1939        }
1940
1941        // Row 10: (w < min-width) and (h > max-height)
1942        (false, true, true, false) => (min_w, max_h),
1943
1944        // Row 11: (w > max-width) and (h < min-height)
1945        (true, false, false, true) => (max_w, min_h),
1946
1947        // Fallback (impossible combinations like w_over && w_under)
1948        _ => (w.max(min_w).min(max_w), h.max(min_h).min(max_h)),
1949    }
1950}
1951
1952// +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
1953// +spec:min-max-sizing:12667d - width/height/min-width/min-height/max-width/max-height properties from CSS Sizing 3
1954/// +spec:min-max-sizing:205e9e - intrinsic size constraints (min/max-content contributions, min/max sizing properties)
1955// +spec:min-max-sizing:cac146 - min-width/min-height specify minimum box dimensions; max overridden by min
1956// +spec:width-calculation:e77d58 - min/max-width clamping algorithm per CSS 2.2 § 10.4
1957// +spec:width-calculation:1d63f0 - min-width/max-width property resolution and value meanings
1958/// Apply min-width and max-width constraints to tentative width
1959/// Per CSS 2.2 § 10.4: min-width overrides max-width if min > max
1960fn apply_width_constraints(
1961    styled_dom: &StyledDom,
1962    id: NodeId,
1963    node_state: &StyledNodeState,
1964    tentative_width: f32,
1965    containing_block_width: f32,
1966    box_props: &BoxProps,
1967) -> f32 {
1968    use azul_css::props::basic::{
1969        pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
1970        SizeMetric,
1971    };
1972
1973    use crate::solver3::getters::{get_css_max_width, get_css_min_width, MultiValue};
1974
1975    // +spec:display-property:0c55e5 - auto min-width resolves to 0 for CSS2 display types
1976    // Resolve min-width (default is 0)
1977    let min_width = match get_css_min_width(styled_dom, id, node_state) {
1978        MultiValue::Exact(mw) => {
1979            let px = &mw.inner;
1980            let pixels_opt = match px.metric {
1981                SizeMetric::Px => Some(px.number.get()),
1982                SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
1983                SizeMetric::In => Some(px.number.get() * 96.0),
1984                SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
1985                SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
1986                SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
1987                SizeMetric::Percent => None,
1988                _ => None,
1989            };
1990
1991            match pixels_opt {
1992                Some(pixels) => pixels,
1993                None => px
1994                    .to_percent()
1995                    .map(|p| {
1996                        resolve_percentage_with_box_model(
1997                            containing_block_width,
1998                            p.get(),
1999                            (box_props.margin.left, box_props.margin.right),
2000                            (box_props.border.left, box_props.border.right),
2001                            (box_props.padding.left, box_props.padding.right),
2002                        )
2003                    })
2004                    .unwrap_or(0.0),
2005            }
2006        }
2007        _ => 0.0,
2008    };
2009
2010    // Resolve max-width (default is infinity/none)
2011    let max_width = match get_css_max_width(styled_dom, id, node_state) {
2012        MultiValue::Exact(mw) => {
2013            let px = &mw.inner;
2014            // Check if it's the default "max" value (f32::MAX)
2015            if px.number.get() >= core::f32::MAX - 1.0 {
2016                None
2017            } else {
2018                let pixels_opt = match px.metric {
2019                    SizeMetric::Px => Some(px.number.get()),
2020                    SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
2021                    SizeMetric::In => Some(px.number.get() * 96.0),
2022                    SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
2023                    SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
2024                    SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
2025                    SizeMetric::Percent => None,
2026                    _ => None,
2027                };
2028
2029                match pixels_opt {
2030                    Some(pixels) => Some(pixels),
2031                    None => px.to_percent().map(|p| {
2032                        resolve_percentage_with_box_model(
2033                            containing_block_width,
2034                            p.get(),
2035                            (box_props.margin.left, box_props.margin.right),
2036                            (box_props.border.left, box_props.border.right),
2037                            (box_props.padding.left, box_props.padding.right),
2038                        )
2039                    }),
2040                }
2041            }
2042        }
2043        _ => None,
2044    };
2045
2046    // Apply constraints: max(min_width, min(tentative, max_width))
2047    // If min > max, min wins per CSS spec
2048    let mut result = tentative_width;
2049
2050    if let Some(max) = max_width {
2051        result = result.min(max);
2052    }
2053
2054    result = result.max(min_width);
2055
2056    result
2057}
2058
2059/// Apply min-height and max-height constraints to tentative height
2060/// Per CSS 2.2 § 10.7: min-height overrides max-height if min > max
2061// +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)
2062// +spec:height-calculation:982aaf - min-height/max-height constrain box heights to a range
2063// +spec:height-calculation:c6c33a - min-height and max-height property resolution and application
2064fn apply_height_constraints(
2065    styled_dom: &StyledDom,
2066    id: NodeId,
2067    node_state: &StyledNodeState,
2068    tentative_height: f32,
2069    containing_block_height: f32,
2070    box_props: &BoxProps,
2071) -> f32 {
2072    use azul_css::props::basic::{
2073        pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
2074        SizeMetric,
2075    };
2076
2077    use crate::solver3::getters::{get_css_max_height, get_css_min_height, MultiValue};
2078
2079    // for backwards-compat with CSS2 display types (block, inline, inline-block, table)
2080    // Resolve min-height (default is 0)
2081    let min_height = match get_css_min_height(styled_dom, id, node_state) {
2082        MultiValue::Exact(mh) => {
2083            let px = &mh.inner;
2084            let pixels_opt = match px.metric {
2085                SizeMetric::Px => Some(px.number.get()),
2086                SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
2087                SizeMetric::In => Some(px.number.get() * 96.0),
2088                SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
2089                SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
2090                SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
2091                SizeMetric::Percent => None,
2092                _ => None,
2093            };
2094
2095            match pixels_opt {
2096                Some(pixels) => pixels,
2097                None => px
2098                    .to_percent()
2099                    .map(|p| {
2100                        resolve_percentage_with_box_model(
2101                            containing_block_height,
2102                            p.get(),
2103                            (box_props.margin.top, box_props.margin.bottom),
2104                            (box_props.border.top, box_props.border.bottom),
2105                            (box_props.padding.top, box_props.padding.bottom),
2106                        )
2107                    })
2108                    .unwrap_or(0.0),
2109            }
2110        }
2111        _ => 0.0,
2112    };
2113
2114    // Resolve max-height (default is infinity/none)
2115    let max_height = match get_css_max_height(styled_dom, id, node_state) {
2116        MultiValue::Exact(mh) => {
2117            let px = &mh.inner;
2118            // Check if it's the default "max" value (f32::MAX)
2119            if px.number.get() >= core::f32::MAX - 1.0 {
2120                None
2121            } else {
2122                let pixels_opt = match px.metric {
2123                    SizeMetric::Px => Some(px.number.get()),
2124                    SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
2125                    SizeMetric::In => Some(px.number.get() * 96.0),
2126                    SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
2127                    SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
2128                    SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
2129                    SizeMetric::Percent => None,
2130                    _ => None,
2131                };
2132
2133                match pixels_opt {
2134                    Some(pixels) => Some(pixels),
2135                    None => px.to_percent().map(|p| {
2136                        resolve_percentage_with_box_model(
2137                            containing_block_height,
2138                            p.get(),
2139                            (box_props.margin.top, box_props.margin.bottom),
2140                            (box_props.border.top, box_props.border.bottom),
2141                            (box_props.padding.top, box_props.padding.bottom),
2142                        )
2143                    }),
2144                }
2145            }
2146        }
2147        _ => None,
2148    };
2149
2150    // +spec:height-calculation:297001 - min/max height constraint algorithm per CSS 2.2 §10.7
2151    // Apply constraints: max(min_height, min(tentative, max_height))
2152    // If min > max, min wins per CSS spec
2153    let mut result = tentative_height;
2154
2155    if let Some(max) = max_height {
2156        result = result.min(max);
2157    }
2158
2159    result = result.max(min_height);
2160
2161    result
2162}
2163
2164pub fn extract_text_from_node(styled_dom: &StyledDom, node_id: NodeId) -> Option<String> {
2165    match &styled_dom.node_data.as_container()[node_id].get_node_type() {
2166        NodeType::Text(text_data) => Some(text_data.as_str().to_string()),
2167        _ => None,
2168    }
2169}