Skip to main content

azul_layout/solver3/
sizing.rs

1//! solver3/sizing.rs
2//!
3//! Pass 2: Sizing calculations (intrinsic and used sizes)
4
5use std::{
6    collections::{BTreeMap, BTreeSet},
7    sync::Arc,
8};
9
10use azul_core::{
11    dom::{FormattingContext, NodeId, NodeType},
12    geom::LogicalSize,
13    resources::RendererResources,
14    styled_dom::{StyledDom, StyledNodeState},
15};
16use azul_css::{
17    css::CssPropertyValue,
18    props::{
19        basic::PixelValue,
20        layout::{LayoutDisplay, LayoutHeight, LayoutPosition, LayoutWidth, LayoutWritingMode},
21        property::{CssProperty, CssPropertyType},
22    },
23    LayoutDebugMessage,
24};
25use rust_fontconfig::FcFontCache;
26
27#[cfg(feature = "text_layout")]
28use crate::text3;
29use crate::{
30    font::parsed::ParsedFont,
31    font_traits::{
32        AvailableSpace, FontLoaderTrait, FontManager, ImageSource, InlineContent, InlineImage,
33        InlineShape, LayoutCache, LayoutFragment, ObjectFit, ParsedFontTrait, ShapeDefinition,
34        StyleProperties, StyledRun, UnifiedConstraints,
35    },
36    solver3::{
37        fc::split_text_for_whitespace,
38        geometry::{BoxProps, BoxSizing, IntrinsicSizes},
39        getters::{
40            get_css_box_sizing, get_css_height, get_css_width, get_display_property,
41            get_style_properties, get_writing_mode, MultiValue,
42        },
43        layout_tree::{AnonymousBoxType, LayoutNode, LayoutTree},
44        positioning::get_position_type,
45        LayoutContext, LayoutError, Result,
46    },
47};
48
49/// Resolves a percentage value against an available size, accounting for the CSS box model.
50///
51/// According to CSS 2.1 Section 10.2, percentages are resolved against the containing block's
52/// dimensions. However, when an element has margins, borders, or padding, these must be
53/// subtracted from the containing block size to get the "available" space that the percentage
54/// resolves against.
55///
56/// This is critical for correct layout calculations, especially when elements use percentage
57/// widths/heights combined with margins. Without this adjustment, elements overflow their
58/// containing blocks.
59///
60/// # Arguments
61///
62/// * `containing_block_dimension` - The full dimension of the containing block (width or height)
63/// * `percentage` - The percentage value to resolve (e.g., 100% = 1.0, 50% = 0.5)
64/// * `margins` - The two margins in the relevant axis (left+right for width, top+bottom for height)
65/// * `borders` - The two borders in the relevant axis
66/// * `paddings` - The two paddings in the relevant axis
67///
68/// # Returns
69///
70/// The resolved pixel value, which is:
71/// `percentage * (containing_block_dimension - margins - borders - paddings)`
72///
73/// The result is clamped to a minimum of 0.0 to prevent negative sizes.
74///
75/// # Example
76///
77/// ```text
78/// // Body element: width: 100%, margin: 20px
79/// // Containing block (html): 595px wide
80/// // Expected body width: 595 - 20 - 20 = 555px
81///
82/// let body_width = resolve_percentage_with_box_model(
83///     595.0,           // containing block width
84///     1.0,             // 100%
85///     (20.0, 20.0),    // left and right margins
86///     (0.0, 0.0),      // no borders
87///     (0.0, 0.0),      // no paddings
88/// );
89/// assert_eq!(body_width, 555.0);
90/// ```
91///
92/// # CSS Specification
93///
94/// From CSS 2.1 Section 10.2: "If the width is set to a percentage, it is calculated
95/// with respect to the width of the generated box's containing block."
96///
97/// The percentage is resolved against the containing block dimension directly.
98/// Margins, borders, and padding are NOT subtracted from the base for percentage
99/// resolution in content-box sizing. They may cause overflow if the total exceeds
100/// the containing block width.
101pub fn resolve_percentage_with_box_model(
102    containing_block_dimension: f32,
103    percentage: f32,
104    _margins: (f32, f32),
105    _borders: (f32, f32),
106    _paddings: (f32, f32),
107) -> f32 {
108    // CSS 2.1 Section 10.2: percentages resolve against containing block,
109    // not available space after margins/borders/padding
110    (containing_block_dimension * percentage).max(0.0)
111}
112
113/// Phase 2a: Calculate intrinsic sizes (bottom-up pass)
114pub fn calculate_intrinsic_sizes<T: ParsedFontTrait>(
115    ctx: &mut LayoutContext<'_, T>,
116    tree: &mut LayoutTree,
117    dirty_nodes: &BTreeSet<usize>,
118) -> Result<()> {
119    if dirty_nodes.is_empty() {
120        return Ok(());
121    }
122
123    ctx.debug_log("Starting intrinsic size calculation");
124    let mut calculator = IntrinsicSizeCalculator::new(ctx);
125    calculator.calculate_intrinsic_recursive(tree, tree.root)?;
126    ctx.debug_log("Finished intrinsic size calculation");
127    Ok(())
128}
129
130struct IntrinsicSizeCalculator<'a, 'b, T: ParsedFontTrait> {
131    ctx: &'a mut LayoutContext<'b, T>,
132    text_cache: LayoutCache,
133}
134
135impl<'a, 'b, T: ParsedFontTrait> IntrinsicSizeCalculator<'a, 'b, T> {
136    fn new(ctx: &'a mut LayoutContext<'b, T>) -> Self {
137        Self {
138            ctx,
139            text_cache: LayoutCache::new(),
140        }
141    }
142
143    fn calculate_intrinsic_recursive(
144        &mut self,
145        tree: &mut LayoutTree,
146        node_index: usize,
147    ) -> Result<IntrinsicSizes> {
148        static COUNTER: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
149        let count = COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
150        if count % 50 == 0 {}
151
152        let node = tree
153            .get(node_index)
154            .cloned()
155            .ok_or(LayoutError::InvalidTree)?;
156
157        // Out-of-flow elements do not contribute to their parent's intrinsic size.
158        let position = get_position_type(self.ctx.styled_dom, node.dom_node_id);
159        if position == LayoutPosition::Absolute || position == LayoutPosition::Fixed {
160            if let Some(n) = tree.get_mut(node_index) {
161                n.intrinsic_sizes = Some(IntrinsicSizes::default());
162            }
163            return Ok(IntrinsicSizes::default());
164        }
165
166        // First, calculate children's intrinsic sizes
167        let mut child_intrinsics = BTreeMap::new();
168        for &child_index in &node.children {
169            let child_intrinsic = self.calculate_intrinsic_recursive(tree, child_index)?;
170            child_intrinsics.insert(child_index, child_intrinsic);
171        }
172
173        // Then calculate this node's intrinsic size based on its children
174        let intrinsic = self.calculate_node_intrinsic_sizes(tree, node_index, &child_intrinsics)?;
175
176        if let Some(n) = tree.get_mut(node_index) {
177            n.intrinsic_sizes = Some(intrinsic);
178        }
179
180        Ok(intrinsic)
181    }
182
183    fn calculate_node_intrinsic_sizes(
184        &mut self,
185        tree: &LayoutTree,
186        node_index: usize,
187        child_intrinsics: &BTreeMap<usize, IntrinsicSizes>,
188    ) -> Result<IntrinsicSizes> {
189        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
190
191        // IFrames are replaced elements with a default intrinsic size of 300x150px
192        // (same as HTML <iframe> elements)
193        if let Some(dom_id) = node.dom_node_id {
194            let node_data = &self.ctx.styled_dom.node_data.as_container()[dom_id];
195            if node_data.is_iframe_node() {
196                return Ok(IntrinsicSizes {
197                    min_content_width: 300.0,
198                    max_content_width: 300.0,
199                    preferred_width: None, // Will be determined by CSS or flex-grow
200                    min_content_height: 150.0,
201                    max_content_height: 150.0,
202                    preferred_height: None, // Will be determined by CSS or flex-grow
203                });
204            }
205            
206            // Images are replaced elements - get intrinsic size from the ImageRef
207            if let NodeType::Image(image_ref) = node_data.get_node_type() {
208                let size = image_ref.get_size();
209                let width = if size.width > 0.0 { size.width } else { 100.0 };
210                let height = if size.height > 0.0 { size.height } else { 100.0 };
211                return Ok(IntrinsicSizes {
212                    min_content_width: width,
213                    max_content_width: width,
214                    preferred_width: Some(width),
215                    min_content_height: height,
216                    max_content_height: height,
217                    preferred_height: Some(height),
218                });
219            }
220        }
221
222        match node.formatting_context {
223            FormattingContext::Block { .. } => {
224                self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics)
225            }
226            FormattingContext::Inline => self.calculate_inline_intrinsic_sizes(tree, node_index),
227            FormattingContext::Table => {
228                self.calculate_table_intrinsic_sizes(tree, node_index, child_intrinsics)
229            }
230            _ => self.calculate_block_intrinsic_sizes(tree, node_index, child_intrinsics),
231        }
232    }
233
234    fn calculate_block_intrinsic_sizes(
235        &mut self,
236        tree: &LayoutTree,
237        node_index: usize,
238        child_intrinsics: &BTreeMap<usize, IntrinsicSizes>,
239    ) -> Result<IntrinsicSizes> {
240        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
241        let writing_mode = if let Some(dom_id) = node.dom_node_id {
242            let node_state =
243                &self.ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
244            get_writing_mode(self.ctx.styled_dom, dom_id, node_state).unwrap_or_default()
245        } else {
246            LayoutWritingMode::default()
247        };
248
249        // If there are no layout children but this block contains text content directly,
250        // we need to calculate intrinsic sizes based on the text
251        if child_intrinsics.is_empty() && node.dom_node_id.is_some() {
252            let dom_id = node.dom_node_id.unwrap();
253            let node_hierarchy = &self.ctx.styled_dom.node_hierarchy.as_container();
254
255            // Check if this node has DOM children with text
256            let has_text = dom_id.az_children(node_hierarchy).any(|child_id| {
257                let child_node_data = &self.ctx.styled_dom.node_data.as_container()[child_id];
258                matches!(child_node_data.get_node_type(), NodeType::Text(_))
259            });
260
261            if has_text {
262                self.ctx.debug_log(&format!(
263                    "Block node {} has no layout children but has text DOM children - calculating \
264                     as inline content",
265                    node_index
266                ));
267                // This block contains inline content (text), so calculate its intrinsic size
268                // using inline content measurement
269                return self.calculate_inline_intrinsic_sizes(tree, node_index);
270            }
271        }
272
273        let mut max_child_min_cross = 0.0f32;
274        let mut max_child_max_cross = 0.0f32;
275        let mut total_main_size = 0.0;
276
277        for &child_index in &node.children {
278            if let Some(child_intrinsic) = child_intrinsics.get(&child_index) {
279                let (child_min_cross, child_max_cross, child_main_size) = match writing_mode {
280                    LayoutWritingMode::HorizontalTb => (
281                        child_intrinsic.min_content_width,
282                        child_intrinsic.max_content_width,
283                        child_intrinsic.max_content_height,
284                    ),
285                    _ => (
286                        child_intrinsic.min_content_height,
287                        child_intrinsic.max_content_height,
288                        child_intrinsic.max_content_width,
289                    ),
290                };
291
292                max_child_min_cross = max_child_min_cross.max(child_min_cross);
293                max_child_max_cross = max_child_max_cross.max(child_max_cross);
294                total_main_size += child_main_size;
295            }
296        }
297
298        let (min_width, max_width, min_height, max_height) = match writing_mode {
299            LayoutWritingMode::HorizontalTb => (
300                max_child_min_cross,
301                max_child_max_cross,
302                total_main_size,
303                total_main_size,
304            ),
305            _ => (
306                total_main_size,
307                total_main_size,
308                max_child_min_cross,
309                max_child_max_cross,
310            ),
311        };
312
313        Ok(IntrinsicSizes {
314            min_content_width: min_width,
315            max_content_width: max_width,
316            preferred_width: None,
317            min_content_height: min_height,
318            max_content_height: max_height,
319            preferred_height: None,
320        })
321    }
322
323    fn calculate_inline_intrinsic_sizes(
324        &mut self,
325        tree: &LayoutTree,
326        node_index: usize,
327    ) -> Result<IntrinsicSizes> {
328        self.ctx.debug_log(&format!(
329            "Calculating inline intrinsic sizes for node {}",
330            node_index
331        ));
332
333        // This call is now valid because we added the function to fc.rs
334        let inline_content = collect_inline_content(&mut self.ctx, tree, node_index)?;
335
336        if inline_content.is_empty() {
337            self.ctx
338                .debug_log("No inline content found, returning default sizes");
339            return Ok(IntrinsicSizes::default());
340        }
341
342        self.ctx.debug_log(&format!(
343            "Found {} inline content items",
344            inline_content.len()
345        ));
346
347        // Layout with "min-content" constraints (effectively zero width).
348        // This forces all possible line breaks, giving the width of the longest unbreakable unit.
349        let min_fragments = vec![LayoutFragment {
350            id: "min".to_string(),
351            constraints: UnifiedConstraints {
352                available_width: AvailableSpace::MinContent,
353                ..Default::default()
354            },
355        }];
356
357        // Get pre-loaded fonts from font manager
358        let loaded_fonts = self.ctx.font_manager.get_loaded_fonts();
359
360        let min_layout = match self.text_cache.layout_flow(
361            &inline_content,
362            &[],
363            &min_fragments,
364            &self.ctx.font_manager.font_chain_cache,
365            &self.ctx.font_manager.fc_cache,
366            &loaded_fonts,
367            self.ctx.debug_messages,
368        ) {
369            Ok(layout) => layout,
370            Err(e) => {
371                self.ctx.debug_log(&format!(
372                    "Warning: Sizing failed during min-content layout: {:?}",
373                    e
374                ));
375                self.ctx
376                    .debug_log("Using fallback: returning default intrinsic sizes");
377                // Return reasonable defaults instead of crashing
378                return Ok(IntrinsicSizes {
379                    min_content_width: 100.0, // Arbitrary fallback width
380                    max_content_width: 300.0,
381                    preferred_width: None,
382                    min_content_height: 20.0, // Arbitrary fallback height
383                    max_content_height: 20.0,
384                    preferred_height: None,
385                });
386            }
387        };
388
389        // Layout with "max-content" constraints (infinite width).
390        // This produces a single, long line, giving the natural width of the content.
391        let max_fragments = vec![LayoutFragment {
392            id: "max".to_string(),
393            constraints: UnifiedConstraints {
394                available_width: AvailableSpace::MaxContent,
395                ..Default::default()
396            },
397        }];
398
399        let max_layout = match self.text_cache.layout_flow(
400            &inline_content,
401            &[],
402            &max_fragments,
403            &self.ctx.font_manager.font_chain_cache,
404            &self.ctx.font_manager.fc_cache,
405            &loaded_fonts,
406            self.ctx.debug_messages,
407        ) {
408            Ok(layout) => layout,
409            Err(e) => {
410                self.ctx.debug_log(&format!(
411                    "Warning: Sizing failed during max-content layout: {:?}",
412                    e
413                ));
414                self.ctx.debug_log("Using fallback from min-content layout");
415                // If max-content fails but min-content succeeded, use min as fallback
416                min_layout.clone()
417            }
418        };
419
420        let min_width = min_layout
421            .fragment_layouts
422            .get("min")
423            .map(|l| l.bounds().width)
424            .unwrap_or(0.0);
425
426        let max_width = max_layout
427            .fragment_layouts
428            .get("max")
429            .map(|l| l.bounds().width)
430            .unwrap_or(0.0);
431
432        // The height is typically calculated at the max_content_width.
433        let height = max_layout
434            .fragment_layouts
435            .get("max")
436            .map(|l| l.bounds().height)
437            .unwrap_or(0.0);
438
439        Ok(IntrinsicSizes {
440            min_content_width: min_width,
441            max_content_width: max_width,
442            preferred_width: None, // preferred_width comes from CSS, not content.
443            min_content_height: height, // Height can change with width, but this is a common model.
444            max_content_height: height,
445            preferred_height: None,
446        })
447    }
448
449    fn calculate_table_intrinsic_sizes(
450        &self,
451        _tree: &LayoutTree,
452        _node_index: usize,
453        _child_intrinsics: &BTreeMap<usize, IntrinsicSizes>,
454    ) -> Result<IntrinsicSizes> {
455        Ok(IntrinsicSizes::default())
456    }
457}
458
459/// Gathers all inline content for the intrinsic sizing pass.
460///
461/// This function recursively collects text and inline-level content according to
462/// CSS Sizing Level 3, Section 4.1: "Intrinsic Sizes"
463/// https://www.w3.org/TR/css-sizing-3/#intrinsic-sizes
464///
465/// For inline formatting contexts, we need to gather:
466/// 1. Text nodes (inline content)
467/// 2. Inline-level boxes (display: inline, inline-block, etc.)
468/// 3. Atomic inline-level elements (replaced elements like images)
469///
470/// The key difference from `collect_and_measure_inline_content` in fc.rs is that
471/// this version is used for intrinsic sizing (calculating min/max-content widths)
472/// before the actual layout pass, so it must recursively gather content from
473/// inline descendants without laying them out first.
474fn collect_inline_content_for_sizing<T: ParsedFontTrait>(
475    ctx: &mut LayoutContext<'_, T>,
476    tree: &LayoutTree,
477    ifc_root_index: usize,
478) -> Result<Vec<InlineContent>> {
479    ctx.debug_log(&format!(
480        "Collecting inline content from node {} for intrinsic sizing",
481        ifc_root_index
482    ));
483
484    let mut content = Vec::new();
485
486    // Recursively collect inline content from this node and its inline descendants
487    collect_inline_content_recursive(ctx, tree, ifc_root_index, &mut content)?;
488
489    ctx.debug_log(&format!(
490        "Collected {} inline content items from node {}",
491        content.len(),
492        ifc_root_index
493    ));
494
495    Ok(content)
496}
497
498/// Recursive helper for collecting inline content.
499///
500/// According to CSS Sizing Level 3, the intrinsic size of an inline formatting context
501/// is based on all inline-level content, including text in nested inline elements.
502///
503/// This function:
504/// - Collects text from the current node if it's a text node
505/// - Collects text from DOM children (text nodes may not be in layout tree)
506/// - Recursively collects from inline children (display: inline)
507/// - Treats non-inline children as atomic inline-level boxes
508fn collect_inline_content_recursive<T: ParsedFontTrait>(
509    ctx: &mut LayoutContext<'_, T>,
510    tree: &LayoutTree,
511    node_index: usize,
512    content: &mut Vec<InlineContent>,
513) -> Result<()> {
514    let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
515
516    // CRITICAL FIX: Text nodes may exist in the DOM but not as separate layout nodes!
517    // We need to check the DOM children for text content.
518    let Some(dom_id) = node.dom_node_id else {
519        // No DOM ID means this is a synthetic node, skip text extraction
520        return process_layout_children(ctx, tree, node, content);
521    };
522
523    // First check if THIS node is a text node
524    if let Some(text) = extract_text_from_node(ctx.styled_dom, dom_id) {
525        let style_props = Arc::new(get_style_properties(ctx.styled_dom, dom_id));
526        ctx.debug_log(&format!("Found text in node {}: '{}'", node_index, text));
527        // Use split_text_for_whitespace to correctly handle white-space: pre with \n
528        let text_items = split_text_for_whitespace(
529            ctx.styled_dom,
530            dom_id,
531            &text,
532            style_props,
533        );
534        content.extend(text_items);
535    }
536
537    // CRITICAL: Also check DOM children for text nodes!
538    // Text nodes are often not represented as separate layout nodes.
539    let node_hierarchy = &ctx.styled_dom.node_hierarchy.as_container();
540    for child_id in dom_id.az_children(node_hierarchy) {
541        // Check if this DOM child is a text node
542        let child_dom_node = &ctx.styled_dom.node_data.as_container()[child_id];
543        if let NodeType::Text(text_data) = child_dom_node.get_node_type() {
544            let text = text_data.as_str().to_string();
545            let style_props = Arc::new(get_style_properties(ctx.styled_dom, child_id));
546            ctx.debug_log(&format!(
547                "Found text in DOM child of node {}: '{}'",
548                node_index, text
549            ));
550            // Use split_text_for_whitespace to correctly handle white-space: pre with \n
551            let text_items = split_text_for_whitespace(
552                ctx.styled_dom,
553                child_id,
554                &text,
555                style_props,
556            );
557            content.extend(text_items);
558        }
559    }
560
561    process_layout_children(ctx, tree, node, content)
562}
563
564/// Helper to process layout tree children for inline content collection
565fn process_layout_children<T: ParsedFontTrait>(
566    ctx: &mut LayoutContext<'_, T>,
567    tree: &LayoutTree,
568    node: &LayoutNode,
569    content: &mut Vec<InlineContent>,
570) -> Result<()> {
571    use azul_css::props::basic::SizeMetric;
572    use azul_css::props::layout::{LayoutHeight, LayoutWidth};
573
574    // Process layout tree children (these are elements with layout properties)
575    for &child_index in &node.children {
576        let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
577        let Some(child_dom_id) = child_node.dom_node_id else {
578            continue;
579        };
580
581        let display = get_display_property(ctx.styled_dom, Some(child_dom_id));
582
583        // CSS Sizing Level 3: Inline-level boxes participate in the IFC
584        if display.unwrap_or_default() == LayoutDisplay::Inline {
585            // Recursively collect content from inline children
586            // This is CRITICAL for proper intrinsic width calculation!
587            ctx.debug_log(&format!(
588                "Recursing into inline child at node {}",
589                child_index
590            ));
591            collect_inline_content_recursive(ctx, tree, child_index, content)?;
592        } else {
593            // Non-inline children are treated as atomic inline-level boxes
594            // (e.g., inline-block, images, floats)
595            // Their intrinsic size must have been calculated in the bottom-up pass
596            let intrinsic_sizes = child_node.intrinsic_sizes.unwrap_or_default();
597
598            // CSS 2.2 § 10.3.9: For inline-block elements with explicit CSS width/height,
599            // use the CSS-defined values instead of intrinsic sizes.
600            let node_state =
601                &ctx.styled_dom.styled_nodes.as_container()[child_dom_id].styled_node_state;
602            let css_width = get_css_width(ctx.styled_dom, child_dom_id, node_state);
603            let css_height = get_css_height(ctx.styled_dom, child_dom_id, node_state);
604
605            // Resolve CSS width - use explicit value if set, otherwise fall back to intrinsic
606            let used_width = match css_width {
607                MultiValue::Exact(LayoutWidth::Px(px)) => {
608                    // Convert PixelValue to f32
609                    use azul_css::props::basic::pixel::{DEFAULT_FONT_SIZE, PT_TO_PX};
610                    match px.metric {
611                        SizeMetric::Px => px.number.get(),
612                        SizeMetric::Pt => px.number.get() * PT_TO_PX,
613                        SizeMetric::In => px.number.get() * 96.0,
614                        SizeMetric::Cm => px.number.get() * 96.0 / 2.54,
615                        SizeMetric::Mm => px.number.get() * 96.0 / 25.4,
616                        SizeMetric::Em | SizeMetric::Rem => px.number.get() * DEFAULT_FONT_SIZE,
617                        // For percentages and viewport units, fall back to intrinsic
618                        _ => intrinsic_sizes.max_content_width,
619                    }
620                }
621                MultiValue::Exact(LayoutWidth::MinContent) => intrinsic_sizes.min_content_width,
622                MultiValue::Exact(LayoutWidth::MaxContent) => intrinsic_sizes.max_content_width,
623                // For Auto or other values, use intrinsic size
624                _ => intrinsic_sizes.max_content_width,
625            };
626
627            // Resolve CSS height - use explicit value if set, otherwise fall back to intrinsic
628            let used_height = match css_height {
629                MultiValue::Exact(LayoutHeight::Px(px)) => {
630                    use azul_css::props::basic::pixel::{DEFAULT_FONT_SIZE, PT_TO_PX};
631                    match px.metric {
632                        SizeMetric::Px => px.number.get(),
633                        SizeMetric::Pt => px.number.get() * PT_TO_PX,
634                        SizeMetric::In => px.number.get() * 96.0,
635                        SizeMetric::Cm => px.number.get() * 96.0 / 2.54,
636                        SizeMetric::Mm => px.number.get() * 96.0 / 25.4,
637                        SizeMetric::Em | SizeMetric::Rem => px.number.get() * DEFAULT_FONT_SIZE,
638                        _ => intrinsic_sizes.max_content_height,
639                    }
640                }
641                MultiValue::Exact(LayoutHeight::MinContent) => intrinsic_sizes.min_content_height,
642                MultiValue::Exact(LayoutHeight::MaxContent) => intrinsic_sizes.max_content_height,
643                _ => intrinsic_sizes.max_content_height,
644            };
645
646            ctx.debug_log(&format!(
647                "Found atomic inline child at node {}: display={:?}, intrinsic_width={}, used_width={}, css_width={:?}",
648                child_index, display, intrinsic_sizes.max_content_width, used_width, css_width
649            ));
650
651            // Represent as a rectangular shape with the resolved dimensions
652            content.push(InlineContent::Shape(InlineShape {
653                shape_def: ShapeDefinition::Rectangle {
654                    size: crate::text3::cache::Size {
655                        width: used_width,
656                        height: used_height,
657                    },
658                    corner_radius: None,
659                },
660                fill: None,
661                stroke: None,
662                baseline_offset: used_height,
663                source_node_id: Some(child_dom_id),
664            }));
665        }
666    }
667
668    Ok(())
669}
670
671// Keep old name as an alias for backward compatibility
672pub fn collect_inline_content<T: ParsedFontTrait>(
673    ctx: &mut LayoutContext<'_, T>,
674    tree: &LayoutTree,
675    ifc_root_index: usize,
676) -> Result<Vec<InlineContent>> {
677    collect_inline_content_for_sizing(ctx, tree, ifc_root_index)
678}
679
680fn calculate_intrinsic_recursive<T: ParsedFontTrait>(
681    ctx: &mut LayoutContext<'_, T>,
682    tree: &mut LayoutTree,
683    node_index: usize,
684) -> Result<IntrinsicSizes> {
685    let node = tree
686        .get(node_index)
687        .cloned()
688        .ok_or(LayoutError::InvalidTree)?;
689
690    // Out-of-flow elements do not contribute to their parent's intrinsic size.
691    let position = get_position_type(ctx.styled_dom, node.dom_node_id);
692    if position == LayoutPosition::Absolute || position == LayoutPosition::Fixed {
693        if let Some(n) = tree.get_mut(node_index) {
694            n.intrinsic_sizes = Some(IntrinsicSizes::default());
695        }
696        return Ok(IntrinsicSizes::default());
697    }
698
699    // First, calculate children's intrinsic sizes
700    let mut child_intrinsics = BTreeMap::new();
701    for &child_index in &node.children {
702        let child_intrinsic = calculate_intrinsic_recursive(ctx, tree, child_index)?;
703        child_intrinsics.insert(child_index, child_intrinsic);
704    }
705
706    // Then calculate this node's intrinsic size based on its children
707    let intrinsic = calculate_node_intrinsic_sizes_stub(ctx, &node, &child_intrinsics);
708
709    if let Some(n) = tree.get_mut(node_index) {
710        n.intrinsic_sizes = Some(intrinsic.clone());
711    }
712
713    Ok(intrinsic)
714}
715
716/// STUB: Calculates intrinsic sizes for a node based on its children
717/// TODO: Implement proper intrinsic size calculation logic
718fn calculate_node_intrinsic_sizes_stub<T: ParsedFontTrait>(
719    _ctx: &LayoutContext<'_, T>,
720    _node: &LayoutNode,
721    child_intrinsics: &BTreeMap<usize, IntrinsicSizes>,
722) -> IntrinsicSizes {
723    // Simple stub: aggregate children's sizes
724    let mut max_width: f32 = 0.0;
725    let mut max_height: f32 = 0.0;
726    let mut total_width: f32 = 0.0;
727    let mut total_height: f32 = 0.0;
728
729    for intrinsic in child_intrinsics.values() {
730        max_width = max_width.max(intrinsic.max_content_width);
731        max_height = max_height.max(intrinsic.max_content_height);
732        total_width += intrinsic.max_content_width;
733        total_height += intrinsic.max_content_height;
734    }
735
736    IntrinsicSizes {
737        min_content_width: total_width.min(max_width),
738        min_content_height: total_height.min(max_height),
739        max_content_width: max_width.max(total_width),
740        max_content_height: max_height.max(total_height),
741        preferred_width: None,
742        preferred_height: None,
743    }
744}
745
746/// Calculates the used size of a single node based on its CSS properties and
747/// the available space provided by its containing block.
748///
749/// This implementation correctly handles writing modes and percentage-based sizes
750/// according to the CSS specification:
751/// 1. `width` and `height` CSS properties are resolved to pixel values. Percentages are calculated
752///    based on the containing block's PHYSICAL dimensions (`width` for `width`, `height` for
753///    `height`), regardless of writing mode.
754/// 2. The resolved physical `width` is then mapped to the node's logical CROSS size.
755/// 3. The resolved physical `height` is then mapped to the node's logical MAIN size.
756/// 4. A final `LogicalSize` is constructed from these logical dimensions.
757pub fn calculate_used_size_for_node(
758    styled_dom: &StyledDom,
759    dom_id: Option<NodeId>,
760    containing_block_size: LogicalSize,
761    intrinsic: IntrinsicSizes,
762    _box_props: &BoxProps,
763) -> Result<LogicalSize> {
764    let Some(id) = dom_id else {
765        // Anonymous boxes:
766        // - Width fills the containing block (like block-level elements)
767        // - Height is auto (content-based)
768        // CSS 2.2 § 9.2.1.1: Anonymous boxes inherit from their enclosing box
769        return Ok(LogicalSize::new(
770            containing_block_size.width,
771            if intrinsic.max_content_height > 0.0 {
772                intrinsic.max_content_height
773            } else {
774                // Auto height - will be resolved from content
775                0.0
776            },
777        ));
778    };
779
780    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
781    let css_width = get_css_width(styled_dom, id, node_state);
782    let css_height = get_css_height(styled_dom, id, node_state);
783    let writing_mode = get_writing_mode(styled_dom, id, node_state);
784    let display = get_display_property(styled_dom, Some(id));
785
786    // Step 1: Resolve the CSS `width` property into a concrete pixel value.
787    // Percentage values for `width` are resolved against the containing block's width.
788    let resolved_width = match css_width.unwrap_or_default() {
789        LayoutWidth::Auto => {
790            // 'auto' width resolution depends on the display type.
791            match display.unwrap_or_default() {
792                LayoutDisplay::Block
793                | LayoutDisplay::FlowRoot
794                | LayoutDisplay::ListItem
795                | LayoutDisplay::Flex
796                | LayoutDisplay::Grid => {
797                    // For block-level elements (including flex and grid containers),
798                    // 'auto' width fills the containing block (minus margins, borders, padding).
799                    // CSS 2.1 Section 10.3.3: width = containing_block_width - margin_left -
800                    // margin_right - border_left - border_right - padding_left - padding_right
801                    //
802                    // Note: Flex/Grid CONTAINERS behave like blocks for sizing purposes.
803                    // Flex/Grid ITEMS have different sizing, but that's handled by Taffy
804                    // during the formatting context layout, not here.
805                    let available_width = containing_block_size.width
806                        - _box_props.margin.left
807                        - _box_props.margin.right
808                        - _box_props.border.left
809                        - _box_props.border.right
810                        - _box_props.padding.left
811                        - _box_props.padding.right;
812
813                    available_width.max(0.0)
814                }
815                LayoutDisplay::Inline | LayoutDisplay::InlineBlock => {
816                    // For inline-level elements, 'auto' width is the shrink-to-fit width,
817                    // which is the max-content width
818                    intrinsic.max_content_width
819                }
820                // Table and other display types use intrinsic sizing
821                _ => intrinsic.max_content_width,
822            }
823        }
824        LayoutWidth::Px(px) => {
825            // Resolve percentage or absolute pixel value
826            use azul_css::props::basic::{
827                pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
828                SizeMetric,
829            };
830            let pixels_opt = match px.metric {
831                SizeMetric::Px => Some(px.number.get()),
832                SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
833                SizeMetric::In => Some(px.number.get() * 96.0),
834                SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
835                SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
836                SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
837                SizeMetric::Percent => None,
838                SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => None,
839            };
840
841            match pixels_opt {
842                Some(pixels) => pixels,
843                None => match px.to_percent() {
844                    Some(p) => {
845                        let result = resolve_percentage_with_box_model(
846                            containing_block_size.width,
847                            p.get(),
848                            (_box_props.margin.left, _box_props.margin.right),
849                            (_box_props.border.left, _box_props.border.right),
850                            (_box_props.padding.left, _box_props.padding.right),
851                        );
852
853                        result
854                    }
855                    None => intrinsic.max_content_width,
856                },
857            }
858        }
859        LayoutWidth::MinContent => intrinsic.min_content_width,
860        LayoutWidth::MaxContent => intrinsic.max_content_width,
861    };
862
863    // Step 2: Resolve the CSS `height` property into a concrete pixel value.
864    // Percentage values for `height` are resolved against the containing block's height.
865    let resolved_height = match css_height.unwrap_or_default() {
866        LayoutHeight::Auto => {
867            // For 'auto' height, we initially use the intrinsic content height.
868            // For block containers, this will be updated later in the layout process
869            // after the children's heights are known.
870            intrinsic.max_content_height
871        }
872        LayoutHeight::Px(px) => {
873            // Resolve percentage or absolute pixel value
874            use azul_css::props::basic::{
875                pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
876                SizeMetric,
877            };
878            let pixels_opt = match px.metric {
879                SizeMetric::Px => Some(px.number.get()),
880                SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
881                SizeMetric::In => Some(px.number.get() * 96.0),
882                SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
883                SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
884                SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
885                SizeMetric::Percent => None,
886                SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => None,
887            };
888
889            match pixels_opt {
890                Some(pixels) => pixels,
891                None => match px.to_percent() {
892                    Some(p) => resolve_percentage_with_box_model(
893                        containing_block_size.height,
894                        p.get(),
895                        (_box_props.margin.top, _box_props.margin.bottom),
896                        (_box_props.border.top, _box_props.border.bottom),
897                        (_box_props.padding.top, _box_props.padding.bottom),
898                    ),
899                    None => intrinsic.max_content_height,
900                },
901            }
902        }
903        LayoutHeight::MinContent => intrinsic.min_content_height,
904        LayoutHeight::MaxContent => intrinsic.max_content_height,
905    };
906
907    // Step 3: Apply min/max constraints (CSS 2.2 § 10.4 and § 10.7)
908    // "The tentative used width is calculated (without 'min-width' and 'max-width')
909    // ...If the tentative used width is greater than 'max-width', the rules above are
910    // applied again using the computed value of 'max-width' as the computed value for 'width'.
911    // If the resulting width is smaller than 'min-width', the rules above are applied again
912    // using the value of 'min-width' as the computed value for 'width'."
913
914    let constrained_width = apply_width_constraints(
915        styled_dom,
916        id,
917        node_state,
918        resolved_width,
919        containing_block_size.width,
920        _box_props,
921    );
922
923    let constrained_height = apply_height_constraints(
924        styled_dom,
925        id,
926        node_state,
927        resolved_height,
928        containing_block_size.height,
929        _box_props,
930    );
931
932    // Step 4: Convert to border-box dimensions, respecting box-sizing property
933    // CSS box-sizing:
934    // - content-box (default): width/height set content size, border+padding are added
935    // - border-box: width/height set border-box size, border+padding are included
936    let box_sizing = match get_css_box_sizing(styled_dom, id, node_state) {
937        MultiValue::Exact(bs) => bs,
938        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
939            azul_css::props::layout::LayoutBoxSizing::ContentBox
940        }
941    };
942
943    let (border_box_width, border_box_height) = match box_sizing {
944        azul_css::props::layout::LayoutBoxSizing::BorderBox => {
945            // border-box: The width/height values already include border and padding
946            // CSS Box Sizing Level 3: "the specified width and height (and respective min/max
947            // properties) on this element determine the border box of the element"
948            (constrained_width, constrained_height)
949        }
950        azul_css::props::layout::LayoutBoxSizing::ContentBox => {
951            // content-box: The width/height values set the content size,
952            // border and padding are added outside
953            // CSS 2.2 § 8.4: "The properties that apply to and affect box dimensions are:
954            // margin, border, padding, width, and height."
955            let border_box_width = constrained_width
956                + _box_props.padding.left
957                + _box_props.padding.right
958                + _box_props.border.left
959                + _box_props.border.right;
960            let border_box_height = constrained_height
961                + _box_props.padding.top
962                + _box_props.padding.bottom
963                + _box_props.border.top
964                + _box_props.border.bottom;
965            (border_box_width, border_box_height)
966        }
967    };
968
969    // Step 5: Map the resolved physical dimensions to logical dimensions.
970    // The `width` property always corresponds to the cross (inline) axis size.
971    // The `height` property always corresponds to the main (block) axis size.
972    let cross_size = border_box_width;
973    let main_size = border_box_height;
974
975    // Step 6: Construct the final LogicalSize from the logical dimensions.
976    let result =
977        LogicalSize::from_main_cross(main_size, cross_size, writing_mode.unwrap_or_default());
978
979    Ok(result)
980}
981
982/// Apply min-width and max-width constraints to tentative width
983/// Per CSS 2.2 § 10.4: min-width overrides max-width if min > max
984fn apply_width_constraints(
985    styled_dom: &StyledDom,
986    id: NodeId,
987    node_state: &StyledNodeState,
988    tentative_width: f32,
989    containing_block_width: f32,
990    box_props: &BoxProps,
991) -> f32 {
992    use azul_css::props::basic::{
993        pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
994        SizeMetric,
995    };
996
997    use crate::solver3::getters::{get_css_max_width, get_css_min_width, MultiValue};
998
999    // Resolve min-width (default is 0)
1000    let min_width = match get_css_min_width(styled_dom, id, node_state) {
1001        MultiValue::Exact(mw) => {
1002            let px = &mw.inner;
1003            let pixels_opt = match px.metric {
1004                SizeMetric::Px => Some(px.number.get()),
1005                SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
1006                SizeMetric::In => Some(px.number.get() * 96.0),
1007                SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
1008                SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
1009                SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
1010                SizeMetric::Percent => None,
1011                _ => None,
1012            };
1013
1014            match pixels_opt {
1015                Some(pixels) => pixels,
1016                None => px
1017                    .to_percent()
1018                    .map(|p| {
1019                        resolve_percentage_with_box_model(
1020                            containing_block_width,
1021                            p.get(),
1022                            (box_props.margin.left, box_props.margin.right),
1023                            (box_props.border.left, box_props.border.right),
1024                            (box_props.padding.left, box_props.padding.right),
1025                        )
1026                    })
1027                    .unwrap_or(0.0),
1028            }
1029        }
1030        _ => 0.0,
1031    };
1032
1033    // Resolve max-width (default is infinity/none)
1034    let max_width = match get_css_max_width(styled_dom, id, node_state) {
1035        MultiValue::Exact(mw) => {
1036            let px = &mw.inner;
1037            // Check if it's the default "max" value (f32::MAX)
1038            if px.number.get() >= core::f32::MAX - 1.0 {
1039                None
1040            } else {
1041                let pixels_opt = match px.metric {
1042                    SizeMetric::Px => Some(px.number.get()),
1043                    SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
1044                    SizeMetric::In => Some(px.number.get() * 96.0),
1045                    SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
1046                    SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
1047                    SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
1048                    SizeMetric::Percent => None,
1049                    _ => None,
1050                };
1051
1052                match pixels_opt {
1053                    Some(pixels) => Some(pixels),
1054                    None => px.to_percent().map(|p| {
1055                        resolve_percentage_with_box_model(
1056                            containing_block_width,
1057                            p.get(),
1058                            (box_props.margin.left, box_props.margin.right),
1059                            (box_props.border.left, box_props.border.right),
1060                            (box_props.padding.left, box_props.padding.right),
1061                        )
1062                    }),
1063                }
1064            }
1065        }
1066        _ => None,
1067    };
1068
1069    // Apply constraints: max(min_width, min(tentative, max_width))
1070    // If min > max, min wins per CSS spec
1071    let mut result = tentative_width;
1072
1073    if let Some(max) = max_width {
1074        result = result.min(max);
1075    }
1076
1077    result = result.max(min_width);
1078
1079    result
1080}
1081
1082/// Apply min-height and max-height constraints to tentative height
1083/// Per CSS 2.2 § 10.7: min-height overrides max-height if min > max
1084fn apply_height_constraints(
1085    styled_dom: &StyledDom,
1086    id: NodeId,
1087    node_state: &StyledNodeState,
1088    tentative_height: f32,
1089    containing_block_height: f32,
1090    box_props: &BoxProps,
1091) -> f32 {
1092    use azul_css::props::basic::{
1093        pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
1094        SizeMetric,
1095    };
1096
1097    use crate::solver3::getters::{get_css_max_height, get_css_min_height, MultiValue};
1098
1099    // Resolve min-height (default is 0)
1100    let min_height = match get_css_min_height(styled_dom, id, node_state) {
1101        MultiValue::Exact(mh) => {
1102            let px = &mh.inner;
1103            let pixels_opt = match px.metric {
1104                SizeMetric::Px => Some(px.number.get()),
1105                SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
1106                SizeMetric::In => Some(px.number.get() * 96.0),
1107                SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
1108                SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
1109                SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
1110                SizeMetric::Percent => None,
1111                _ => None,
1112            };
1113
1114            match pixels_opt {
1115                Some(pixels) => pixels,
1116                None => px
1117                    .to_percent()
1118                    .map(|p| {
1119                        resolve_percentage_with_box_model(
1120                            containing_block_height,
1121                            p.get(),
1122                            (box_props.margin.top, box_props.margin.bottom),
1123                            (box_props.border.top, box_props.border.bottom),
1124                            (box_props.padding.top, box_props.padding.bottom),
1125                        )
1126                    })
1127                    .unwrap_or(0.0),
1128            }
1129        }
1130        _ => 0.0,
1131    };
1132
1133    // Resolve max-height (default is infinity/none)
1134    let max_height = match get_css_max_height(styled_dom, id, node_state) {
1135        MultiValue::Exact(mh) => {
1136            let px = &mh.inner;
1137            // Check if it's the default "max" value (f32::MAX)
1138            if px.number.get() >= core::f32::MAX - 1.0 {
1139                None
1140            } else {
1141                let pixels_opt = match px.metric {
1142                    SizeMetric::Px => Some(px.number.get()),
1143                    SizeMetric::Pt => Some(px.number.get() * PT_TO_PX),
1144                    SizeMetric::In => Some(px.number.get() * 96.0),
1145                    SizeMetric::Cm => Some(px.number.get() * 96.0 / 2.54),
1146                    SizeMetric::Mm => Some(px.number.get() * 96.0 / 25.4),
1147                    SizeMetric::Em | SizeMetric::Rem => Some(px.number.get() * DEFAULT_FONT_SIZE),
1148                    SizeMetric::Percent => None,
1149                    _ => None,
1150                };
1151
1152                match pixels_opt {
1153                    Some(pixels) => Some(pixels),
1154                    None => px.to_percent().map(|p| {
1155                        resolve_percentage_with_box_model(
1156                            containing_block_height,
1157                            p.get(),
1158                            (box_props.margin.top, box_props.margin.bottom),
1159                            (box_props.border.top, box_props.border.bottom),
1160                            (box_props.padding.top, box_props.padding.bottom),
1161                        )
1162                    }),
1163                }
1164            }
1165        }
1166        _ => None,
1167    };
1168
1169    // Apply constraints: max(min_height, min(tentative, max_height))
1170    // If min > max, min wins per CSS spec
1171    let mut result = tentative_height;
1172
1173    if let Some(max) = max_height {
1174        result = result.min(max);
1175    }
1176
1177    result = result.max(min_height);
1178
1179    result
1180}
1181
1182fn collect_text_recursive(
1183    tree: &LayoutTree,
1184    node_index: usize,
1185    styled_dom: &StyledDom,
1186    content: &mut Vec<InlineContent>,
1187) {
1188    let node = match tree.get(node_index) {
1189        Some(n) => n,
1190        None => return,
1191    };
1192
1193    // If this node has text content, add it
1194    if let Some(dom_id) = node.dom_node_id {
1195        if let Some(text) = extract_text_from_node(styled_dom, dom_id) {
1196            content.push(InlineContent::Text(StyledRun {
1197                text,
1198                style: std::sync::Arc::new(StyleProperties::default()),
1199                logical_start_byte: 0,
1200                source_node_id: Some(dom_id),
1201            }));
1202        }
1203    }
1204
1205    // Recurse into children
1206    for &child_index in &node.children {
1207        collect_text_recursive(tree, child_index, styled_dom, content);
1208    }
1209}
1210
1211pub fn extract_text_from_node(styled_dom: &StyledDom, node_id: NodeId) -> Option<String> {
1212    match &styled_dom.node_data.as_container()[node_id].get_node_type() {
1213        NodeType::Text(text_data) => Some(text_data.as_str().to_string()),
1214        _ => None,
1215    }
1216}
1217
1218fn debug_log(debug_messages: &mut Option<Vec<LayoutDebugMessage>>, message: &str) {
1219    if let Some(messages) = debug_messages {
1220        messages.push(LayoutDebugMessage::info(message));
1221    }
1222}
1223
1224#[cfg(test)]
1225mod tests {
1226    use super::*;
1227
1228    #[test]
1229    fn test_resolve_percentage_with_box_model_basic() {
1230        // 100% of 595px with no margins/borders/paddings should be 595px
1231        let result = resolve_percentage_with_box_model(
1232            595.0,
1233            1.0, // 100%
1234            (0.0, 0.0),
1235            (0.0, 0.0),
1236            (0.0, 0.0),
1237        );
1238        assert_eq!(result, 595.0);
1239    }
1240
1241    #[test]
1242    fn test_resolve_percentage_with_box_model_with_margins() {
1243        // Body element: width: 100%, margin: 20px
1244        // Containing block (html): 595px wide
1245        // CSS spec: percentage resolves against containing block, NOT available space
1246        // Expected: 595px (margins are ignored for percentage resolution)
1247        let result = resolve_percentage_with_box_model(
1248            595.0,
1249            1.0, // 100%
1250            (20.0, 20.0),
1251            (0.0, 0.0),
1252            (0.0, 0.0),
1253        );
1254        assert_eq!(result, 595.0);
1255    }
1256
1257    #[test]
1258    fn test_resolve_percentage_with_box_model_with_all_box_properties() {
1259        // Element with margin: 10px, border: 5px, padding: 8px
1260        // width: 100% of 500px container
1261        // CSS spec: percentage resolves against containing block
1262        // Expected: 500px (margins/borders/padding are ignored)
1263        let result = resolve_percentage_with_box_model(
1264            500.0,
1265            1.0, // 100%
1266            (10.0, 10.0),
1267            (5.0, 5.0),
1268            (8.0, 8.0),
1269        );
1270        assert_eq!(result, 500.0);
1271    }
1272
1273    #[test]
1274    fn test_resolve_percentage_with_box_model_50_percent() {
1275        // 50% of 600px containing block
1276        // CSS spec: 50% of containing block = 300px
1277        // (margins don't affect percentage resolution)
1278        let result = resolve_percentage_with_box_model(
1279            600.0,
1280            0.5, // 50%
1281            (20.0, 20.0),
1282            (0.0, 0.0),
1283            (0.0, 0.0),
1284        );
1285        assert_eq!(result, 300.0);
1286    }
1287
1288    #[test]
1289    fn test_resolve_percentage_with_box_model_asymmetric() {
1290        // Asymmetric margins/borders/paddings
1291        // Container: 1000px
1292        // CSS spec: percentage resolves against containing block
1293        // 100% of 1000px = 1000px (margins/borders/padding ignored)
1294        let result = resolve_percentage_with_box_model(
1295            1000.0,
1296            1.0,
1297            (100.0, 50.0),
1298            (10.0, 20.0),
1299            (5.0, 15.0),
1300        );
1301        assert_eq!(result, 1000.0);
1302    }
1303
1304    #[test]
1305    fn test_resolve_percentage_with_box_model_negative_clamping() {
1306        // Edge case: margins larger than container
1307        // CSS spec: percentage still resolves against containing block
1308        // Result should still be 100px (100% of 100px)
1309        let result = resolve_percentage_with_box_model(
1310            100.0,
1311            1.0,
1312            (60.0, 60.0), // margins ignored for percentage resolution
1313            (0.0, 0.0),
1314            (0.0, 0.0),
1315        );
1316        assert_eq!(result, 100.0);
1317    }
1318
1319    #[test]
1320    fn test_resolve_percentage_with_box_model_zero_percent() {
1321        // 0% should always give 0, regardless of margins
1322        let result = resolve_percentage_with_box_model(
1323            1000.0,
1324            0.0, // 0%
1325            (100.0, 100.0),
1326            (10.0, 10.0),
1327            (5.0, 5.0),
1328        );
1329        assert_eq!(result, 0.0);
1330    }
1331}