Skip to main content

azul_layout/solver3/
fc.rs

1//! solver3/fc.rs - Formatting Context Layout
2//!
3//! This module implements the CSS Visual Formatting Model's formatting contexts:
4//!
5//! - **Block Formatting Context (BFC)**: CSS 2.2 § 9.4.1 Block-level boxes in normal flow, with
6//!   margin collapsing and float positioning.
7//!
8//! - **Inline Formatting Context (IFC)**: CSS 2.2 § 9.4.2 Inline-level content (text,
9//!   inline-blocks) laid out in line boxes.
10//!
11//! - **Table Formatting Context**: CSS 2.2 § 17 Table layout with column width calculation and cell
12//!   positioning.
13//!
14//! - **Flex/Grid Formatting Contexts**: CSS Flexbox/Grid via Taffy Delegated to the Taffy layout
15//!   engine for modern layout modes.
16//!
17//! # Module Organization
18//!
19//! 1. **Constants & Types** - Magic numbers as named constants, core types
20//! 2. **Entry Point** - `layout_formatting_context` dispatcher
21//! 3. **BFC Layout** - Block formatting context implementation
22//! 4. **IFC Layout** - Inline formatting context implementation
23//! 5. **Table Layout** - Table formatting context implementation
24//! 6. **Flex/Grid Layout** - Taffy bridge wrappers
25//! 7. **Helper Functions** - Property getters, margin collapsing, utilities
26
27use std::{
28    collections::{BTreeMap, HashMap},
29    sync::Arc,
30};
31
32use azul_core::{
33    dom::{FormattingContext, NodeId, NodeType},
34    geom::{LogicalPosition, LogicalRect, LogicalSize},
35    resources::RendererResources,
36    styled_dom::{StyledDom, StyledNodeState},
37};
38use azul_css::{
39    css::CssPropertyValue,
40    props::{
41        basic::{
42            font::{StyleFontStyle, StyleFontWeight},
43            pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
44            ColorU, PhysicalSize, PropertyContext, ResolutionContext, SizeMetric,
45        },
46        layout::{
47            ColumnCount, LayoutBorderSpacing, LayoutClear, LayoutDisplay, LayoutFloat,
48            LayoutHeight, LayoutJustifyContent, LayoutOverflow, LayoutPosition, LayoutTableLayout,
49            LayoutTextJustify, LayoutWidth, LayoutWritingMode, ShapeInside, ShapeOutside,
50            StyleBorderCollapse, StyleCaptionSide,
51        },
52        property::CssProperty,
53        style::{
54            BorderStyle, StyleDirection, StyleHyphens, StyleListStylePosition, StyleListStyleType,
55            StyleTextAlign, StyleTextCombineUpright, StyleVerticalAlign, StyleVisibility,
56            StyleWhiteSpace,
57        },
58    },
59};
60use rust_fontconfig::FcWeight;
61use taffy::{AvailableSpace, LayoutInput, Line, Size as TaffySize};
62
63#[cfg(feature = "text_layout")]
64use crate::text3;
65use crate::{
66    debug_ifc_layout, debug_info, debug_log, debug_table_layout, debug_warning,
67    font_traits::{
68        ContentIndex, FontLoaderTrait, ImageSource, InlineContent, InlineImage, InlineShape,
69        LayoutFragment, ObjectFit, ParsedFontTrait, SegmentAlignment, ShapeBoundary,
70        ShapeDefinition, ShapedItem, Size, StyleProperties, StyledRun, TextLayoutCache,
71        UnifiedConstraints,
72    },
73    solver3::{
74        geometry::{BoxProps, EdgeSizes, IntrinsicSizes},
75        getters::{
76            get_css_height, get_css_width, get_direction_property,
77            get_display_property, get_element_font_size, get_float, get_clear,
78            get_list_style_position, get_list_style_type, get_overflow_x, get_overflow_y,
79            get_parent_font_size, get_root_font_size, get_style_properties,
80            get_text_align, get_vertical_align_property, get_visibility,
81            get_white_space_property, get_writing_mode, MultiValue,
82        },
83        layout_tree::{
84            AnonymousBoxType, CachedInlineLayout, LayoutNode, LayoutTree, PseudoElement,
85        },
86        positioning::get_position_type,
87        scrollbar::ScrollbarRequirements,
88        sizing::extract_text_from_node,
89        taffy_bridge, LayoutContext, LayoutDebugMessage, LayoutError, Result,
90    },
91    text3::cache::{AvailableSpace as Text3AvailableSpace, TextAlign as Text3TextAlign},
92};
93
94/// Default scrollbar width in pixels (CSS `scrollbar-width: auto`).
95/// This is only used as a fallback when per-node CSS cannot be queried.
96/// Prefer `getters::get_layout_scrollbar_width_px()` for per-node resolution.
97pub const DEFAULT_SCROLLBAR_WIDTH_PX: f32 = 16.0;
98
99// Note: DEFAULT_FONT_SIZE and PT_TO_PX are imported from pixel
100
101/// Result of BFC layout with margin escape information
102#[derive(Debug, Clone)]
103pub(crate) struct BfcLayoutResult {
104    /// Standard layout output (positions, overflow size, baseline)
105    pub output: LayoutOutput,
106    /// Top margin that escaped the BFC (for parent-child collapse)
107    /// If Some, this margin should be used by parent instead of positioning this BFC
108    pub escaped_top_margin: Option<f32>,
109    /// Bottom margin that escaped the BFC (for parent-child collapse)
110    /// If Some, this margin should collapse with next sibling
111    pub escaped_bottom_margin: Option<f32>,
112}
113
114impl BfcLayoutResult {
115    pub fn from_output(output: LayoutOutput) -> Self {
116        Self {
117            output,
118            escaped_top_margin: None,
119            escaped_bottom_margin: None,
120        }
121    }
122}
123
124/// The CSS `overflow` property behavior.
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum OverflowBehavior {
127    Visible,
128    Hidden,
129    Clip,
130    Scroll,
131    Auto,
132}
133
134impl OverflowBehavior {
135    pub fn is_clipped(&self) -> bool {
136        matches!(self, Self::Hidden | Self::Clip | Self::Scroll | Self::Auto)
137    }
138
139    pub fn is_scroll(&self) -> bool {
140        matches!(self, Self::Scroll | Self::Auto)
141    }
142}
143
144/// Input constraints for a layout function.
145#[derive(Debug)]
146pub struct LayoutConstraints<'a> {
147    /// The available space for the content, excluding padding and borders.
148    pub available_size: LogicalSize,
149    /// The CSS writing-mode of the context.
150    pub writing_mode: LayoutWritingMode,
151    /// The state of the parent Block Formatting Context, if applicable.
152    /// This is how state (like floats) is passed down.
153    pub bfc_state: Option<&'a mut BfcState>,
154    // Other properties like text-align would go here.
155    pub text_align: TextAlign,
156    /// The size of the containing block (parent's content box).
157    /// This is used for resolving percentage-based sizes and as parent_size for Taffy.
158    pub containing_block_size: LogicalSize,
159    /// The semantic type of the available width constraint.
160    ///
161    /// This field is crucial for correct inline layout caching:
162    /// - `Definite(w)`: Normal layout with a specific available width
163    /// - `MinContent`: Intrinsic minimum width measurement (maximum wrapping)
164    /// - `MaxContent`: Intrinsic maximum width measurement (no wrapping)
165    ///
166    /// When caching inline layouts, we must track which constraint type was used
167    /// to compute the cached result. A layout computed with `MinContent` (width=0)
168    /// must not be reused when the actual available width is known.
169    pub available_width_type: Text3AvailableSpace,
170}
171
172/// Manages all layout state for a single Block Formatting Context.
173/// This struct is created by the BFC root and lives for the duration of its layout.
174#[derive(Debug, Clone)]
175pub struct BfcState {
176    /// The current position for the next in-flow block element.
177    pub pen: LogicalPosition,
178    /// The state of all floated elements within this BFC.
179    pub floats: FloatingContext,
180    /// The state of margin collapsing within this BFC.
181    pub margins: MarginCollapseContext,
182}
183
184impl BfcState {
185    pub fn new() -> Self {
186        Self {
187            pen: LogicalPosition::zero(),
188            floats: FloatingContext::default(),
189            margins: MarginCollapseContext::default(),
190        }
191    }
192}
193
194/// Manages vertical margin collapsing within a BFC.
195#[derive(Debug, Default, Clone)]
196pub struct MarginCollapseContext {
197    /// The bottom margin of the last in-flow, block-level element.
198    /// Can be positive or negative.
199    pub last_in_flow_margin_bottom: f32,
200}
201
202/// The result of laying out a formatting context.
203#[derive(Debug, Default, Clone)]
204pub struct LayoutOutput {
205    /// The final positions of child nodes, relative to the container's content-box origin.
206    pub positions: BTreeMap<usize, LogicalPosition>,
207    /// The total size occupied by the content, which may exceed `available_size`.
208    pub overflow_size: LogicalSize,
209    /// The baseline of the context, if applicable, measured from the top of its content box.
210    pub baseline: Option<f32>,
211}
212
213/// Text alignment options
214#[derive(Debug, Clone, Copy, Default)]
215pub enum TextAlign {
216    #[default]
217    Start,
218    End,
219    Center,
220    Justify,
221}
222
223/// Represents a single floated element within a BFC.
224#[derive(Debug, Clone, Copy)]
225struct FloatBox {
226    /// The type of float (Left or Right).
227    kind: LayoutFloat,
228    /// The rectangle of the float's content box (origin includes top/left margin offset).
229    rect: LogicalRect,
230    /// The margin sizes (needed to calculate true margin-box bounds).
231    margin: EdgeSizes,
232}
233
234/// Manages the state of all floated elements within a Block Formatting Context.
235#[derive(Debug, Default, Clone)]
236pub struct FloatingContext {
237    /// All currently positioned floats within the BFC.
238    pub floats: Vec<FloatBox>,
239}
240
241impl FloatingContext {
242    /// Add a newly positioned float to the context
243    pub fn add_float(&mut self, kind: LayoutFloat, rect: LogicalRect, margin: EdgeSizes) {
244        self.floats.push(FloatBox { kind, rect, margin });
245    }
246
247    /// Finds the available space on the cross-axis for a line box at a given main-axis range.
248    ///
249    /// Returns a tuple of (`cross_start_offset`, `cross_end_offset`) relative to the
250    /// BFC content box, defining the available space for an in-flow element.
251    pub fn available_line_box_space(
252        &self,
253        main_start: f32,
254        main_end: f32,
255        bfc_cross_size: f32,
256        wm: LayoutWritingMode,
257    ) -> (f32, f32) {
258        let mut available_cross_start = 0.0_f32;
259        let mut available_cross_end = bfc_cross_size;
260
261        for float in &self.floats {
262            // Get the logical main-axis span of the existing float.
263            let float_main_start = float.rect.origin.main(wm);
264            let float_main_end = float_main_start + float.rect.size.main(wm);
265
266            // Check for overlap on the main axis.
267            if main_end > float_main_start && main_start < float_main_end {
268                // The float overlaps with the main-axis range of the element we're placing.
269                let float_cross_start = float.rect.origin.cross(wm);
270                let float_cross_end = float_cross_start + float.rect.size.cross(wm);
271
272                if float.kind == LayoutFloat::Left {
273                    // "line-left", i.e., cross-start
274                    available_cross_start = available_cross_start.max(float_cross_end);
275                } else {
276                    // Float::Right, i.e., cross-end
277                    available_cross_end = available_cross_end.min(float_cross_start);
278                }
279            }
280        }
281        (available_cross_start, available_cross_end)
282    }
283
284    /// Returns the main-axis offset needed to be clear of floats of the given type.
285    pub fn clearance_offset(
286        &self,
287        clear: LayoutClear,
288        current_main_offset: f32,
289        wm: LayoutWritingMode,
290    ) -> f32 {
291        let mut max_end_offset = 0.0_f32;
292
293        let check_left = clear == LayoutClear::Left || clear == LayoutClear::Both;
294        let check_right = clear == LayoutClear::Right || clear == LayoutClear::Both;
295
296        for float in &self.floats {
297            let should_clear_this_float = (check_left && float.kind == LayoutFloat::Left)
298                || (check_right && float.kind == LayoutFloat::Right);
299
300            if should_clear_this_float {
301                // CSS 2.2 § 9.5.2: "the top border edge of the box be below the bottom outer edge"
302                // Outer edge = margin-box boundary (content + padding + border + margin)
303                let float_margin_box_end = float.rect.origin.main(wm)
304                    + float.rect.size.main(wm)
305                    + float.margin.main_end(wm);
306                max_end_offset = max_end_offset.max(float_margin_box_end);
307            }
308        }
309
310        if max_end_offset > current_main_offset {
311            max_end_offset
312        } else {
313            current_main_offset
314        }
315    }
316}
317
318/// Encapsulates all state needed to lay out a single Block Formatting Context.
319struct BfcLayoutState {
320    /// The current position for the next in-flow block element.
321    pen: LogicalPosition,
322    floats: FloatingContext,
323    margins: MarginCollapseContext,
324    /// The writing mode of the BFC root.
325    writing_mode: LayoutWritingMode,
326}
327
328/// Result of a formatting context layout operation
329#[derive(Debug, Default)]
330pub struct LayoutResult {
331    pub positions: Vec<(usize, LogicalPosition)>,
332    pub overflow_size: Option<LogicalSize>,
333    pub baseline_offset: f32,
334}
335
336// Entry Point & Dispatcher
337
338/// Main dispatcher for formatting context layout.
339///
340/// Routes layout to the appropriate formatting context handler based on the node's
341/// `formatting_context` property. This is the main entry point for all layout operations.
342///
343/// # CSS Spec References
344/// - CSS 2.2 § 9.4: Formatting contexts
345/// - CSS Flexbox § 3: Flex formatting contexts
346/// - CSS Grid § 5: Grid formatting contexts
347pub fn layout_formatting_context<T: ParsedFontTrait>(
348    ctx: &mut LayoutContext<'_, T>,
349    tree: &mut LayoutTree,
350    text_cache: &mut crate::font_traits::TextLayoutCache,
351    node_index: usize,
352    constraints: &LayoutConstraints,
353    float_cache: &mut std::collections::BTreeMap<usize, FloatingContext>,
354) -> Result<BfcLayoutResult> {
355    let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
356
357    debug_info!(
358        ctx,
359        "[layout_formatting_context] node_index={}, fc={:?}, available_size={:?}",
360        node_index,
361        node.formatting_context,
362        constraints.available_size
363    );
364
365    match node.formatting_context {
366        FormattingContext::Block { .. } => {
367            layout_bfc(ctx, tree, text_cache, node_index, constraints, float_cache)
368        }
369        FormattingContext::Inline => layout_ifc(ctx, text_cache, tree, node_index, constraints)
370            .map(BfcLayoutResult::from_output),
371        FormattingContext::InlineBlock => {
372            // CSS 2.2 § 9.4.1: "inline-blocks... establish new block formatting contexts"
373            // InlineBlock ALWAYS establishes a BFC for its contents.
374            // The element itself participates as an atomic inline in its parent's IFC,
375            // but its children are laid out in a BFC, not an IFC.
376            let mut temp_float_cache = std::collections::BTreeMap::new();
377            layout_bfc(ctx, tree, text_cache, node_index, constraints, &mut temp_float_cache)
378        }
379        FormattingContext::Table => layout_table_fc(ctx, tree, text_cache, node_index, constraints)
380            .map(BfcLayoutResult::from_output),
381        FormattingContext::Flex | FormattingContext::Grid => {
382            layout_flex_grid(ctx, tree, text_cache, node_index, constraints)
383        }
384        _ => {
385            // Unknown formatting context - fall back to BFC
386            let mut temp_float_cache = std::collections::BTreeMap::new();
387            layout_bfc(
388                ctx,
389                tree,
390                text_cache,
391                node_index,
392                constraints,
393                &mut temp_float_cache,
394            )
395        }
396    }
397}
398
399// Flex / grid layout (taffy Bridge)
400
401/// Lays out a Flex or Grid formatting context using the Taffy layout engine.
402///
403/// # CSS Spec References
404///
405/// - CSS Flexbox § 9: Flex Layout Algorithm
406/// - CSS Grid § 12: Grid Layout Algorithm
407///
408/// # Implementation Notes
409///
410/// - Resolves explicit CSS dimensions to pixel values for `known_dimensions`
411/// - Uses `InherentSize` mode when explicit dimensions are set
412/// - Uses `ContentSize` mode for auto-sizing (shrink-to-fit)
413fn layout_flex_grid<T: ParsedFontTrait>(
414    ctx: &mut LayoutContext<'_, T>,
415    tree: &mut LayoutTree,
416    text_cache: &mut crate::font_traits::TextLayoutCache,
417    node_index: usize,
418    constraints: &LayoutConstraints,
419) -> Result<BfcLayoutResult> {
420    // Available space comes directly from constraints - margins are handled by Taffy
421    let available_space = TaffySize {
422        width: AvailableSpace::Definite(constraints.available_size.width),
423        height: AvailableSpace::Definite(constraints.available_size.height),
424    };
425
426    let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
427
428    // Resolve explicit CSS dimensions to pixel values.
429    // This is CRITICAL for align-items: stretch to work correctly!
430    // Taffy uses known_dimensions to calculate cross_axis_available_space for children.
431    let (explicit_width, has_explicit_width) =
432        resolve_explicit_dimension_width(ctx, node, constraints);
433    let (explicit_height, has_explicit_height) =
434        resolve_explicit_dimension_height(ctx, node, constraints);
435
436    // FIX: For root nodes or nodes where the parent provides a definite size,
437    // use the available_size as known_dimensions if no explicit CSS width/height is set.
438    // This is critical for `align-self: stretch` to work - Taffy needs to know the
439    // cross-axis size of the container to stretch children to fill it.
440    let is_root = node.parent.is_none();
441    
442    // NOTE: For root nodes, margins are already handled by calculate_used_size_for_node()
443    // which subtracts margin from the containing block width when resolving 'auto' width.
444    // Therefore, constraints.available_size already reflects the margin-adjusted size.
445    // We do NOT subtract margins again here - that would cause double subtraction.
446    
447    let effective_width = if has_explicit_width {
448        explicit_width
449    } else if is_root && constraints.available_size.width.is_finite() {
450        // Root node: use available_size directly (margin already subtracted in sizing.rs)
451        Some(constraints.available_size.width)
452    } else {
453        None
454    };
455    let effective_height = if has_explicit_height {
456        explicit_height
457    } else if is_root && constraints.available_size.height.is_finite() {
458        // Root node: use available_size directly (margin already subtracted in sizing.rs)
459        Some(constraints.available_size.height)
460    } else {
461        None
462    };
463    let has_effective_width = effective_width.is_some();
464    let has_effective_height = effective_height.is_some();
465
466    // FIX: Taffy interprets known_dimensions as Border Box size.
467    // CSS width/height properties define Content Box size (by default, box-sizing: content-box).
468    // We must add border and padding to the explicit dimensions to get the correct Border
469    // Box size for Taffy.
470    // NOTE: For root nodes using viewport size, no adjustment needed - viewport is already border-box.
471    let width_adjustment = node.box_props.border.left
472        + node.box_props.border.right
473        + node.box_props.padding.left
474        + node.box_props.padding.right;
475    let height_adjustment = node.box_props.border.top
476        + node.box_props.border.bottom
477        + node.box_props.padding.top
478        + node.box_props.padding.bottom;
479
480    // Apply adjustment only if dimensions come from explicit CSS (convert content-box to border-box)
481    // For root nodes using viewport size, no adjustment needed
482    let adjusted_width = if has_explicit_width {
483        explicit_width.map(|w| w + width_adjustment)
484    } else {
485        effective_width // Already in border-box for viewport
486    };
487    let adjusted_height = if has_explicit_height {
488        explicit_height.map(|h| h + height_adjustment)
489    } else {
490        effective_height // Already in border-box for viewport
491    };
492
493    // CSS Flexbox § 9.2: Use InherentSize when explicit dimensions are set,
494    // ContentSize for auto-sizing (shrink-to-fit behavior).
495    let sizing_mode = if has_effective_width || has_effective_height {
496        taffy::SizingMode::InherentSize
497    } else {
498        taffy::SizingMode::ContentSize
499    };
500
501    let known_dimensions = TaffySize {
502        width: adjusted_width,
503        height: adjusted_height,
504    };
505
506    // parent_size tells Taffy the size of the container's parent.
507    // For root nodes, the "parent" is the viewport, but since margins are already
508    // handled by calculate_used_size_for_node(), we use containing_block_size directly.
509    // For non-root nodes, containing_block_size is already the parent's content-box.
510    let parent_size = translate_taffy_size(constraints.containing_block_size);
511
512    let taffy_inputs = LayoutInput {
513        known_dimensions,
514        parent_size,
515        available_space,
516        run_mode: taffy::RunMode::PerformLayout,
517        sizing_mode,
518        axis: taffy::RequestedAxis::Both,
519        // Flex and Grid containers establish a new BFC, preventing margin collapse.
520        vertical_margins_are_collapsible: Line::FALSE,
521    };
522
523    debug_info!(
524        ctx,
525        "CALLING LAYOUT_TAFFY FOR FLEX/GRID FC node_index={:?}",
526        node_index
527    );
528
529    let taffy_output =
530        taffy_bridge::layout_taffy_subtree(ctx, tree, text_cache, node_index, taffy_inputs);
531
532    // Collect child positions from the tree (Taffy stores results directly on nodes).
533    let mut output = LayoutOutput::default();
534    // Use content_size for overflow detection, not container size.
535    // content_size represents the actual size of all children, which may exceed the container.
536    output.overflow_size = translate_taffy_size_back(taffy_output.content_size);
537
538    let children: Vec<usize> = tree.get(node_index).unwrap().children.clone();
539    for &child_idx in &children {
540        if let Some(child_node) = tree.get(child_idx) {
541            if let Some(pos) = child_node.relative_position {
542                output.positions.insert(child_idx, pos);
543            }
544        }
545    }
546
547    Ok(BfcLayoutResult::from_output(output))
548}
549
550/// Resolves explicit CSS width to pixel value for Taffy layout.
551fn resolve_explicit_dimension_width<T: ParsedFontTrait>(
552    ctx: &LayoutContext<'_, T>,
553    node: &LayoutNode,
554    constraints: &LayoutConstraints,
555) -> (Option<f32>, bool) {
556    node.dom_node_id
557        .map(|id| {
558            let width = get_css_width(
559                ctx.styled_dom,
560                id,
561                &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state,
562            );
563            match width.unwrap_or_default() {
564                LayoutWidth::Auto => (None, false),
565                LayoutWidth::Px(px) => {
566                    let pixels = resolve_size_metric(
567                        px.metric,
568                        px.number.get(),
569                        constraints.available_size.width,
570                        ctx.viewport_size,
571                    );
572                    (Some(pixels), true)
573                }
574                LayoutWidth::MinContent | LayoutWidth::MaxContent => (None, false),
575                LayoutWidth::Calc(_) => (None, false), // TODO: resolve calc
576            }
577        })
578        .unwrap_or((None, false))
579}
580
581/// Resolves explicit CSS height to pixel value for Taffy layout.
582fn resolve_explicit_dimension_height<T: ParsedFontTrait>(
583    ctx: &LayoutContext<'_, T>,
584    node: &LayoutNode,
585    constraints: &LayoutConstraints,
586) -> (Option<f32>, bool) {
587    node.dom_node_id
588        .map(|id| {
589            let height = get_css_height(
590                ctx.styled_dom,
591                id,
592                &ctx.styled_dom.styled_nodes.as_container()[id].styled_node_state,
593            );
594            match height.unwrap_or_default() {
595                LayoutHeight::Auto => (None, false),
596                LayoutHeight::Px(px) => {
597                    let pixels = resolve_size_metric(
598                        px.metric,
599                        px.number.get(),
600                        constraints.available_size.height,
601                        ctx.viewport_size,
602                    );
603                    (Some(pixels), true)
604                }
605                LayoutHeight::MinContent | LayoutHeight::MaxContent => (None, false),
606                LayoutHeight::Calc(_) => (None, false), // TODO: resolve calc
607            }
608        })
609        .unwrap_or((None, false))
610}
611
612/// Position a float within a BFC, considering existing floats.
613/// Returns the LogicalRect (margin box) for the float.
614fn position_float(
615    float_ctx: &FloatingContext,
616    float_type: LayoutFloat,
617    size: LogicalSize,
618    margin: &EdgeSizes,
619    current_main_offset: f32,
620    bfc_cross_size: f32,
621    wm: LayoutWritingMode,
622) -> LogicalRect {
623    // Start at the current main-axis position (Y in horizontal-tb)
624    let mut main_start = current_main_offset;
625
626    // Calculate total size including margins
627    let total_main = size.main(wm) + margin.main_start(wm) + margin.main_end(wm);
628    let total_cross = size.cross(wm) + margin.cross_start(wm) + margin.cross_end(wm);
629
630    // Find a position where the float fits
631    let cross_start = loop {
632        let (avail_start, avail_end) = float_ctx.available_line_box_space(
633            main_start,
634            main_start + total_main,
635            bfc_cross_size,
636            wm,
637        );
638
639        let available_width = avail_end - avail_start;
640
641        if available_width >= total_cross {
642            // Found space that fits
643            if float_type == LayoutFloat::Left {
644                // Position at line-left (avail_start)
645                break avail_start + margin.cross_start(wm);
646            } else {
647                // Position at line-right (avail_end - size)
648                break avail_end - total_cross + margin.cross_start(wm);
649            }
650        }
651
652        // Not enough space at this Y, move down past the lowest overlapping float
653        let next_main = float_ctx
654            .floats
655            .iter()
656            .filter(|f| {
657                let f_main_start = f.rect.origin.main(wm);
658                let f_main_end = f_main_start + f.rect.size.main(wm);
659                f_main_end > main_start && f_main_start < main_start + total_main
660            })
661            .map(|f| f.rect.origin.main(wm) + f.rect.size.main(wm))
662            .max_by(|a, b| a.partial_cmp(b).unwrap());
663
664        if let Some(next) = next_main {
665            main_start = next;
666        } else {
667            // No overlapping floats found, use current position anyway
668            if float_type == LayoutFloat::Left {
669                break avail_start + margin.cross_start(wm);
670            } else {
671                break avail_end - total_cross + margin.cross_start(wm);
672            }
673        }
674    };
675
676    LogicalRect {
677        origin: LogicalPosition::from_main_cross(
678            main_start + margin.main_start(wm),
679            cross_start,
680            wm,
681        ),
682        size,
683    }
684}
685
686// Block Formatting Context (CSS 2.2 § 9.4.1)
687
688/// Lays out a Block Formatting Context (BFC).
689///
690/// This is the corrected, architecturally-sound implementation. It solves the
691/// "chicken-and-egg" problem by performing its own two-pass layout:
692///
693/// 1. **Sizing Pass:** It first iterates through its children and triggers their layout recursively
694///    by calling `calculate_layout_for_subtree`. This ensures that the `used_size` property of each
695///    child is correctly populated.
696///
697/// 2. **Positioning Pass:** It then iterates through the children again. Now that each child has a
698///    valid size, it can apply the standard block-flow logic: stacking them vertically and
699///    advancing a "pen" by each child's outer height.
700///
701/// # Margin Collapsing Architecture
702///
703/// CSS 2.1 Section 8.3.1 compliant margin collapsing:
704///
705/// ```text
706/// layout_bfc()
707///   ├─ Check parent border/padding blockers
708///   ├─ For each child:
709///   │   ├─ Check child border/padding blockers
710///   │   ├─ is_first_child?
711///   │   │   └─ Check parent-child top collapse
712///   │   ├─ Sibling collapse?
713///   │   │   └─ advance_pen_with_margin_collapse()
714///   │   │       └─ collapse_margins(prev_bottom, curr_top)
715///   │   ├─ Position child
716///   │   ├─ is_empty_block()?
717///   │   │   └─ Collapse own top+bottom margins (collapse through)
718///   │   └─ Save bottom margin for next sibling
719///   └─ Check parent-child bottom collapse
720/// ```
721///
722/// **Collapsing Rules:**
723///
724/// - Sibling margins: Adjacent vertical margins collapse to max (or sum if mixed signs)
725/// - Parent-child: First child's top margin can escape parent (if no border/padding)
726/// - Parent-child: Last child's bottom margin can escape parent (if no border/padding/height)
727/// - Empty blocks: Top+bottom margins collapse with each other, then with siblings
728/// - Blockers: Border, padding, inline content, or new BFC prevents collapsing
729///
730/// This approach is compliant with the CSS visual formatting model and works within
731/// the constraints of the existing layout engine architecture.
732fn layout_bfc<T: ParsedFontTrait>(
733    ctx: &mut LayoutContext<'_, T>,
734    tree: &mut LayoutTree,
735    text_cache: &mut crate::font_traits::TextLayoutCache,
736    node_index: usize,
737    constraints: &LayoutConstraints,
738    float_cache: &mut std::collections::BTreeMap<usize, FloatingContext>,
739) -> Result<BfcLayoutResult> {
740    let node = tree
741        .get(node_index)
742        .ok_or(LayoutError::InvalidTree)?
743        .clone();
744    let writing_mode = constraints.writing_mode;
745    let mut output = LayoutOutput::default();
746
747    debug_info!(
748        ctx,
749        "\n[layout_bfc] ENTERED for node_index={}, children.len()={}, incoming_bfc_state={}",
750        node_index,
751        node.children.len(),
752        constraints.bfc_state.is_some()
753    );
754
755    // Initialize FloatingContext for this BFC
756    //
757    // We always recalculate float positions in this pass, but we'll store them in the cache
758    // so that subsequent layout passes (for auto-sizing) have access to the positioned floats
759    let mut float_context = FloatingContext::default();
760
761    // Calculate this node's content-box size for use as containing block for children
762    // CSS 2.2 § 10.1: The containing block for in-flow children is formed by the
763    // content edge of the parent's content box.
764    //
765    // We use constraints.available_size directly as this already represents the
766    // content-box available to this node (set by parent). For nodes with explicit
767    // sizes, used_size contains the border-box which we convert to content-box.
768    let mut children_containing_block_size = if let Some(used_size) = node.used_size {
769        // Node has explicit used_size (border-box) - convert to content-box
770        node.box_props.inner_size(used_size, writing_mode)
771    } else {
772        // No used_size yet - use available_size directly (this is already content-box
773        // when coming from parent's layout constraints)
774        constraints.available_size
775    };
776
777    // Proactively reserve space for vertical scrollbar if overflow-y is auto/scroll.
778    // This ensures children are laid out with the correct available width from the start,
779    // preventing the "children overlap scrollbar" layout issue.
780    // Uses per-node CSS `scrollbar-width` + OS overlay preference.
781    let scrollbar_reservation = node
782        .dom_node_id
783        .map(|dom_id| {
784            let styled_node_state = ctx
785                .styled_dom
786                .styled_nodes
787                .as_container()
788                .get(dom_id)
789                .map(|s| s.styled_node_state.clone())
790                .unwrap_or_default();
791            let overflow_y =
792                crate::solver3::getters::get_overflow_y(ctx.styled_dom, dom_id, &styled_node_state);
793            use azul_css::props::layout::LayoutOverflow;
794            match overflow_y.unwrap_or_default() {
795                LayoutOverflow::Scroll | LayoutOverflow::Auto => {
796                    crate::solver3::getters::get_layout_scrollbar_width_px(ctx, dom_id, &styled_node_state)
797                }
798                _ => 0.0,
799            }
800        })
801        .unwrap_or(0.0);
802
803    if scrollbar_reservation > 0.0 {
804        children_containing_block_size.width =
805            (children_containing_block_size.width - scrollbar_reservation).max(0.0);
806    }
807
808    // === Pass 1: Pre-compute child sizes (restored two-pass BFC) ===
809    //
810    // Inspired by Taffy's two-pass approach: first measure, then position.
811    //
812    // This was removed in commit 1a3e5850 and replaced with a single-pass approach
813    // that computed sizes just-in-time during positioning. The single-pass approach
814    // caused regression 8e092a2e because positioning decisions (margin collapsing,
815    // float clearance, available width after floats) depend on knowing ALL sibling
816    // sizes upfront, not just the ones visited so far.
817    //
818    // With the per-node cache (§9.1-§9.2), the re-added Pass 1 is efficient:
819    // - Each child subtree is computed once and stored in NodeCache
820    // - Pass 2 positioning reads sizes from tree nodes (used_size set by Pass 1)
821    // - When calculate_layout_for_subtree recurses into children after layout_bfc
822    //   returns, it hits the per-node cache (same available_size) — O(1) per child.
823    //
824    // Performance: O(n) for the tree. No double-computation thanks to caching.
825    {
826        let mut temp_positions: super::PositionVec = Vec::new();
827        let mut temp_scrollbar_reflow = false;
828
829        for &child_index in &node.children {
830            let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
831            let child_dom_id = child_node.dom_node_id;
832
833            // Skip absolutely/fixed positioned children — they're laid out separately
834            let position_type = get_position_type(ctx.styled_dom, child_dom_id);
835            if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
836                continue;
837            }
838
839            // Compute the child's full subtree layout with temporary positions.
840            // Position (0,0) is intentionally wrong — Pass 1 only cares about sizing.
841            // The correct positions are determined in Pass 2 below.
842            crate::solver3::cache::calculate_layout_for_subtree(
843                ctx,
844                tree,
845                text_cache,
846                child_index,
847                LogicalPosition::zero(),
848                children_containing_block_size,
849                &mut temp_positions,
850                &mut temp_scrollbar_reflow,
851                float_cache,
852                crate::solver3::cache::ComputeMode::ComputeSize,
853            )?;
854        }
855    }
856
857    // === Pass 2: Position children using known sizes ===
858    //
859    // All children now have used_size set from Pass 1. This pass handles:
860    // - Margin collapsing (parent-child + sibling-sibling)
861    // - Float positioning and clearance
862    // - Normal flow block positioning
863
864    let mut main_pen = 0.0f32;
865    let mut max_cross_size = 0.0f32;
866
867    // Track escaped margins separately from content-box height
868    // CSS 2.2 § 8.3.1: Escaped margins don't contribute to parent's content-box height,
869    // but DO affect sibling positioning within the parent
870    let mut total_escaped_top_margin = 0.0f32;
871    // Track all inter-sibling margins (collapsed) - these are also not part of content height
872    let mut total_sibling_margins = 0.0f32;
873
874    // Margin collapsing state
875    let mut last_margin_bottom = 0.0f32;
876    let mut is_first_child = true;
877    let mut first_child_index: Option<usize> = None;
878    let mut last_child_index: Option<usize> = None;
879
880    // Parent's own margins (for escape calculation)
881    let parent_margin_top = node.box_props.margin.main_start(writing_mode);
882    let parent_margin_bottom = node.box_props.margin.main_end(writing_mode);
883
884    // Check if parent (this BFC root) has border/padding that prevents parent-child collapse
885    let parent_has_top_blocker = has_margin_collapse_blocker(&node.box_props, writing_mode, true);
886    let parent_has_bottom_blocker =
887        has_margin_collapse_blocker(&node.box_props, writing_mode, false);
888
889    // Track accumulated top margin for first-child escape
890    let mut accumulated_top_margin = 0.0f32;
891    let mut top_margin_resolved = false;
892    // Track if first child's margin escaped (for return value)
893    let mut top_margin_escaped = false;
894
895    // Track if we have any actual content (non-empty blocks)
896    let mut has_content = false;
897
898    for &child_index in &node.children {
899        let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
900        let child_dom_id = child_node.dom_node_id;
901
902        let position_type = get_position_type(ctx.styled_dom, child_dom_id);
903        if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
904            continue;
905        }
906
907        // Check if this child is a float - if so, position it at current main_pen
908        let is_float = if let Some(node_id) = child_dom_id {
909            let float_type = get_float_property(ctx.styled_dom, Some(node_id));
910
911            if float_type != LayoutFloat::None {
912                // Calculate float size just-in-time if not already computed
913                let float_size = match child_node.used_size {
914                    Some(size) => size,
915                    None => {
916                        let intrinsic = child_node.intrinsic_sizes.unwrap_or_default();
917                        let computed_size = crate::solver3::sizing::calculate_used_size_for_node(
918                            ctx.styled_dom,
919                            child_dom_id,
920                            children_containing_block_size,
921                            intrinsic,
922                            &child_node.box_props,
923                            ctx.viewport_size,
924                        )?;
925                        if let Some(node_mut) = tree.get_mut(child_index) {
926                            node_mut.used_size = Some(computed_size);
927                        }
928                        computed_size
929                    }
930                };
931                // Re-borrow after potential mutation
932                let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
933                let float_margin = &child_node.box_props.margin;
934
935                // CSS 2.2 § 9.5: Float margins don't collapse with any other margins.
936                // If there's a previous in-flow element with a bottom margin, we must
937                // include it in the Y position calculation for this float.
938                let float_y = main_pen + last_margin_bottom;
939
940                debug_info!(
941                    ctx,
942                    "[layout_bfc] Positioning float: index={}, type={:?}, size={:?}, at Y={} \
943                     (main_pen={} + last_margin={})",
944                    child_index,
945                    float_type,
946                    float_size,
947                    float_y,
948                    main_pen,
949                    last_margin_bottom
950                );
951
952                // Position the float at the CURRENT main_pen + last margin (respects DOM order!)
953                let float_rect = position_float(
954                    &float_context,
955                    float_type,
956                    float_size,
957                    float_margin,
958                    // Include last_margin_bottom since float margins don't collapse!
959                    float_y,
960                    constraints.available_size.cross(writing_mode),
961                    writing_mode,
962                );
963
964                debug_info!(ctx, "[layout_bfc] Float positioned at: {:?}", float_rect);
965
966                // Add to float context BEFORE positioning next element
967                float_context.add_float(float_type, float_rect, *float_margin);
968
969                // Store position in output
970                output.positions.insert(child_index, float_rect.origin);
971
972                debug_info!(
973                    ctx,
974                    "[layout_bfc] *** FLOAT POSITIONED: child={}, main_pen={} (unchanged - floats \
975                     don't advance pen)",
976                    child_index,
977                    main_pen
978                );
979
980                // Floats are taken out of normal flow - DON'T advance main_pen
981                // Continue to next child
982                continue;
983            }
984            false
985        } else {
986            false
987        };
988
989        // Early exit for floats (already handled above)
990        if is_float {
991            continue;
992        }
993
994        // From here: normal flow (non-float) children only
995
996        // Track first and last in-flow children for parent-child collapse
997        if first_child_index.is_none() {
998            first_child_index = Some(child_index);
999        }
1000        last_child_index = Some(child_index);
1001
1002        // Calculate child's used_size just-in-time if not already computed
1003        // This replaces the old "Pass 1" that recursively laid out grandchildren with wrong positions
1004        let child_size = match child_node.used_size {
1005            Some(size) => size,
1006            None => {
1007                // Calculate size without recursive layout
1008                let intrinsic = child_node.intrinsic_sizes.unwrap_or_default();
1009                let child_used_size = crate::solver3::sizing::calculate_used_size_for_node(
1010                    ctx.styled_dom,
1011                    child_dom_id,
1012                    children_containing_block_size,
1013                    intrinsic,
1014                    &child_node.box_props,
1015                    ctx.viewport_size,
1016                )?;
1017                // Update the node with computed size (we need to re-borrow mutably)
1018                if let Some(node_mut) = tree.get_mut(child_index) {
1019                    node_mut.used_size = Some(child_used_size);
1020                }
1021                child_used_size
1022            }
1023        };
1024        // Re-borrow child_node after potential mutation
1025        let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1026        let child_margin = &child_node.box_props.margin;
1027
1028        debug_info!(
1029            ctx,
1030            "[layout_bfc] Child {} margin from box_props: top={}, right={}, bottom={}, left={}",
1031            child_index,
1032            child_margin.top,
1033            child_margin.right,
1034            child_margin.bottom,
1035            child_margin.left
1036        );
1037
1038        // IMPORTANT: Use the ACTUAL margins from box_props, NOT escaped margins!
1039        //
1040        // Escaped margins are only relevant for the parent-child relationship WITHIN a node's
1041        // own BFC layout. When positioning this child in ITS parent's BFC, we use its actual
1042        // margins. CSS 2.2 § 8.3.1: Margin collapsing happens between ADJACENT margins,
1043        // which means:
1044        //
1045        // - Parent's top and first child's top (if no blocker)
1046        // - Sibling's bottom and next sibling's top
1047        // - Parent's bottom and last child's bottom (if no blocker)
1048        //
1049        // The escaped_top_margin stored in the child node is for its OWN children, not for itself!
1050        let child_margin_top = child_margin.main_start(writing_mode);
1051        let child_margin_bottom = child_margin.main_end(writing_mode);
1052
1053        debug_info!(
1054            ctx,
1055            "[layout_bfc] Child {} final margins: margin_top={}, margin_bottom={}",
1056            child_index,
1057            child_margin_top,
1058            child_margin_bottom
1059        );
1060
1061        // Check if this child has border/padding that prevents margin collapsing
1062        let child_has_top_blocker =
1063            has_margin_collapse_blocker(&child_node.box_props, writing_mode, true);
1064        let child_has_bottom_blocker =
1065            has_margin_collapse_blocker(&child_node.box_props, writing_mode, false);
1066
1067        // Check for clear property FIRST - clearance affects whether element is considered empty
1068        // CSS 2.2 § 9.5.2: "Clearance inhibits margin collapsing"
1069        // An element with clearance is NOT empty even if it has no content
1070        let child_clear = if let Some(node_id) = child_dom_id {
1071            get_clear_property(ctx.styled_dom, Some(node_id))
1072        } else {
1073            LayoutClear::None
1074        };
1075        debug_info!(
1076            ctx,
1077            "[layout_bfc] Child {} clear property: {:?}",
1078            child_index,
1079            child_clear
1080        );
1081
1082        // PHASE 1: Empty Block Detection & Self-Collapse
1083        let is_empty = is_empty_block(child_node);
1084
1085        // Handle empty blocks FIRST (they collapse through and don't participate in layout)
1086        // EXCEPTION: Elements with clear property are NOT skipped even if empty!
1087        // CSS 2.2 § 9.5.2: Clear property affects positioning even for empty elements
1088        if is_empty
1089            && !child_has_top_blocker
1090            && !child_has_bottom_blocker
1091            && child_clear == LayoutClear::None
1092        {
1093            // Empty block: collapse its own top and bottom margins FIRST
1094            let self_collapsed = collapse_margins(child_margin_top, child_margin_bottom);
1095
1096            // Then collapse with previous margin (sibling or parent)
1097            if is_first_child {
1098                is_first_child = false;
1099                // Empty first child: its collapsed margin can escape with parent's
1100                if !parent_has_top_blocker {
1101                    accumulated_top_margin = collapse_margins(parent_margin_top, self_collapsed);
1102                } else {
1103                    // Parent has blocker: add margins
1104                    if accumulated_top_margin == 0.0 {
1105                        accumulated_top_margin = parent_margin_top;
1106                    }
1107                    main_pen += accumulated_top_margin + self_collapsed;
1108                    top_margin_resolved = true;
1109                    accumulated_top_margin = 0.0;
1110                }
1111                last_margin_bottom = self_collapsed;
1112            } else {
1113                // Empty sibling: collapse with previous sibling's bottom margin
1114                last_margin_bottom = collapse_margins(last_margin_bottom, self_collapsed);
1115            }
1116
1117            // Skip positioning and pen advance (empty has no visual presence)
1118            continue;
1119        }
1120
1121        // From here on: non-empty blocks only (or empty blocks with clear property)
1122
1123        // Apply clearance if needed
1124        // CSS 2.2 § 9.5.2: Clearance inhibits margin collapsing
1125        let clearance_applied = if child_clear != LayoutClear::None {
1126            let cleared_offset =
1127                float_context.clearance_offset(child_clear, main_pen, writing_mode);
1128            debug_info!(
1129                ctx,
1130                "[layout_bfc] Child {} clearance check: cleared_offset={}, main_pen={}",
1131                child_index,
1132                cleared_offset,
1133                main_pen
1134            );
1135            if cleared_offset > main_pen {
1136                debug_info!(
1137                    ctx,
1138                    "[layout_bfc] Applying clearance: child={}, clear={:?}, old_pen={}, new_pen={}",
1139                    child_index,
1140                    child_clear,
1141                    main_pen,
1142                    cleared_offset
1143                );
1144                main_pen = cleared_offset;
1145                true // Signal that clearance was applied
1146            } else {
1147                false
1148            }
1149        } else {
1150            false
1151        };
1152
1153        // PHASE 2: Parent-Child Top Margin Escape (First Child)
1154        //
1155        // CSS 2.2 § 8.3.1: "The top margin of a box is adjacent to the top margin of its first
1156        // in-flow child if the box has no top border, no top padding, and the child has no
1157        // clearance." CSS 2.2 § 9.5.2: "Clearance inhibits margin collapsing"
1158
1159        if is_first_child {
1160            is_first_child = false;
1161
1162            // Clearance prevents collapse (acts as invisible blocker)
1163            if clearance_applied {
1164                // Clearance inhibits all margin collapsing for this element
1165                // The clearance has already positioned main_pen past floats
1166                //
1167                // CSS 2.2 § 8.3.1: Parent's margin was already handled by parent's parent BFC
1168                // We only add child's margin in our content-box coordinate space
1169                main_pen += child_margin_top;
1170                debug_info!(
1171                    ctx,
1172                    "[layout_bfc] First child {} with CLEARANCE: no collapse, child_margin={}, \
1173                     main_pen={}",
1174                    child_index,
1175                    child_margin_top,
1176                    main_pen
1177                );
1178            } else if !parent_has_top_blocker {
1179                // Margin Escape Case
1180                //
1181                // CSS 2.2 § 8.3.1: "The top margin of an in-flow block element collapses with
1182                // its first in-flow block-level child's top margin if the element has no top
1183                // border, no top padding, and the child has no clearance."
1184                //
1185                // When margins collapse, they "escape" upward through the parent to be resolved
1186                // in the grandparent's coordinate space. This is critical for understanding the
1187                // coordinate system separation:
1188                //
1189                // Example:
1190                // <body padding=20>
1191                //  <div margin=0>
1192                //      <div margin=30></div>
1193                //  </div>
1194                // </body>
1195                //
1196                //   - Middle div (our parent) has no padding → margins can escape
1197                //   - Inner div's 30px margin collapses with middle div's 0px margin = 30px
1198                //   - This 30px margin "escapes" to be handled by body's BFC
1199                //   - Body positions middle div at Y=30 (relative to body's content-box)
1200                //   - Middle div's content-box height does NOT include the escaped 30px
1201                //   - Inner div is positioned at Y=0 in middle div's content-box
1202                //
1203                // **NOTE**: This is a subtle but critical distinction in coordinate systems:
1204                //
1205                //   - Parent's margin belongs to grandparent's coordinate space
1206                //   - Child's margin (when escaped) also belongs to grandparent's coordinate space
1207                //   - They collapse BEFORE entering this BFC's coordinate space
1208                //   - We return the collapsed margin so grandparent can position parent correctly
1209                //
1210                // **NOTE**: Child's own blocker status (padding/border) is IRRELEVANT for
1211                // parent-child  collapse. The child may have padding that prevents
1212                // collapse with ITS OWN  children, but this doesn't prevent its
1213                // margin from escaping  through its parent.
1214                //
1215                // **NOTE**: Previously, we incorrectly added parent_margin_top to main_pen in
1216                //  the blocked case, which double-counted the margin by mixing
1217                //  coordinate systems. The parent's margin is NEVER in our (the
1218                //  parent's content-box) coordinate system!
1219                //
1220                // We collapse the parent's margin with the child's margin.
1221                // This combined margin is what "escapes" to the grandparent.
1222                // The grandparent uses this to position the parent.
1223                //
1224                // Effectively, we are saying "The parent starts here, but its effective
1225                // top margin is now max(parent_margin, child_margin)".
1226
1227                accumulated_top_margin = collapse_margins(parent_margin_top, child_margin_top);
1228                top_margin_resolved = true;
1229                top_margin_escaped = true;
1230
1231                // Track escaped margin so it gets subtracted from content-box height
1232                // The escaped margin is NOT part of our content-box - it belongs to our
1233                // parent's parent
1234                total_escaped_top_margin = accumulated_top_margin;
1235
1236                // Position child at pen (no margin applied - it escaped!)
1237                debug_info!(
1238                    ctx,
1239                    "[layout_bfc] First child {} margin ESCAPES: parent_margin={}, \
1240                     child_margin={}, collapsed={}, total_escaped={}",
1241                    child_index,
1242                    parent_margin_top,
1243                    child_margin_top,
1244                    accumulated_top_margin,
1245                    total_escaped_top_margin
1246                );
1247            } else {
1248                // Margin Blocked Case
1249                //
1250                // CSS 2.2 § 8.3.1: "no top padding and no top border" required for collapse.
1251                // When padding or border exists, margins do NOT collapse and exist in different
1252                // coordinate spaces.
1253                //
1254                // CRITICAL COORDINATE SYSTEM SEPARATION:
1255                //
1256                //   This is where the architecture becomes subtle. When layout_bfc() is called:
1257                //   1. We are INSIDE the parent's content-box coordinate space (main_pen starts at
1258                //      0)
1259                //   2. The parent's own margin was ALREADY RESOLVED by the grandparent's BFC
1260                //   3. The parent's margin is in the grandparent's coordinate space, not ours
1261                //   4. We NEVER reference the parent's margin in this BFC - it's outside our scope
1262                //
1263                // Example:
1264                //
1265                // <body padding=20>
1266                //   <div margin=30 padding=20>
1267                //      <div margin=30></div>
1268                //   </div>
1269                // </body>
1270                //
1271                //   - Middle div has padding=20 → blocker exists, margins don't collapse
1272                //   - Body's BFC positions middle div at Y=30 (middle div's margin, in body's
1273                //     space)
1274                //   - Middle div's BFC starts at its content-box (after the padding)
1275                //   - main_pen=0 at the top of middle div's content-box
1276                //   - Inner div has margin=30 → we add 30 to main_pen (in OUR coordinate space)
1277                //   - Inner div positioned at Y=30 (relative to middle div's content-box)
1278                //   - Absolute position: 20 (body padding) + 30 (middle margin) + 20 (middle
1279                //     padding) + 30 (inner margin) = 100px
1280                //
1281                // **NOTE**: Previous code incorrectly added parent_margin_top to main_pen here:
1282                //
1283                //     - main_pen += parent_margin_top;  // WRONG! Mixes coordinate systems
1284                //     - main_pen += child_margin_top;
1285                //
1286                //   This caused the "double margin" bug where margins were applied twice:
1287                //
1288                //   - Once by grandparent positioning parent (correct)
1289                //   - Again inside parent's BFC (INCORRECT - wrong coordinate system)
1290                //
1291                //   The parent's margin belongs to GRANDPARENT's coordinate space and was already
1292                //   used to position the parent. Adding it again here is like adding feet to
1293                //   meters.
1294                //
1295                //   We ONLY add the child's margin in our (parent's content-box) coordinate space.
1296                //   The parent's margin is irrelevant to us - it's outside our scope.
1297
1298                main_pen += child_margin_top;
1299                debug_info!(
1300                    ctx,
1301                    "[layout_bfc] First child {} BLOCKED: parent_has_blocker={}, advanced by \
1302                     child_margin={}, main_pen={}",
1303                    child_index,
1304                    parent_has_top_blocker,
1305                    child_margin_top,
1306                    main_pen
1307                );
1308            }
1309        } else {
1310            // Not first child: handle sibling collapse
1311            // CSS 2.2 § 8.3.1 Rule 1: "Vertical margins of adjacent block boxes in the normal flow
1312            // collapse" CSS 2.2 § 9.5.2: "Clearance inhibits margin collapsing"
1313
1314            // Resolve accumulated top margin if not yet done (for parent's first in-flow child)
1315            if !top_margin_resolved {
1316                main_pen += accumulated_top_margin;
1317                top_margin_resolved = true;
1318                debug_info!(
1319                    ctx,
1320                    "[layout_bfc] RESOLVED top margin for node {} at sibling {}: accumulated={}, \
1321                     main_pen={}",
1322                    node_index,
1323                    child_index,
1324                    accumulated_top_margin,
1325                    main_pen
1326                );
1327            }
1328
1329            if clearance_applied {
1330                // Clearance inhibits collapsing - add full margin
1331                main_pen += child_margin_top;
1332                debug_info!(
1333                    ctx,
1334                    "[layout_bfc] Child {} with CLEARANCE: no collapse with sibling, \
1335                     child_margin_top={}, main_pen={}",
1336                    child_index,
1337                    child_margin_top,
1338                    main_pen
1339                );
1340            } else {
1341                // Sibling Margin Collapse
1342                //
1343                // CSS 2.2 § 8.3.1: "Vertical margins of adjacent block boxes in the normal
1344                // flow collapse." The collapsed margin is the maximum of the two margins.
1345                //
1346                // IMPORTANT: Sibling margins ARE part of the parent's content-box height!
1347                //
1348                // Unlike escaped margins (which belong to grandparent's space), sibling margins
1349                // are the space BETWEEN children within our content-box.
1350                //
1351                // Example:
1352                //
1353                // <div>
1354                //  <div margin-bottom=30></div>
1355                //  <div margin-top=40></div>
1356                // </div>
1357                //
1358                //   - First child ends at Y=100 (including its content + margins)
1359                //   - Collapsed margin = max(30, 40) = 40px
1360                //   - Second child starts at Y=140 (100 + 40)
1361                //   - Parent's content-box height includes this 40px gap
1362                //
1363                // We track total_sibling_margins for debugging, but NOTE: we do **not**
1364                // subtract these from content-box height! They are part of the layout space.
1365                //
1366                // Previously we subtracted total_sibling_margins from content-box height:
1367                //
1368                //   content_box_height = main_pen - total_escaped_top_margin -
1369                // total_sibling_margins;
1370                //
1371                // This was wrong because sibling margins are between boxes (part of content),
1372                // not outside boxes (like escaped margins).
1373
1374                let collapsed = collapse_margins(last_margin_bottom, child_margin_top);
1375                main_pen += collapsed;
1376                total_sibling_margins += collapsed;
1377                debug_info!(
1378                    ctx,
1379                    "[layout_bfc] Sibling collapse for child {}: last_margin_bottom={}, \
1380                     child_margin_top={}, collapsed={}, main_pen={}, total_sibling_margins={}",
1381                    child_index,
1382                    last_margin_bottom,
1383                    child_margin_top,
1384                    collapsed,
1385                    main_pen,
1386                    total_sibling_margins
1387                );
1388            }
1389        }
1390
1391        // Position child (non-empty blocks only reach here)
1392        //
1393        // CSS 2.2 § 9.4.1: "In a block formatting context, each box's left outer edge touches
1394        // the left edge of the containing block (for right-to-left formatting, right edges touch).
1395        // This is true even in the presence of floats (although a box's line boxes may shrink
1396        // due to the floats), unless the box establishes a new block formatting context
1397        // (in which case the box itself may become narrower due to the floats)."
1398        //
1399        // CSS 2.2 § 9.5: "The border box of a table, a block-level replaced element, or an element
1400        // in the normal flow that establishes a new block formatting context (such as an element
1401        // with 'overflow' other than 'visible') must not overlap any floats in the same block
1402        // formatting context as the element itself."
1403
1404        let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1405        let establishes_bfc = establishes_new_bfc(ctx, child_node);
1406
1407        // Query available space considering floats ONLY if child establishes new BFC
1408        let (cross_start, cross_end, available_cross) = if establishes_bfc {
1409            // New BFC: Must shrink or move down to avoid overlapping floats
1410            let (start, end) = float_context.available_line_box_space(
1411                main_pen,
1412                main_pen + child_size.main(writing_mode),
1413                constraints.available_size.cross(writing_mode),
1414                writing_mode,
1415            );
1416            let available = end - start;
1417
1418            debug_info!(
1419                ctx,
1420                "[layout_bfc] Child {} establishes BFC: shrinking to avoid floats, \
1421                 cross_range={}..{}, available_cross={}",
1422                child_index,
1423                start,
1424                end,
1425                available
1426            );
1427
1428            (start, end, available)
1429        } else {
1430            // Normal flow: Overlaps floats, positioned at full width
1431            // Only the child's INLINE CONTENT (if any) wraps around floats
1432            let start = 0.0;
1433            let end = constraints.available_size.cross(writing_mode);
1434            let available = end - start;
1435
1436            debug_info!(
1437                ctx,
1438                "[layout_bfc] Child {} is normal flow: overlapping floats at full width, \
1439                 available_cross={}",
1440                child_index,
1441                available
1442            );
1443
1444            (start, end, available)
1445        };
1446
1447        // Get child's margin, margin_auto, size, and formatting context
1448        let (child_margin_cloned, child_margin_auto, child_used_size, is_inline_fc, child_dom_id_for_debug) = {
1449            let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1450            (
1451                child_node.box_props.margin.clone(),
1452                child_node.box_props.margin_auto,
1453                child_node.used_size.unwrap_or_default(),
1454                child_node.formatting_context == FormattingContext::Inline,
1455                child_node.dom_node_id,
1456            )
1457        };
1458        let child_margin = &child_margin_cloned;
1459
1460        debug_info!(
1461            ctx,
1462            "[layout_bfc] Child {} margin_auto: left={}, right={}, top={}, bottom={}",
1463            child_index,
1464            child_margin_auto.left,
1465            child_margin_auto.right,
1466            child_margin_auto.top,
1467            child_margin_auto.bottom
1468        );
1469        debug_info!(
1470            ctx,
1471            "[layout_bfc] Child {} used_size: width={}, height={}",
1472            child_index,
1473            child_used_size.width,
1474            child_used_size.height
1475        );
1476
1477        // Position child
1478        // For normal flow blocks (including IFCs): position at full width (cross_start = 0)
1479        // For BFC-establishing blocks: position in available space between floats
1480        //
1481        // CSS 2.2 § 10.3.3: If margin-left and margin-right are both auto, 
1482        // their used values are equal, centering the element horizontally.
1483        
1484        let (child_cross_pos, mut child_main_pos) = if establishes_bfc {
1485            // BFC: Position in space between floats
1486            (
1487                cross_start + child_margin.cross_start(writing_mode),
1488                main_pen,
1489            )
1490        } else {
1491            // Normal flow: Check for margin: auto centering
1492            let available_cross = constraints.available_size.cross(writing_mode);
1493            let child_cross_size = child_used_size.cross(writing_mode);
1494            
1495            debug_info!(
1496                ctx,
1497                "[layout_bfc] Child {} centering check: available_cross={}, child_cross_size={}, margin_auto.left={}, margin_auto.right={}",
1498                child_index,
1499                available_cross,
1500                child_cross_size,
1501                child_margin_auto.left,
1502                child_margin_auto.right
1503            );
1504            
1505            // CSS 2.2 § 10.3.3: If both margin-left and margin-right are auto,
1506            // center the element within the available space
1507            let cross_pos = if child_margin_auto.left && child_margin_auto.right {
1508                // Center: (available - child_width) / 2
1509                let remaining_space = (available_cross - child_cross_size).max(0.0);
1510                debug_info!(
1511                    ctx,
1512                    "[layout_bfc] Child {} CENTERING: remaining_space={}, cross_pos={}",
1513                    child_index,
1514                    remaining_space,
1515                    remaining_space / 2.0
1516                );
1517                remaining_space / 2.0
1518            } else if child_margin_auto.left {
1519                // Only left is auto: push element to the right
1520                let remaining_space = (available_cross - child_cross_size - child_margin.right).max(0.0);
1521                debug_info!(
1522                    ctx,
1523                    "[layout_bfc] Child {} margin-left:auto only, pushing right: remaining_space={}",
1524                    child_index,
1525                    remaining_space
1526                );
1527                remaining_space
1528            } else if child_margin_auto.right {
1529                // Only right is auto: element stays at left with its margin
1530                debug_info!(
1531                    ctx,
1532                    "[layout_bfc] Child {} margin-right:auto only, using left margin={}",
1533                    child_index,
1534                    child_margin.cross_start(writing_mode)
1535                );
1536                child_margin.cross_start(writing_mode)
1537            } else {
1538                // No auto margins: use normal margin
1539                debug_info!(
1540                    ctx,
1541                    "[layout_bfc] Child {} NO auto margins, using left margin={}",
1542                    child_index,
1543                    child_margin.cross_start(writing_mode)
1544                );
1545                child_margin.cross_start(writing_mode)
1546            };
1547            
1548            (cross_pos, main_pen)
1549        };
1550
1551        // NOTE: We do NOT adjust child_main_pos based on child's escaped_top_margin here!
1552        // The escaped_top_margin represents margins that escaped FROM the child's own children.
1553        // The child's position in THIS BFC is determined by main_pen and the child's own margin
1554        // (which was already handled in the margin collapse logic above).
1555        //
1556        // Previously, this code incorrectly added child_escaped_margin to child_main_pos,
1557        // which caused double-application of margins because:
1558        // 1. The child's margin was used to calculate its position in THIS BFC
1559        // 2. Then its escaped_top_margin (which included its own margin) was added again
1560        //
1561        // The correct behavior per CSS 2.2 § 8.3.1 is:
1562        // - The child's escaped_top_margin is used by THIS node's parent to position THIS node
1563        // - It does NOT affect how we position the child within our content-box
1564
1565        // final_pos is [CoordinateSpace::Parent] - relative to this BFC's content-box
1566        let final_pos =
1567            LogicalPosition::from_main_cross(child_main_pos, child_cross_pos, writing_mode);
1568
1569        debug_info!(
1570            ctx,
1571            "[layout_bfc] *** NORMAL FLOW BLOCK POSITIONED: child={}, final_pos={:?}, \
1572             main_pen={}, establishes_bfc={}",
1573            child_index,
1574            final_pos,
1575            main_pen,
1576            establishes_bfc
1577        );
1578
1579        // Re-layout IFC children with float context for correct text wrapping
1580        // Normal flow blocks WITH inline content need float context propagated
1581        if is_inline_fc && !establishes_bfc {
1582            // Use cached floats if available (from previous layout passes),
1583            // otherwise use the floats positioned in this pass
1584            let floats_for_ifc = float_cache.get(&node_index).unwrap_or(&float_context);
1585
1586            debug_info!(
1587                ctx,
1588                "[layout_bfc] Re-layouting IFC child {} (normal flow) with parent's float context \
1589                 at Y={}, child_cross_pos={}",
1590                child_index,
1591                main_pen,
1592                child_cross_pos
1593            );
1594            debug_info!(
1595                ctx,
1596                "[layout_bfc]   Using {} floats (from cache: {})",
1597                floats_for_ifc.floats.len(),
1598                float_cache.contains_key(&node_index)
1599            );
1600
1601            // Translate float coordinates from BFC-relative to IFC-relative
1602            // The IFC child is positioned at (child_cross_pos, main_pen) in BFC coordinates
1603            // Floats need to be relative to the IFC's CONTENT-BOX origin (inside padding/border)
1604            let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1605            let padding_border_cross = child_node.box_props.padding.cross_start(writing_mode)
1606                + child_node.box_props.border.cross_start(writing_mode);
1607            let padding_border_main = child_node.box_props.padding.main_start(writing_mode)
1608                + child_node.box_props.border.main_start(writing_mode);
1609
1610            // Content-box origin in BFC coordinates
1611            let content_box_cross = child_cross_pos + padding_border_cross;
1612            let content_box_main = main_pen + padding_border_main;
1613
1614            debug_info!(
1615                ctx,
1616                "[layout_bfc]   Border-box at ({}, {}), Content-box at ({}, {}), \
1617                 padding+border=({}, {})",
1618                child_cross_pos,
1619                main_pen,
1620                content_box_cross,
1621                content_box_main,
1622                padding_border_cross,
1623                padding_border_main
1624            );
1625
1626            let mut ifc_floats = FloatingContext::default();
1627            for float_box in &floats_for_ifc.floats {
1628                // Convert float position from BFC coords to IFC CONTENT-BOX relative coords
1629                let float_rel_to_ifc = LogicalRect {
1630                    origin: LogicalPosition {
1631                        x: float_box.rect.origin.x - content_box_cross,
1632                        y: float_box.rect.origin.y - content_box_main,
1633                    },
1634                    size: float_box.rect.size,
1635                };
1636
1637                debug_info!(
1638                    ctx,
1639                    "[layout_bfc] Float {:?}: BFC coords = {:?}, IFC-content-relative = {:?}",
1640                    float_box.kind,
1641                    float_box.rect,
1642                    float_rel_to_ifc
1643                );
1644
1645                ifc_floats.add_float(float_box.kind, float_rel_to_ifc, float_box.margin);
1646            }
1647
1648            // Create a BfcState with IFC-relative float coordinates
1649            let mut bfc_state = BfcState {
1650                pen: LogicalPosition::zero(), // IFC starts at its own origin
1651                floats: ifc_floats.clone(),
1652                margins: MarginCollapseContext::default(),
1653            };
1654
1655            debug_info!(
1656                ctx,
1657                "[layout_bfc]   Created IFC-relative FloatingContext with {} floats",
1658                ifc_floats.floats.len()
1659            );
1660
1661            // Get the IFC child's content-box size (after padding/border)
1662            let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1663            let child_dom_id = child_node.dom_node_id;
1664
1665            // For inline elements (display: inline), use containing block width as available
1666            // width. Inline elements flow within the containing block and wrap at its width.
1667            // CSS 2.2 § 10.3.1: For inline elements, available width = containing block width.
1668            let display = get_display_property(ctx.styled_dom, child_dom_id).unwrap_or_default();
1669            let child_content_size = if display == LayoutDisplay::Inline {
1670                // Inline elements use the containing block's content-box width
1671                LogicalSize::new(
1672                    children_containing_block_size.width,
1673                    children_containing_block_size.height,
1674                )
1675            } else {
1676                // Block-level elements use their own content-box
1677                child_node.box_props.inner_size(child_size, writing_mode)
1678            };
1679
1680            debug_info!(
1681                ctx,
1682                "[layout_bfc]   IFC child size: border-box={:?}, content-box={:?}",
1683                child_size,
1684                child_content_size
1685            );
1686
1687            // Create new constraints with float context
1688            // IMPORTANT: Use the child's CONTENT-BOX width, not the BFC width!
1689            let ifc_constraints = LayoutConstraints {
1690                available_size: child_content_size,
1691                bfc_state: Some(&mut bfc_state),
1692                writing_mode,
1693                text_align: constraints.text_align,
1694                containing_block_size: constraints.containing_block_size,
1695                available_width_type: Text3AvailableSpace::Definite(child_content_size.width),
1696            };
1697
1698            // Re-layout the IFC with float awareness
1699            // This will pass floats as exclusion zones to text3 for line wrapping
1700            let ifc_result = layout_formatting_context(
1701                ctx,
1702                tree,
1703                text_cache,
1704                child_index,
1705                &ifc_constraints,
1706                float_cache,
1707            )?;
1708
1709            // DON'T update used_size - the box keeps its full width!
1710            // Only the text layout inside changes to wrap around floats
1711
1712            debug_info!(
1713                ctx,
1714                "[layout_bfc] IFC child {} re-layouted with float context (text will wrap, box \
1715                 stays full width)",
1716                child_index
1717            );
1718
1719            // NOTE: We do NOT merge inline-block positions from the IFC's output.positions here!
1720            // The IFC's inline-block children will be correctly positioned when 
1721            // calculate_layout_for_subtree recursively processes the IFC node (child_index).
1722            // At that point, layout_ifc will be called again, and the inline-block positions
1723            // will be relative to the IFC's content-box, which is what we want.
1724            //
1725            // Merging them here would cause them to be processed by process_inflow_child
1726            // with the BFC's content-box position (self_content_box_pos of the BFC), 
1727            // resulting in incorrect absolute positions.
1728        }
1729
1730        output.positions.insert(child_index, final_pos);
1731
1732        // Advance the pen past the child's content size
1733        // CSS margin collapse: escaped margins are handled via accumulated_top_margin
1734        // at the START of layout, not by adjusting positions after layout.
1735        // We simply advance by the child's actual size.
1736        main_pen += child_size.main(writing_mode);
1737        has_content = true;
1738
1739        // Update last margin for next sibling
1740        // CSS 2.2 § 8.3.1: The bottom margin of this box will collapse with the top margin
1741        // of the next sibling (if no clearance or blockers intervene)
1742        // CSS 2.2 § 9.5.2: If clearance was applied, margin collapsing is inhibited
1743        if clearance_applied {
1744            // Clearance inhibits collapse - next sibling starts fresh
1745            last_margin_bottom = 0.0;
1746        } else {
1747            last_margin_bottom = child_margin_bottom;
1748        }
1749
1750        debug_info!(
1751            ctx,
1752            "[layout_bfc] Child {} positioned at final_pos={:?}, size={:?}, advanced main_pen to \
1753             {}, last_margin_bottom={}, clearance_applied={}",
1754            child_index,
1755            final_pos,
1756            child_size,
1757            main_pen,
1758            last_margin_bottom,
1759            clearance_applied
1760        );
1761
1762        // Track the maximum cross-axis size to determine the BFC's overflow size.
1763        let child_cross_extent =
1764            child_cross_pos + child_size.cross(writing_mode) + child_margin.cross_end(writing_mode);
1765        max_cross_size = max_cross_size.max(child_cross_extent);
1766    }
1767
1768    // Store the float context in cache for future layout passes
1769    // This happens after ALL children (floats and normal) have been positioned
1770    debug_info!(
1771        ctx,
1772        "[layout_bfc] Storing {} floats in cache for node {}",
1773        float_context.floats.len(),
1774        node_index
1775    );
1776    float_cache.insert(node_index, float_context.clone());
1777
1778    // PHASE 3: Parent-Child Bottom Margin Escape
1779    let mut escaped_top_margin = None;
1780    let mut escaped_bottom_margin = None;
1781
1782    // Handle top margin escape
1783    if top_margin_escaped {
1784        // First child's margin escaped through parent
1785        escaped_top_margin = Some(accumulated_top_margin);
1786        debug_info!(
1787            ctx,
1788            "[layout_bfc] Returning escaped top margin: accumulated={}, node={}",
1789            accumulated_top_margin,
1790            node_index
1791        );
1792    } else if !top_margin_resolved && accumulated_top_margin > 0.0 {
1793        // No content was positioned, all margins accumulated (empty blocks)
1794        escaped_top_margin = Some(accumulated_top_margin);
1795        debug_info!(
1796            ctx,
1797            "[layout_bfc] Escaping top margin (no content): accumulated={}, node={}",
1798            accumulated_top_margin,
1799            node_index
1800        );
1801    } else if !top_margin_resolved {
1802        // Unusual case: no content, zero margin
1803        escaped_top_margin = Some(accumulated_top_margin);
1804        debug_info!(
1805            ctx,
1806            "[layout_bfc] Escaping top margin (zero, no content): accumulated={}, node={}",
1807            accumulated_top_margin,
1808            node_index
1809        );
1810    } else {
1811        debug_info!(
1812            ctx,
1813            "[layout_bfc] NOT escaping top margin: top_margin_resolved={}, escaped={}, \
1814             accumulated={}, node={}",
1815            top_margin_resolved,
1816            top_margin_escaped,
1817            accumulated_top_margin,
1818            node_index
1819        );
1820    }
1821
1822    // Handle bottom margin escape
1823    if let Some(last_idx) = last_child_index {
1824        let last_child = tree.get(last_idx).ok_or(LayoutError::InvalidTree)?;
1825        let last_has_bottom_blocker =
1826            has_margin_collapse_blocker(&last_child.box_props, writing_mode, false);
1827
1828        debug_info!(
1829            ctx,
1830            "[layout_bfc] Bottom margin for node {}: parent_has_bottom_blocker={}, \
1831             last_has_bottom_blocker={}, last_margin_bottom={}, main_pen_before={}",
1832            node_index,
1833            parent_has_bottom_blocker,
1834            last_has_bottom_blocker,
1835            last_margin_bottom,
1836            main_pen
1837        );
1838
1839        if !parent_has_bottom_blocker && !last_has_bottom_blocker && has_content {
1840            // Last child's bottom margin can escape
1841            let collapsed_bottom = collapse_margins(parent_margin_bottom, last_margin_bottom);
1842            escaped_bottom_margin = Some(collapsed_bottom);
1843            debug_info!(
1844                ctx,
1845                "[layout_bfc] Bottom margin ESCAPED for node {}: collapsed={}",
1846                node_index,
1847                collapsed_bottom
1848            );
1849            // Don't add last_margin_bottom to pen (it escaped)
1850        } else {
1851            // Can't escape: add to pen
1852            main_pen += last_margin_bottom;
1853            // NOTE: We do NOT add parent_margin_bottom to main_pen here!
1854            // parent_margin_bottom is added OUTSIDE the content-box (in the margin-box)
1855            // The content-box height should only include children's content and margins
1856            debug_info!(
1857                ctx,
1858                "[layout_bfc] Bottom margin BLOCKED for node {}: added last_margin_bottom={}, \
1859                 main_pen_after={}",
1860                node_index,
1861                last_margin_bottom,
1862                main_pen
1863            );
1864        }
1865    } else {
1866        // No children: just use parent's margins
1867        if !top_margin_resolved {
1868            main_pen += parent_margin_top;
1869        }
1870        main_pen += parent_margin_bottom;
1871    }
1872
1873    // CRITICAL: If this is a root node (no parent), apply escaped margins directly
1874    // instead of propagating them upward (since there's no parent to receive them)
1875    let is_root_node = node.parent.is_none();
1876    if is_root_node {
1877        if let Some(top) = escaped_top_margin {
1878            // Adjust all child positions downward by the escaped top margin
1879            for (_, pos) in output.positions.iter_mut() {
1880                let current_main = pos.main(writing_mode);
1881                *pos = LogicalPosition::from_main_cross(
1882                    current_main + top,
1883                    pos.cross(writing_mode),
1884                    writing_mode,
1885                );
1886            }
1887            main_pen += top;
1888        }
1889        if let Some(bottom) = escaped_bottom_margin {
1890            main_pen += bottom;
1891        }
1892        // For root nodes, don't propagate margins further
1893        escaped_top_margin = None;
1894        escaped_bottom_margin = None;
1895    }
1896
1897    // CSS 2.2 § 9.5: Floats don't contribute to container height with overflow:visible
1898    //
1899    // However, browsers DO expand containers to contain floats in specific cases:
1900    //
1901    // 1. If there's NO in-flow content (main_pen == 0), floats determine height
1902    // 2. If container establishes a BFC (overflow != visible)
1903    //
1904    // In this case, we have in-flow content (main_pen > 0) and overflow:visible,
1905    // so floats should NOT expand the container. Their margins can "bleed" beyond
1906    // the container boundaries into the parent.
1907    //
1908    // This matches Chrome/Firefox behavior where float margins escape through
1909    // the container's padding when there's existing in-flow content.
1910
1911    // Content-box Height Calculation
1912    //
1913    // CSS 2.2 § 8.3.1: "The top border edge of the box is defined to coincide with
1914    // the top border edge of the [first] child" when margins collapse/escape.
1915    //
1916    // This means escaped margins do NOT contribute to the parent's content-box height.
1917    //
1918    // Calculation:
1919    //
1920    //   main_pen = total vertical space used by all children and margins
1921    //
1922    //   Components of main_pen:
1923    //
1924    //   1. Children's border-boxes (always included)
1925    //   2. Sibling collapsed margins (space BETWEEN children - part of content)
1926    //   3. First child's position (0 if margin escaped, margin_top if blocked)
1927    //
1928    //   What to subtract:
1929    //
1930    //   - total_escaped_top_margin: First child's margin that went to grandparent's space This
1931    //     margin is OUTSIDE our content-box, so we must subtract it.
1932    //
1933    //   What NOT to subtract:
1934    //
1935    //   - total_sibling_margins: These are the gaps BETWEEN children, which are
1936    //    legitimately part of our content area's layout space.
1937    //
1938    // Example with escaped margin:
1939    //   <div class="parent" padding=0>              <!-- Node 2 -->
1940    //     <div class="child1" margin=30></div>      <!-- Node 3, margin escapes -->
1941    //     <div class="child2" margin=40></div>      <!-- Node 5 -->
1942    //   </div>
1943    //
1944    //   Layout process:
1945    //
1946    //   - Node 3 positioned at main_pen=0 (margin escaped)
1947    //   - Node 3 size=140px → main_pen advances to 140
1948    //   - Sibling collapse: max(30 child1 bottom, 40 child2 top) = 40px
1949    //   - main_pen advances to 180
1950    //   - Node 5 size=130px → main_pen advances to 310
1951    //   - total_escaped_top_margin = 30
1952    //   - total_sibling_margins = 40 (tracked but NOT subtracted)
1953    //   - content_box_height = 310 - 30 = 280px ✓
1954    //
1955    // Previously, we calculated:
1956    //
1957    //   content_box_height = main_pen - total_escaped_top_margin - total_sibling_margins
1958    //
1959    // This incorrectly subtracted sibling margins, making parent too small.
1960    // Sibling margins are *between* boxes (part of layout), not *outside* boxes
1961    // (like escaped margins).
1962
1963    let content_box_height = main_pen - total_escaped_top_margin;
1964    output.overflow_size =
1965        LogicalSize::from_main_cross(content_box_height, max_cross_size, writing_mode);
1966
1967    debug_info!(
1968        ctx,
1969        "[layout_bfc] FINAL for node {}: main_pen={}, total_escaped_top={}, \
1970         total_sibling_margins={}, content_box_height={}",
1971        node_index,
1972        main_pen,
1973        total_escaped_top_margin,
1974        total_sibling_margins,
1975        content_box_height
1976    );
1977
1978    // Baseline calculation would happen here in a full implementation.
1979    output.baseline = None;
1980
1981    // Store escaped margins in the LayoutNode for use by parent
1982    if let Some(node_mut) = tree.get_mut(node_index) {
1983        node_mut.escaped_top_margin = escaped_top_margin;
1984        node_mut.escaped_bottom_margin = escaped_bottom_margin;
1985    }
1986
1987    if let Some(node_mut) = tree.get_mut(node_index) {
1988        node_mut.baseline = output.baseline;
1989    }
1990
1991    Ok(BfcLayoutResult {
1992        output,
1993        escaped_top_margin,
1994        escaped_bottom_margin,
1995    })
1996}
1997
1998// Inline Formatting Context (CSS 2.2 § 9.4.2)
1999
2000/// Lays out an Inline Formatting Context (IFC) by delegating to the `text3` engine.
2001///
2002/// This function acts as a bridge between the box-tree world of `solver3` and the
2003/// rich text layout world of `text3`. Its responsibilities are:
2004///
2005/// 1. **Collect Content**: Traverse the direct children of the IFC root and convert them into a
2006///    `Vec<InlineContent>`, the input format for `text3`. This involves:
2007///
2008///     - Recursively laying out `inline-block` children to determine their final size and baseline,
2009///       which are then passed to `text3` as opaque objects.
2010///     - Extracting raw text runs from inline text nodes.
2011///
2012/// 2. **Translate Constraints**: Convert the `LayoutConstraints` (available space, floats) from
2013///    `solver3` into the more detailed `UnifiedConstraints` that `text3` requires.
2014///
2015/// 3. **Invoke Text Layout**: Call the `text3` cache's `layout_flow` method to perform the complex
2016///    tasks of BIDI analysis, shaping, line breaking, justification, and vertical alignment.
2017///
2018/// 4. **Integrate Results**: Process the `UnifiedLayout` returned by `text3`:
2019///
2020///     - Store the rich layout result on the IFC root `LayoutNode` for the display list generation
2021///       pass.
2022///     - Update the `positions` map for all `inline-block` children based on the positions
2023///       calculated by `text3`.
2024///     - Extract the final overflow size and baseline for the IFC root itself
2025fn layout_ifc<T: ParsedFontTrait>(
2026    ctx: &mut LayoutContext<'_, T>,
2027    text_cache: &mut crate::font_traits::TextLayoutCache,
2028    tree: &mut LayoutTree,
2029    node_index: usize,
2030    constraints: &LayoutConstraints,
2031) -> Result<LayoutOutput> {
2032    let ifc_start = (ctx.get_system_time_fn.cb)();
2033
2034    let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;;
2035    let float_count = constraints
2036        .bfc_state
2037        .as_ref()
2038        .map(|s| s.floats.floats.len())
2039        .unwrap_or(0);
2040    debug_info!(
2041        ctx,
2042        "[layout_ifc] ENTRY: node_index={}, has_bfc_state={}, float_count={}",
2043        node_index,
2044        constraints.bfc_state.is_some(),
2045        float_count
2046    );
2047    debug_ifc_layout!(ctx, "CALLED for node_index={}", node_index);
2048
2049    // For anonymous boxes, we need to find the DOM ID from a parent or child
2050    // CSS 2.2 § 9.2.1.1: Anonymous boxes inherit properties from their enclosing box
2051    let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
2052    let ifc_root_dom_id = match node.dom_node_id {
2053        Some(id) => id,
2054        None => {
2055            // Anonymous box - get DOM ID from parent or first child with DOM ID
2056            let parent_dom_id = node
2057                .parent
2058                .and_then(|p| tree.get(p))
2059                .and_then(|n| n.dom_node_id);
2060
2061            if let Some(id) = parent_dom_id {
2062                id
2063            } else {
2064                // Try to find DOM ID from first child
2065                node.children
2066                    .iter()
2067                    .filter_map(|&child_idx| tree.get(child_idx))
2068                    .filter_map(|n| n.dom_node_id)
2069                    .next()
2070                    .ok_or(LayoutError::InvalidTree)?
2071            }
2072        }
2073    };
2074
2075    debug_ifc_layout!(ctx, "ifc_root_dom_id={:?}", ifc_root_dom_id);
2076
2077    // Phase 1: Collect and measure all inline-level children.
2078    let phase1_start = (ctx.get_system_time_fn.cb)();
2079    let (inline_content, child_map) =
2080        collect_and_measure_inline_content(ctx, text_cache, tree, node_index, constraints)?;
2081    let _phase1_time = (ctx.get_system_time_fn.cb)().duration_since(&phase1_start);
2082
2083    debug_info!(
2084        ctx,
2085        "[layout_ifc] Collected {} inline content items for node {}",
2086        inline_content.len(),
2087        node_index
2088    );
2089    if inline_content.len() > 10 {
2090        let _text_count = inline_content.iter().filter(|i| matches!(i, InlineContent::Text(_))).count();
2091        let _shape_count = inline_content.iter().filter(|i| matches!(i, InlineContent::Shape(_))).count();
2092    }
2093    for (i, item) in inline_content.iter().enumerate() {
2094        match item {
2095            InlineContent::Text(run) => debug_info!(ctx, "  [{}] Text: '{}'", i, run.text),
2096            InlineContent::Marker {
2097                run,
2098                position_outside,
2099            } => debug_info!(
2100                ctx,
2101                "  [{}] Marker: '{}' (outside={})",
2102                i,
2103                run.text,
2104                position_outside
2105            ),
2106            InlineContent::Shape(_) => debug_info!(ctx, "  [{}] Shape", i),
2107            InlineContent::Image(_) => debug_info!(ctx, "  [{}] Image", i),
2108            _ => debug_info!(ctx, "  [{}] Other", i),
2109        }
2110    }
2111
2112    debug_ifc_layout!(
2113        ctx,
2114        "Collected {} inline content items",
2115        inline_content.len()
2116    );
2117
2118    if inline_content.is_empty() {
2119        debug_warning!(ctx, "inline_content is empty, returning default output!");
2120        return Ok(LayoutOutput::default());
2121    }
2122
2123    // === Phase 2c stub: IFC incremental relayout decision tree ===
2124    //
2125    // When a cached IFC layout exists and only specific items are dirty,
2126    // we can potentially skip full text_cache.layout_flow() and just:
2127    //   - Reshape only the dirty items (IfcOnly scope)
2128    //   - Shift x_offsets for subsequent items on the same line (nowrap fast path)
2129    //   - Or partial line-break reflow from the affected line onward
2130    //
2131    // For now, this is a no-op: we always fall through to full relayout.
2132    // The item_metrics on CachedInlineLayout enable this optimization
2133    // once Phase 2d is implemented.
2134    let _cached_ifc = tree
2135        .get(node_index)
2136        .and_then(|n| n.inline_layout_result.as_ref());
2137    // TODO(Phase 2d): Check dirty children's RelayoutScope via item_metrics.
2138    //   If max scope is None → return cached layout directly (repaint only).
2139    //   If max scope is IfcOnly and all dirty items are on nowrap lines
2140    //     → reshape + shift, skip layout_flow().
2141    //   Otherwise → full layout_flow() below.
2142
2143    // Phase 2: Translate constraints and define a single layout fragment for text3.
2144    let text3_constraints =
2145        translate_to_text3_constraints(ctx, constraints, ctx.styled_dom, ifc_root_dom_id);
2146
2147    // Clone constraints for caching (before they're moved into fragments)
2148    let cached_constraints = text3_constraints.clone();
2149
2150    debug_info!(
2151        ctx,
2152        "[layout_ifc] CALLING text_cache.layout_flow for node {} with {} exclusions",
2153        node_index,
2154        text3_constraints.shape_exclusions.len()
2155    );
2156
2157    let fragments = vec![LayoutFragment {
2158        id: "main".to_string(),
2159        constraints: text3_constraints,
2160    }];
2161
2162    // Phase 3: Invoke the text layout engine.
2163    // Get pre-loaded fonts from font manager (fonts should be loaded before layout)
2164    let phase3_start = (ctx.get_system_time_fn.cb)();
2165    let loaded_fonts = ctx.font_manager.get_loaded_fonts();
2166    let text_layout_result = match text_cache.layout_flow(
2167        &inline_content,
2168        &[],
2169        &fragments,
2170        &ctx.font_manager.font_chain_cache,
2171        &ctx.font_manager.fc_cache,
2172        &loaded_fonts,
2173        ctx.debug_messages,
2174    ) {
2175        Ok(result) => result,
2176        Err(e) => {
2177            // Font errors should not stop layout of other elements.
2178            // Log the error and return a zero-sized layout.
2179            debug_warning!(ctx, "Text layout failed: {:?}", e);
2180            debug_warning!(
2181                ctx,
2182                "Continuing with zero-sized layout for node {}",
2183                node_index
2184            );
2185
2186            let mut output = LayoutOutput::default();
2187            output.overflow_size = LogicalSize::new(0.0, 0.0);
2188            return Ok(output);
2189        }
2190    };
2191    let _phase3_time = (ctx.get_system_time_fn.cb)().duration_since(&phase3_start);
2192    let _total_ifc_time = (ctx.get_system_time_fn.cb)().duration_since(&ifc_start);
2193
2194    // Phase 4: Integrate results back into the solver3 layout tree.
2195    let mut output = LayoutOutput::default();
2196    let node = tree.get_mut(node_index).ok_or(LayoutError::InvalidTree)?;
2197
2198    debug_ifc_layout!(
2199        ctx,
2200        "text_layout_result has {} fragment_layouts",
2201        text_layout_result.fragment_layouts.len()
2202    );
2203
2204    if let Some(main_frag) = text_layout_result.fragment_layouts.get("main") {
2205        let frag_bounds = main_frag.bounds();
2206        debug_ifc_layout!(
2207            ctx,
2208            "Found 'main' fragment with {} items, bounds={}x{}",
2209            main_frag.items.len(),
2210            frag_bounds.width,
2211            frag_bounds.height
2212        );
2213        debug_ifc_layout!(ctx, "Storing inline_layout_result on node {}", node_index);
2214
2215        // Determine if we should store this layout result using the new
2216        // CachedInlineLayout system. The key insight is that inline layouts
2217        // depend on available width:
2218        //
2219        // - Min-content measurement uses width ≈ 0 (maximum line wrapping)
2220        // - Max-content measurement uses width = ∞ (no line wrapping)
2221        // - Final layout uses the actual column/container width
2222        //
2223        // We must track which constraint type was used, otherwise a min-content
2224        // measurement would incorrectly be reused for final rendering.
2225        let has_floats = constraints
2226            .bfc_state
2227            .as_ref()
2228            .map(|s| !s.floats.floats.is_empty())
2229            .unwrap_or(false);
2230        let current_width_type = constraints.available_width_type;
2231
2232        let should_store = match &node.inline_layout_result {
2233            None => {
2234                // No cached result - always store
2235                debug_info!(
2236                    ctx,
2237                    "[layout_ifc] Storing NEW inline_layout_result for node {} (width_type={:?}, \
2238                     has_floats={})",
2239                    node_index,
2240                    current_width_type,
2241                    has_floats
2242                );
2243                true
2244            }
2245            Some(cached) => {
2246                // Check if the new result should replace the cached one
2247                if cached.should_replace_with(current_width_type, has_floats) {
2248                    debug_info!(
2249                        ctx,
2250                        "[layout_ifc] REPLACING inline_layout_result for node {} (old: \
2251                         width={:?}, floats={}) with (new: width={:?}, floats={})",
2252                        node_index,
2253                        cached.available_width,
2254                        cached.has_floats,
2255                        current_width_type,
2256                        has_floats
2257                    );
2258                    true
2259                } else {
2260                    debug_info!(
2261                        ctx,
2262                        "[layout_ifc] KEEPING cached inline_layout_result for node {} (cached: \
2263                         width={:?}, floats={}, new: width={:?}, floats={})",
2264                        node_index,
2265                        cached.available_width,
2266                        cached.has_floats,
2267                        current_width_type,
2268                        has_floats
2269                    );
2270                    false
2271                }
2272            }
2273        };
2274
2275        if should_store {
2276            node.inline_layout_result = Some(CachedInlineLayout::new_with_constraints(
2277                main_frag.clone(),
2278                current_width_type,
2279                has_floats,
2280                cached_constraints,
2281            ));
2282        }
2283
2284        // Extract the overall size and baseline for the IFC root.
2285        output.overflow_size = LogicalSize::new(frag_bounds.width, frag_bounds.height);
2286        output.baseline = main_frag.last_baseline();
2287        node.baseline = output.baseline;
2288
2289        // Position all the inline-block children based on text3's calculations.
2290        // [CoordinateSpace::Parent] - positions are relative to IFC's content-box (0,0)
2291        for positioned_item in &main_frag.items {
2292            if let ShapedItem::Object { source, content, .. } = &positioned_item.item {
2293                if let Some(&child_node_index) = child_map.get(source) {
2294                    // new_relative_pos is [CoordinateSpace::Parent] - relative to this IFC's content-box
2295                    let new_relative_pos = LogicalPosition {
2296                        x: positioned_item.position.x,
2297                        y: positioned_item.position.y,
2298                    };
2299                    output.positions.insert(child_node_index, new_relative_pos);
2300                }
2301            }
2302        }
2303    }
2304
2305    Ok(output)
2306}
2307
2308fn translate_taffy_size(size: LogicalSize) -> TaffySize<Option<f32>> {
2309    TaffySize {
2310        width: Some(size.width),
2311        height: Some(size.height),
2312    }
2313}
2314
2315/// Helper: Convert StyleFontStyle to text3::cache::FontStyle
2316pub(crate) fn convert_font_style(style: StyleFontStyle) -> crate::font_traits::FontStyle {
2317    match style {
2318        StyleFontStyle::Normal => crate::font_traits::FontStyle::Normal,
2319        StyleFontStyle::Italic => crate::font_traits::FontStyle::Italic,
2320        StyleFontStyle::Oblique => crate::font_traits::FontStyle::Oblique,
2321    }
2322}
2323
2324/// Helper: Convert StyleFontWeight to FcWeight
2325pub(crate) fn convert_font_weight(weight: StyleFontWeight) -> FcWeight {
2326    match weight {
2327        StyleFontWeight::W100 => FcWeight::Thin,
2328        StyleFontWeight::W200 => FcWeight::ExtraLight,
2329        StyleFontWeight::W300 | StyleFontWeight::Lighter => FcWeight::Light,
2330        StyleFontWeight::Normal => FcWeight::Normal,
2331        StyleFontWeight::W500 => FcWeight::Medium,
2332        StyleFontWeight::W600 => FcWeight::SemiBold,
2333        StyleFontWeight::Bold => FcWeight::Bold,
2334        StyleFontWeight::W800 => FcWeight::ExtraBold,
2335        StyleFontWeight::W900 | StyleFontWeight::Bolder => FcWeight::Black,
2336    }
2337}
2338
2339/// Resolves a CSS size metric to pixels.
2340///
2341/// - `metric`: The CSS unit (px, pt, em, vw, etc.)
2342/// - `value`: The numeric value
2343/// - `containing_block_size`: Size of containing block (for percentage)
2344/// - `viewport_size`: Viewport dimensions (for vw, vh, vmin, vmax)
2345#[inline]
2346fn resolve_size_metric(
2347    metric: SizeMetric,
2348    value: f32,
2349    containing_block_size: f32,
2350    viewport_size: LogicalSize,
2351) -> f32 {
2352    match metric {
2353        SizeMetric::Px => value,
2354        SizeMetric::Pt => value * PT_TO_PX,
2355        SizeMetric::Percent => value / 100.0 * containing_block_size,
2356        SizeMetric::Em | SizeMetric::Rem => value * DEFAULT_FONT_SIZE,
2357        SizeMetric::Vw => value / 100.0 * viewport_size.width,
2358        SizeMetric::Vh => value / 100.0 * viewport_size.height,
2359        SizeMetric::Vmin => value / 100.0 * viewport_size.width.min(viewport_size.height),
2360        SizeMetric::Vmax => value / 100.0 * viewport_size.width.max(viewport_size.height),
2361        // In, Cm, Mm: convert to pixels using standard DPI (96)
2362        SizeMetric::In => value * 96.0,
2363        SizeMetric::Cm => value * 96.0 / 2.54,
2364        SizeMetric::Mm => value * 96.0 / 25.4,
2365    }
2366}
2367
2368pub fn translate_taffy_size_back(size: TaffySize<f32>) -> LogicalSize {
2369    LogicalSize {
2370        width: size.width,
2371        height: size.height,
2372    }
2373}
2374
2375pub fn translate_taffy_point_back(point: taffy::Point<f32>) -> LogicalPosition {
2376    LogicalPosition {
2377        x: point.x,
2378        y: point.y,
2379    }
2380}
2381
2382/// Checks if a node establishes a new Block Formatting Context (BFC).
2383///
2384/// Per CSS 2.2 § 9.4.1, a BFC is established by:
2385/// - Floats (elements with float other than 'none')
2386/// - Absolutely positioned elements (position: absolute or fixed)
2387/// - Block containers that are not block boxes (e.g., inline-blocks, table-cells)
2388/// - Block boxes with 'overflow' other than 'visible' and 'clip'
2389/// - Elements with 'display: flow-root'
2390/// - Table cells, table captions, and inline-blocks
2391///
2392/// Normal flow block-level boxes do NOT establish a new BFC.
2393///
2394/// This is critical for correct float interaction: normal blocks should overlap floats
2395/// (not shrink around them), while their inline content wraps around floats.
2396fn establishes_new_bfc<T: ParsedFontTrait>(ctx: &LayoutContext<'_, T>, node: &LayoutNode) -> bool {
2397    let Some(dom_id) = node.dom_node_id else {
2398        return false;
2399    };
2400
2401    let node_state = &ctx.styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2402
2403    // 1. Floats establish BFC
2404    let float_val = get_float(ctx.styled_dom, dom_id, node_state);
2405    if matches!(
2406        float_val,
2407        MultiValue::Exact(LayoutFloat::Left | LayoutFloat::Right)
2408    ) {
2409        return true;
2410    }
2411
2412    // 2. Absolutely positioned elements establish BFC
2413    let position = crate::solver3::positioning::get_position_type(ctx.styled_dom, Some(dom_id));
2414    if matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed) {
2415        return true;
2416    }
2417
2418    // 3. Inline-blocks, table-cells, table-captions establish BFC
2419    let display = get_display_property(ctx.styled_dom, Some(dom_id));
2420    if matches!(
2421        display,
2422        MultiValue::Exact(
2423            LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::TableCaption
2424        )
2425    ) {
2426        return true;
2427    }
2428
2429    // 4. display: flow-root establishes BFC
2430    if matches!(display, MultiValue::Exact(LayoutDisplay::FlowRoot)) {
2431        return true;
2432    }
2433
2434    // 5. Block boxes with overflow other than 'visible' or 'clip' establish BFC
2435    // Note: 'clip' does NOT establish BFC per CSS Overflow Module Level 3
2436    let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, node_state);
2437    let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, node_state);
2438
2439    let creates_bfc_via_overflow = |ov: &MultiValue<LayoutOverflow>| {
2440        matches!(
2441            ov,
2442            &MultiValue::Exact(
2443                LayoutOverflow::Hidden | LayoutOverflow::Scroll | LayoutOverflow::Auto
2444            )
2445        )
2446    };
2447
2448    if creates_bfc_via_overflow(&overflow_x) || creates_bfc_via_overflow(&overflow_y) {
2449        return true;
2450    }
2451
2452    // 6. Table, Flex, and Grid containers establish BFC (via FormattingContext)
2453    if matches!(
2454        node.formatting_context,
2455        FormattingContext::Table | FormattingContext::Flex | FormattingContext::Grid
2456    ) {
2457        return true;
2458    }
2459
2460    // Normal flow block boxes do NOT establish BFC
2461    false
2462}
2463
2464/// Translates solver3 layout constraints into the text3 engine's unified constraints.
2465fn translate_to_text3_constraints<'a, T: ParsedFontTrait>(
2466    ctx: &mut LayoutContext<'_, T>,
2467    constraints: &'a LayoutConstraints<'a>,
2468    styled_dom: &StyledDom,
2469    dom_id: NodeId,
2470) -> UnifiedConstraints {
2471    // Convert floats into exclusion zones for text3 to flow around.
2472    let mut shape_exclusions = if let Some(ref bfc_state) = constraints.bfc_state {
2473        debug_info!(
2474            ctx,
2475            "[translate_to_text3] dom_id={:?}, converting {} floats to exclusions",
2476            dom_id,
2477            bfc_state.floats.floats.len()
2478        );
2479        bfc_state
2480            .floats
2481            .floats
2482            .iter()
2483            .enumerate()
2484            .map(|(i, float_box)| {
2485                let rect = crate::text3::cache::Rect {
2486                    x: float_box.rect.origin.x,
2487                    y: float_box.rect.origin.y,
2488                    width: float_box.rect.size.width,
2489                    height: float_box.rect.size.height,
2490                };
2491                debug_info!(
2492                    ctx,
2493                    "[translate_to_text3]   Exclusion #{}: {:?} at ({}, {}) size {}x{}",
2494                    i,
2495                    float_box.kind,
2496                    rect.x,
2497                    rect.y,
2498                    rect.width,
2499                    rect.height
2500                );
2501                ShapeBoundary::Rectangle(rect)
2502            })
2503            .collect()
2504    } else {
2505        debug_info!(
2506            ctx,
2507            "[translate_to_text3] dom_id={:?}, NO bfc_state - no float exclusions",
2508            dom_id
2509        );
2510        Vec::new()
2511    };
2512
2513    debug_info!(
2514        ctx,
2515        "[translate_to_text3] dom_id={:?}, available_size={}x{}, shape_exclusions.len()={}",
2516        dom_id,
2517        constraints.available_size.width,
2518        constraints.available_size.height,
2519        shape_exclusions.len()
2520    );
2521
2522    // Map text-align and justify-content from CSS to text3 enums.
2523    let id = dom_id;
2524    let node_data = &styled_dom.node_data.as_container()[id];
2525    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2526
2527    // Read CSS Shapes properties
2528    // For reference box, use the element's CSS height if available, otherwise available_size
2529    // This is important because available_size.height might be infinite during auto height
2530    // calculation
2531    let ref_box_height = if constraints.available_size.height.is_finite() {
2532        constraints.available_size.height
2533    } else {
2534        // Try to get explicit CSS height
2535        // NOTE: If height is infinite, we can't properly resolve % heights
2536        // This is a limitation - shape-inside with % heights requires finite containing block
2537        styled_dom
2538            .css_property_cache
2539            .ptr
2540            .get_height(node_data, &id, node_state)
2541            .and_then(|v| v.get_property())
2542            .and_then(|h| match h {
2543                LayoutHeight::Px(v) => {
2544                    // Only accept absolute units (px, pt, in, cm, mm) - no %, em, rem
2545                    // since we can't resolve relative units without proper context
2546                    match v.metric {
2547                        SizeMetric::Px => Some(v.number.get()),
2548                        SizeMetric::Pt => Some(v.number.get() * PT_TO_PX),
2549                        SizeMetric::In => Some(v.number.get() * 96.0),
2550                        SizeMetric::Cm => Some(v.number.get() * 96.0 / 2.54),
2551                        SizeMetric::Mm => Some(v.number.get() * 96.0 / 25.4),
2552                        _ => None, // Ignore %, em, rem
2553                    }
2554                }
2555                _ => None,
2556            })
2557            .unwrap_or(constraints.available_size.width) // Fallback: use width as height (square)
2558    };
2559
2560    let reference_box = crate::text3::cache::Rect {
2561        x: 0.0,
2562        y: 0.0,
2563        width: constraints.available_size.width,
2564        height: ref_box_height,
2565    };
2566
2567    // shape-inside: Text flows within the shape boundary
2568    debug_info!(ctx, "Checking shape-inside for node {:?}", id);
2569    debug_info!(
2570        ctx,
2571        "Reference box: {:?} (available_size height was: {})",
2572        reference_box,
2573        constraints.available_size.height
2574    );
2575
2576    let shape_boundaries = styled_dom
2577        .css_property_cache
2578        .ptr
2579        .get_shape_inside(node_data, &id, node_state)
2580        .and_then(|v| {
2581            debug_info!(ctx, "Got shape-inside value: {:?}", v);
2582            v.get_property()
2583        })
2584        .and_then(|shape_inside| {
2585            debug_info!(ctx, "shape-inside property: {:?}", shape_inside);
2586            if let ShapeInside::Shape(css_shape) = shape_inside {
2587                debug_info!(
2588                    ctx,
2589                    "Converting CSS shape to ShapeBoundary: {:?}",
2590                    css_shape
2591                );
2592                let boundary =
2593                    ShapeBoundary::from_css_shape(css_shape, reference_box, ctx.debug_messages);
2594                debug_info!(ctx, "Created ShapeBoundary: {:?}", boundary);
2595                Some(vec![boundary])
2596            } else {
2597                debug_info!(ctx, "shape-inside is None");
2598                None
2599            }
2600        })
2601        .unwrap_or_default();
2602
2603    debug_info!(
2604        ctx,
2605        "Final shape_boundaries count: {}",
2606        shape_boundaries.len()
2607    );
2608
2609    // shape-outside: Text wraps around the shape (adds to exclusions)
2610    debug_info!(ctx, "Checking shape-outside for node {:?}", id);
2611    if let Some(shape_outside_value) = styled_dom
2612        .css_property_cache
2613        .ptr
2614        .get_shape_outside(node_data, &id, node_state)
2615    {
2616        debug_info!(ctx, "Got shape-outside value: {:?}", shape_outside_value);
2617        if let Some(shape_outside) = shape_outside_value.get_property() {
2618            debug_info!(ctx, "shape-outside property: {:?}", shape_outside);
2619            if let ShapeOutside::Shape(css_shape) = shape_outside {
2620                debug_info!(
2621                    ctx,
2622                    "Converting CSS shape-outside to ShapeBoundary: {:?}",
2623                    css_shape
2624                );
2625                let boundary =
2626                    ShapeBoundary::from_css_shape(css_shape, reference_box, ctx.debug_messages);
2627                debug_info!(ctx, "Created ShapeBoundary (exclusion): {:?}", boundary);
2628                shape_exclusions.push(boundary);
2629            }
2630        }
2631    } else {
2632        debug_info!(ctx, "No shape-outside value found");
2633    }
2634
2635    // TODO: clip-path will be used for rendering clipping (not text layout)
2636
2637    let writing_mode = get_writing_mode(styled_dom, id, node_state).unwrap_or_default();
2638
2639    let text_align = get_text_align(styled_dom, id, node_state).unwrap_or_default();
2640
2641    let text_justify = styled_dom
2642        .css_property_cache
2643        .ptr
2644        .get_text_justify(node_data, &id, node_state)
2645        .and_then(|s| s.get_property().copied())
2646        .unwrap_or_default();
2647
2648    // Get font-size for resolving line-height
2649    // Use helper function which checks dependency chain first
2650    let font_size = get_element_font_size(styled_dom, id, node_state);
2651
2652    let line_height_value = styled_dom
2653        .css_property_cache
2654        .ptr
2655        .get_line_height(node_data, &id, node_state)
2656        .and_then(|s| s.get_property().cloned())
2657        .unwrap_or_default();
2658
2659    let hyphenation = styled_dom
2660        .css_property_cache
2661        .ptr
2662        .get_hyphens(node_data, &id, node_state)
2663        .and_then(|s| s.get_property().copied())
2664        .unwrap_or_default();
2665
2666    let overflow_behaviour = get_overflow_x(styled_dom, id, node_state).unwrap_or_default();
2667
2668    // Get vertical-align from CSS property cache (defaults to Baseline per CSS spec)
2669    let vertical_align = match get_vertical_align_property(styled_dom, id, node_state) {
2670        MultiValue::Exact(v) => v,
2671        _ => StyleVerticalAlign::default(),
2672    };
2673
2674    let vertical_align = match vertical_align {
2675        StyleVerticalAlign::Baseline => text3::cache::VerticalAlign::Baseline,
2676        StyleVerticalAlign::Top => text3::cache::VerticalAlign::Top,
2677        StyleVerticalAlign::Middle => text3::cache::VerticalAlign::Middle,
2678        StyleVerticalAlign::Bottom => text3::cache::VerticalAlign::Bottom,
2679        StyleVerticalAlign::Sub => text3::cache::VerticalAlign::Sub,
2680        StyleVerticalAlign::Superscript => text3::cache::VerticalAlign::Super,
2681        StyleVerticalAlign::TextTop => text3::cache::VerticalAlign::TextTop,
2682        StyleVerticalAlign::TextBottom => text3::cache::VerticalAlign::TextBottom,
2683    };
2684    let text_orientation = text3::cache::TextOrientation::default();
2685
2686    // Get the direction property from the CSS cache (defaults to LTR if not set)
2687    let direction = match get_direction_property(styled_dom, id, node_state) {
2688        MultiValue::Exact(d) => Some(match d {
2689            StyleDirection::Ltr => text3::cache::BidiDirection::Ltr,
2690            StyleDirection::Rtl => text3::cache::BidiDirection::Rtl,
2691        }),
2692        _ => None,
2693    };
2694
2695    debug_info!(
2696        ctx,
2697        "dom_id={:?}, available_size={}x{}, setting available_width={}",
2698        dom_id,
2699        constraints.available_size.width,
2700        constraints.available_size.height,
2701        constraints.available_size.width
2702    );
2703
2704    // Get text-indent
2705    let text_indent = styled_dom
2706        .css_property_cache
2707        .ptr
2708        .get_text_indent(node_data, &id, node_state)
2709        .and_then(|s| s.get_property())
2710        .map(|ti| {
2711            let context = ResolutionContext {
2712                element_font_size: get_element_font_size(styled_dom, id, node_state),
2713                parent_font_size: get_parent_font_size(styled_dom, id, node_state),
2714                root_font_size: get_root_font_size(styled_dom, node_state),
2715                containing_block_size: PhysicalSize::new(constraints.available_size.width, 0.0),
2716                element_size: None,
2717                viewport_size: PhysicalSize::new(0.0, 0.0),
2718            };
2719            ti.inner
2720                .resolve_with_context(&context, PropertyContext::Other)
2721        })
2722        .unwrap_or(0.0);
2723
2724    // Get column-count for multi-column layout (default: 1 = no columns)
2725    let columns = styled_dom
2726        .css_property_cache
2727        .ptr
2728        .get_column_count(node_data, &id, node_state)
2729        .and_then(|s| s.get_property())
2730        .map(|cc| match cc {
2731            ColumnCount::Integer(n) => *n,
2732            ColumnCount::Auto => 1,
2733        })
2734        .unwrap_or(1);
2735
2736    // Get column-gap for multi-column layout (default: normal = 1em)
2737    let column_gap = styled_dom
2738        .css_property_cache
2739        .ptr
2740        .get_column_gap(node_data, &id, node_state)
2741        .and_then(|s| s.get_property())
2742        .map(|cg| {
2743            let context = ResolutionContext {
2744                element_font_size: get_element_font_size(styled_dom, id, node_state),
2745                parent_font_size: get_parent_font_size(styled_dom, id, node_state),
2746                root_font_size: get_root_font_size(styled_dom, node_state),
2747                containing_block_size: PhysicalSize::new(0.0, 0.0),
2748                element_size: None,
2749                viewport_size: PhysicalSize::new(0.0, 0.0),
2750            };
2751            cg.inner
2752                .resolve_with_context(&context, PropertyContext::Other)
2753        })
2754        .unwrap_or_else(|| {
2755            // Default: 1em
2756            get_element_font_size(styled_dom, id, node_state)
2757        });
2758
2759    // Map white-space CSS property to TextWrap
2760    let text_wrap = match get_white_space_property(styled_dom, id, node_state) {
2761        MultiValue::Exact(ws) => match ws {
2762            StyleWhiteSpace::Normal => text3::cache::TextWrap::Wrap,
2763            StyleWhiteSpace::Nowrap => text3::cache::TextWrap::NoWrap,
2764            StyleWhiteSpace::Pre => text3::cache::TextWrap::NoWrap,
2765            StyleWhiteSpace::PreWrap => text3::cache::TextWrap::Wrap,
2766            StyleWhiteSpace::PreLine => text3::cache::TextWrap::Wrap,
2767            StyleWhiteSpace::BreakSpaces => text3::cache::TextWrap::Wrap,
2768        },
2769        _ => text3::cache::TextWrap::Wrap,
2770    };
2771
2772    // Get initial-letter for drop caps
2773    let initial_letter = styled_dom
2774        .css_property_cache
2775        .ptr
2776        .get_initial_letter(node_data, &id, node_state)
2777        .and_then(|s| s.get_property())
2778        .map(|il| {
2779            use std::num::NonZeroUsize;
2780            let sink = match il.sink {
2781                azul_css::corety::OptionU32::Some(s) => s,
2782                azul_css::corety::OptionU32::None => il.size,
2783            };
2784            text3::cache::InitialLetter {
2785                size: il.size as f32,
2786                sink,
2787                count: NonZeroUsize::new(1).unwrap(),
2788            }
2789        });
2790
2791    // Get line-clamp for limiting visible lines
2792    let line_clamp = styled_dom
2793        .css_property_cache
2794        .ptr
2795        .get_line_clamp(node_data, &id, node_state)
2796        .and_then(|s| s.get_property())
2797        .and_then(|lc| std::num::NonZeroUsize::new(lc.max_lines));
2798
2799    // Get hanging-punctuation for hanging punctuation marks
2800    let hanging_punctuation = styled_dom
2801        .css_property_cache
2802        .ptr
2803        .get_hanging_punctuation(node_data, &id, node_state)
2804        .and_then(|s| s.get_property())
2805        .map(|hp| hp.enabled)
2806        .unwrap_or(false);
2807
2808    // Get text-combine-upright for vertical text combination
2809    let text_combine_upright = styled_dom
2810        .css_property_cache
2811        .ptr
2812        .get_text_combine_upright(node_data, &id, node_state)
2813        .and_then(|s| s.get_property())
2814        .map(|tcu| match tcu {
2815            StyleTextCombineUpright::None => text3::cache::TextCombineUpright::None,
2816            StyleTextCombineUpright::All => text3::cache::TextCombineUpright::All,
2817            StyleTextCombineUpright::Digits(n) => text3::cache::TextCombineUpright::Digits(*n),
2818        });
2819
2820    // Get exclusion-margin for shape exclusions
2821    let exclusion_margin = styled_dom
2822        .css_property_cache
2823        .ptr
2824        .get_exclusion_margin(node_data, &id, node_state)
2825        .and_then(|s| s.get_property())
2826        .map(|em| em.inner.get() as f32)
2827        .unwrap_or(0.0);
2828
2829    // Get hyphenation-language for language-specific hyphenation
2830    let hyphenation_language = styled_dom
2831        .css_property_cache
2832        .ptr
2833        .get_hyphenation_language(node_data, &id, node_state)
2834        .and_then(|s| s.get_property())
2835        .and_then(|hl| {
2836            #[cfg(feature = "text_layout_hyphenation")]
2837            {
2838                use hyphenation::{Language, Load};
2839                // Parse BCP 47 language code to hyphenation::Language
2840                match hl.inner.as_str() {
2841                    "en-US" | "en" => Some(Language::EnglishUS),
2842                    "de-DE" | "de" => Some(Language::German1996),
2843                    "fr-FR" | "fr" => Some(Language::French),
2844                    "es-ES" | "es" => Some(Language::Spanish),
2845                    "it-IT" | "it" => Some(Language::Italian),
2846                    "pt-PT" | "pt" => Some(Language::Portuguese),
2847                    "nl-NL" | "nl" => Some(Language::Dutch),
2848                    "pl-PL" | "pl" => Some(Language::Polish),
2849                    "ru-RU" | "ru" => Some(Language::Russian),
2850                    "zh-CN" | "zh" => Some(Language::Chinese),
2851                    _ => None, // Unsupported language
2852                }
2853            }
2854            #[cfg(not(feature = "text_layout_hyphenation"))]
2855            {
2856                None::<crate::text3::script::Language>
2857            }
2858        });
2859
2860    UnifiedConstraints {
2861        exclusion_margin,
2862        hyphenation_language,
2863        text_indent,
2864        initial_letter,
2865        line_clamp,
2866        columns,
2867        column_gap,
2868        hanging_punctuation,
2869        text_wrap,
2870        text_combine_upright,
2871        segment_alignment: SegmentAlignment::Total,
2872        overflow: match overflow_behaviour {
2873            LayoutOverflow::Visible => text3::cache::OverflowBehavior::Visible,
2874            LayoutOverflow::Hidden | LayoutOverflow::Clip => text3::cache::OverflowBehavior::Hidden,
2875            LayoutOverflow::Scroll => text3::cache::OverflowBehavior::Scroll,
2876            LayoutOverflow::Auto => text3::cache::OverflowBehavior::Auto,
2877        },
2878        // Use the semantic available_width_type directly instead of converting from float.
2879        // This preserves MinContent/MaxContent semantics for intrinsic sizing.
2880        available_width: constraints.available_width_type,
2881        // For scrollable containers (overflow: scroll/auto), don't constrain height
2882        // so that the full content is laid out and content_size is calculated correctly.
2883        available_height: match overflow_behaviour {
2884            LayoutOverflow::Scroll | LayoutOverflow::Auto => None,
2885            _ => Some(constraints.available_size.height),
2886        },
2887        shape_boundaries, // CSS shape-inside: text flows within shape
2888        shape_exclusions, // CSS shape-outside + floats: text wraps around shapes
2889        writing_mode: Some(match writing_mode {
2890            LayoutWritingMode::HorizontalTb => text3::cache::WritingMode::HorizontalTb,
2891            LayoutWritingMode::VerticalRl => text3::cache::WritingMode::VerticalRl,
2892            LayoutWritingMode::VerticalLr => text3::cache::WritingMode::VerticalLr,
2893        }),
2894        direction, // Use the CSS direction property (currently defaulting to LTR)
2895        hyphenation: match hyphenation {
2896            StyleHyphens::None => false,
2897            StyleHyphens::Auto => true,
2898        },
2899        text_orientation,
2900        text_align: match text_align {
2901            StyleTextAlign::Start => text3::cache::TextAlign::Start,
2902            StyleTextAlign::End => text3::cache::TextAlign::End,
2903            StyleTextAlign::Left => text3::cache::TextAlign::Left,
2904            StyleTextAlign::Right => text3::cache::TextAlign::Right,
2905            StyleTextAlign::Center => text3::cache::TextAlign::Center,
2906            StyleTextAlign::Justify => text3::cache::TextAlign::Justify,
2907        },
2908        text_justify: match text_justify {
2909            LayoutTextJustify::None => text3::cache::JustifyContent::None,
2910            LayoutTextJustify::Auto => text3::cache::JustifyContent::None,
2911            LayoutTextJustify::InterWord => text3::cache::JustifyContent::InterWord,
2912            LayoutTextJustify::InterCharacter => text3::cache::JustifyContent::InterCharacter,
2913            LayoutTextJustify::Distribute => text3::cache::JustifyContent::Distribute,
2914        },
2915        line_height: line_height_value.inner.normalized() * font_size, /* Resolve line-height relative to font-size */
2916        vertical_align, // CSS vertical-align property (defaults to Baseline)
2917    }
2918}
2919
2920// Table Formatting Context (CSS 2.2 § 17)
2921
2922/// Lays out a Table Formatting Context.
2923/// Table column information for layout calculations
2924#[derive(Debug, Clone)]
2925pub struct TableColumnInfo {
2926    /// Minimum width required for this column
2927    pub min_width: f32,
2928    /// Maximum width desired for this column
2929    pub max_width: f32,
2930    /// Computed final width for this column
2931    pub computed_width: Option<f32>,
2932}
2933
2934/// Information about a table cell for layout
2935#[derive(Debug, Clone)]
2936pub struct TableCellInfo {
2937    /// Node index in the layout tree
2938    pub node_index: usize,
2939    /// Column index (0-based)
2940    pub column: usize,
2941    /// Number of columns this cell spans
2942    pub colspan: usize,
2943    /// Row index (0-based)
2944    pub row: usize,
2945    /// Number of rows this cell spans
2946    pub rowspan: usize,
2947}
2948
2949/// Table layout context - holds all information needed for table layout
2950#[derive(Debug)]
2951struct TableLayoutContext {
2952    /// Information about each column
2953    columns: Vec<TableColumnInfo>,
2954    /// Information about each cell
2955    cells: Vec<TableCellInfo>,
2956    /// Number of rows in the table
2957    num_rows: usize,
2958    /// Whether to use fixed or auto layout algorithm
2959    use_fixed_layout: bool,
2960    /// Computed height for each row
2961    row_heights: Vec<f32>,
2962    /// Border collapse mode
2963    border_collapse: StyleBorderCollapse,
2964    /// Border spacing (only used when border_collapse is Separate)
2965    border_spacing: LayoutBorderSpacing,
2966    /// CSS 2.2 Section 17.4: Index of table-caption child, if any
2967    caption_index: Option<usize>,
2968    /// CSS 2.2 Section 17.6: Rows with visibility:collapse (dynamic effects)
2969    /// Set of row indices that have visibility:collapse
2970    collapsed_rows: std::collections::HashSet<usize>,
2971    /// CSS 2.2 Section 17.6: Columns with visibility:collapse (dynamic effects)
2972    /// Set of column indices that have visibility:collapse
2973    collapsed_columns: std::collections::HashSet<usize>,
2974}
2975
2976impl TableLayoutContext {
2977    fn new() -> Self {
2978        Self {
2979            columns: Vec::new(),
2980            cells: Vec::new(),
2981            num_rows: 0,
2982            use_fixed_layout: false,
2983            row_heights: Vec::new(),
2984            border_collapse: StyleBorderCollapse::Separate,
2985            border_spacing: LayoutBorderSpacing::default(),
2986            caption_index: None,
2987            collapsed_rows: std::collections::HashSet::new(),
2988            collapsed_columns: std::collections::HashSet::new(),
2989        }
2990    }
2991}
2992
2993/// Source of a border in the border conflict resolution algorithm
2994#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
2995pub enum BorderSource {
2996    Table = 0,
2997    ColumnGroup = 1,
2998    Column = 2,
2999    RowGroup = 3,
3000    Row = 4,
3001    Cell = 5,
3002}
3003
3004/// Information about a border for conflict resolution
3005#[derive(Debug, Clone)]
3006pub struct BorderInfo {
3007    pub width: f32,
3008    pub style: BorderStyle,
3009    pub color: ColorU,
3010    pub source: BorderSource,
3011}
3012
3013impl BorderInfo {
3014    pub fn new(width: f32, style: BorderStyle, color: ColorU, source: BorderSource) -> Self {
3015        Self {
3016            width,
3017            style,
3018            color,
3019            source,
3020        }
3021    }
3022
3023    /// Get the priority of a border style for conflict resolution
3024    /// Higher number = higher priority
3025    pub fn style_priority(style: &BorderStyle) -> u8 {
3026        match style {
3027            BorderStyle::Hidden => 255, // Highest - suppresses all borders
3028            BorderStyle::None => 0,     // Lowest - loses to everything
3029            BorderStyle::Double => 8,
3030            BorderStyle::Solid => 7,
3031            BorderStyle::Dashed => 6,
3032            BorderStyle::Dotted => 5,
3033            BorderStyle::Ridge => 4,
3034            BorderStyle::Outset => 3,
3035            BorderStyle::Groove => 2,
3036            BorderStyle::Inset => 1,
3037        }
3038    }
3039
3040    /// Compare two borders for conflict resolution per CSS 2.2 Section 17.6.2.1
3041    /// Returns the winning border
3042    pub fn resolve_conflict(a: &BorderInfo, b: &BorderInfo) -> Option<BorderInfo> {
3043        // 1. 'hidden' wins and suppresses all borders
3044        if a.style == BorderStyle::Hidden || b.style == BorderStyle::Hidden {
3045            return None;
3046        }
3047
3048        // 2. Filter out 'none' - if both are none, no border
3049        let a_is_none = a.style == BorderStyle::None;
3050        let b_is_none = b.style == BorderStyle::None;
3051
3052        if a_is_none && b_is_none {
3053            return None;
3054        }
3055        if a_is_none {
3056            return Some(b.clone());
3057        }
3058        if b_is_none {
3059            return Some(a.clone());
3060        }
3061
3062        // 3. Wider border wins
3063        if a.width > b.width {
3064            return Some(a.clone());
3065        }
3066        if b.width > a.width {
3067            return Some(b.clone());
3068        }
3069
3070        // 4. If same width, compare style priority
3071        let a_priority = Self::style_priority(&a.style);
3072        let b_priority = Self::style_priority(&b.style);
3073
3074        if a_priority > b_priority {
3075            return Some(a.clone());
3076        }
3077        if b_priority > a_priority {
3078            return Some(b.clone());
3079        }
3080
3081        // 5. If same style, source priority:
3082        // Cell > Row > RowGroup > Column > ColumnGroup > Table
3083        if a.source > b.source {
3084            return Some(a.clone());
3085        }
3086        if b.source > a.source {
3087            return Some(b.clone());
3088        }
3089
3090        // 6. Same priority - prefer first one (left/top in LTR)
3091        Some(a.clone())
3092    }
3093}
3094
3095/// Get border information for a node
3096fn get_border_info<T: ParsedFontTrait>(
3097    ctx: &LayoutContext<'_, T>,
3098    node: &LayoutNode,
3099    source: BorderSource,
3100) -> (BorderInfo, BorderInfo, BorderInfo, BorderInfo) {
3101    use azul_css::props::{
3102        basic::{
3103            pixel::{PhysicalSize, PropertyContext, ResolutionContext},
3104            ColorU,
3105        },
3106        style::BorderStyle,
3107    };
3108    use get_element_font_size;
3109    use get_parent_font_size;
3110    use get_root_font_size;
3111
3112    let default_border = BorderInfo::new(
3113        0.0,
3114        BorderStyle::None,
3115        ColorU {
3116            r: 0,
3117            g: 0,
3118            b: 0,
3119            a: 0,
3120        },
3121        source,
3122    );
3123
3124    let Some(dom_id) = node.dom_node_id else {
3125        return (
3126            default_border.clone(),
3127            default_border.clone(),
3128            default_border.clone(),
3129            default_border.clone(),
3130        );
3131    };
3132
3133    let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3134    let node_state = StyledNodeState::default();
3135    let cache = &ctx.styled_dom.css_property_cache.ptr;
3136
3137    // FAST PATH: compact cache for normal state
3138    if let Some(ref cc) = cache.compact_cache {
3139        let idx = dom_id.index();
3140
3141        // Border styles from packed u16
3142        let bts = cc.get_border_top_style(idx);
3143        let brs = cc.get_border_right_style(idx);
3144        let bbs = cc.get_border_bottom_style(idx);
3145        let bls = cc.get_border_left_style(idx);
3146
3147        // Border colors from u32 RGBA
3148        let make_color = |raw: u32| -> ColorU {
3149            if raw == 0 {
3150                ColorU { r: 0, g: 0, b: 0, a: 0 }
3151            } else {
3152                ColorU {
3153                    r: ((raw >> 24) & 0xFF) as u8,
3154                    g: ((raw >> 16) & 0xFF) as u8,
3155                    b: ((raw >> 8) & 0xFF) as u8,
3156                    a: (raw & 0xFF) as u8,
3157                }
3158            }
3159        };
3160
3161        let btc = make_color(cc.get_border_top_color_raw(idx));
3162        let brc = make_color(cc.get_border_right_color_raw(idx));
3163        let bbc = make_color(cc.get_border_bottom_color_raw(idx));
3164        let blc = make_color(cc.get_border_left_color_raw(idx));
3165
3166        // Border widths from i16 × 10
3167        let decode_width = |raw: i16| -> f32 {
3168            if raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
3169                0.0 // sentinel → fall back to 0
3170            } else {
3171                raw as f32 / 10.0
3172            }
3173        };
3174
3175        let btw = decode_width(cc.get_border_top_width_raw(idx));
3176        let brw = decode_width(cc.get_border_right_width_raw(idx));
3177        let bbw = decode_width(cc.get_border_bottom_width_raw(idx));
3178        let blw = decode_width(cc.get_border_left_width_raw(idx));
3179
3180        let top = if bts == BorderStyle::None { default_border.clone() }
3181            else { BorderInfo::new(btw, bts, btc, source) };
3182        let right = if brs == BorderStyle::None { default_border.clone() }
3183            else { BorderInfo::new(brw, brs, brc, source) };
3184        let bottom = if bbs == BorderStyle::None { default_border.clone() }
3185            else { BorderInfo::new(bbw, bbs, bbc, source) };
3186        let left = if bls == BorderStyle::None { default_border.clone() }
3187            else { BorderInfo::new(blw, bls, blc, source) };
3188
3189        return (top, right, bottom, left);
3190    }
3191
3192    // SLOW PATH: full cascade resolution
3193    let cache = &ctx.styled_dom.css_property_cache.ptr;
3194
3195    // Create resolution context for border-width (em/rem support, no % support)
3196    let element_font_size = get_element_font_size(ctx.styled_dom, dom_id, &node_state);
3197    let parent_font_size = get_parent_font_size(ctx.styled_dom, dom_id, &node_state);
3198    let root_font_size = get_root_font_size(ctx.styled_dom, &node_state);
3199
3200    let resolution_context = ResolutionContext {
3201        element_font_size,
3202        parent_font_size,
3203        root_font_size,
3204        // Not used for border-width
3205        containing_block_size: PhysicalSize::new(0.0, 0.0),
3206        // Not used for border-width
3207        element_size: None,
3208        viewport_size: PhysicalSize::new(0.0, 0.0),
3209    };
3210
3211    // Top border
3212    let top = cache
3213        .get_border_top_style(node_data, &dom_id, &node_state)
3214        .and_then(|s| s.get_property())
3215        .map(|style_val| {
3216            let width = cache
3217                .get_border_top_width(node_data, &dom_id, &node_state)
3218                .and_then(|w| w.get_property())
3219                .map(|w| {
3220                    w.inner
3221                        .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
3222                })
3223                .unwrap_or(0.0);
3224            let color = cache
3225                .get_border_top_color(node_data, &dom_id, &node_state)
3226                .and_then(|c| c.get_property())
3227                .map(|c| c.inner)
3228                .unwrap_or(ColorU {
3229                    r: 0,
3230                    g: 0,
3231                    b: 0,
3232                    a: 255,
3233                });
3234            BorderInfo::new(width, style_val.inner, color, source)
3235        })
3236        .unwrap_or_else(|| default_border.clone());
3237
3238    // Right border
3239    let right = cache
3240        .get_border_right_style(node_data, &dom_id, &node_state)
3241        .and_then(|s| s.get_property())
3242        .map(|style_val| {
3243            let width = cache
3244                .get_border_right_width(node_data, &dom_id, &node_state)
3245                .and_then(|w| w.get_property())
3246                .map(|w| {
3247                    w.inner
3248                        .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
3249                })
3250                .unwrap_or(0.0);
3251            let color = cache
3252                .get_border_right_color(node_data, &dom_id, &node_state)
3253                .and_then(|c| c.get_property())
3254                .map(|c| c.inner)
3255                .unwrap_or(ColorU {
3256                    r: 0,
3257                    g: 0,
3258                    b: 0,
3259                    a: 255,
3260                });
3261            BorderInfo::new(width, style_val.inner, color, source)
3262        })
3263        .unwrap_or_else(|| default_border.clone());
3264
3265    // Bottom border
3266    let bottom = cache
3267        .get_border_bottom_style(node_data, &dom_id, &node_state)
3268        .and_then(|s| s.get_property())
3269        .map(|style_val| {
3270            let width = cache
3271                .get_border_bottom_width(node_data, &dom_id, &node_state)
3272                .and_then(|w| w.get_property())
3273                .map(|w| {
3274                    w.inner
3275                        .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
3276                })
3277                .unwrap_or(0.0);
3278            let color = cache
3279                .get_border_bottom_color(node_data, &dom_id, &node_state)
3280                .and_then(|c| c.get_property())
3281                .map(|c| c.inner)
3282                .unwrap_or(ColorU {
3283                    r: 0,
3284                    g: 0,
3285                    b: 0,
3286                    a: 255,
3287                });
3288            BorderInfo::new(width, style_val.inner, color, source)
3289        })
3290        .unwrap_or_else(|| default_border.clone());
3291
3292    // Left border
3293    let left = cache
3294        .get_border_left_style(node_data, &dom_id, &node_state)
3295        .and_then(|s| s.get_property())
3296        .map(|style_val| {
3297            let width = cache
3298                .get_border_left_width(node_data, &dom_id, &node_state)
3299                .and_then(|w| w.get_property())
3300                .map(|w| {
3301                    w.inner
3302                        .resolve_with_context(&resolution_context, PropertyContext::BorderWidth)
3303                })
3304                .unwrap_or(0.0);
3305            let color = cache
3306                .get_border_left_color(node_data, &dom_id, &node_state)
3307                .and_then(|c| c.get_property())
3308                .map(|c| c.inner)
3309                .unwrap_or(ColorU {
3310                    r: 0,
3311                    g: 0,
3312                    b: 0,
3313                    a: 255,
3314                });
3315            BorderInfo::new(width, style_val.inner, color, source)
3316        })
3317        .unwrap_or_else(|| default_border.clone());
3318
3319    (top, right, bottom, left)
3320}
3321
3322/// Get the table-layout property for a table node
3323fn get_table_layout_property<T: ParsedFontTrait>(
3324    ctx: &LayoutContext<'_, T>,
3325    node: &LayoutNode,
3326) -> LayoutTableLayout {
3327    let Some(dom_id) = node.dom_node_id else {
3328        return LayoutTableLayout::Auto;
3329    };
3330
3331    let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3332    let node_state = StyledNodeState::default();
3333
3334    ctx.styled_dom
3335        .css_property_cache
3336        .ptr
3337        .get_table_layout(node_data, &dom_id, &node_state)
3338        .and_then(|prop| prop.get_property().copied())
3339        .unwrap_or(LayoutTableLayout::Auto)
3340}
3341
3342/// Get the border-collapse property for a table node
3343fn get_border_collapse_property<T: ParsedFontTrait>(
3344    ctx: &LayoutContext<'_, T>,
3345    node: &LayoutNode,
3346) -> StyleBorderCollapse {
3347    let Some(dom_id) = node.dom_node_id else {
3348        return StyleBorderCollapse::Separate;
3349    };
3350
3351    // FAST PATH: compact cache
3352    if let Some(ref cc) = ctx.styled_dom.css_property_cache.ptr.compact_cache {
3353        return cc.get_border_collapse(dom_id.index());
3354    }
3355
3356    let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3357    let node_state = StyledNodeState::default();
3358
3359    ctx.styled_dom
3360        .css_property_cache
3361        .ptr
3362        .get_border_collapse(node_data, &dom_id, &node_state)
3363        .and_then(|prop| prop.get_property().copied())
3364        .unwrap_or(StyleBorderCollapse::Separate)
3365}
3366
3367/// Get the border-spacing property for a table node
3368fn get_border_spacing_property<T: ParsedFontTrait>(
3369    ctx: &LayoutContext<'_, T>,
3370    node: &LayoutNode,
3371) -> LayoutBorderSpacing {
3372    if let Some(dom_id) = node.dom_node_id {
3373        // FAST PATH: compact cache
3374        if let Some(ref cc) = ctx.styled_dom.css_property_cache.ptr.compact_cache {
3375            let idx = dom_id.index();
3376            let h_raw = cc.get_border_spacing_h_raw(idx);
3377            let v_raw = cc.get_border_spacing_v_raw(idx);
3378            // If both are non-sentinel, use compact values
3379            if h_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
3380                && v_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
3381            {
3382                return LayoutBorderSpacing::new_separate(
3383                    azul_css::props::basic::pixel::PixelValue::px(h_raw as f32 / 10.0),
3384                    azul_css::props::basic::pixel::PixelValue::px(v_raw as f32 / 10.0),
3385                );
3386            }
3387            // sentinel → fall through to slow path
3388        }
3389
3390        let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3391        let node_state = StyledNodeState::default();
3392
3393        if let Some(prop) = ctx.styled_dom.css_property_cache.ptr.get_border_spacing(
3394            node_data,
3395            &dom_id,
3396            &node_state,
3397        ) {
3398            if let Some(value) = prop.get_property() {
3399                return *value;
3400            }
3401        }
3402    }
3403
3404    LayoutBorderSpacing::default() // Default: 0
3405}
3406
3407/// CSS 2.2 Section 17.4 - Tables in the visual formatting model:
3408///
3409/// "The caption box is a block box that retains its own content, padding,
3410/// border, and margin areas. The caption-side property specifies the position
3411/// of the caption box with respect to the table box."
3412///
3413/// Get the caption-side property for a table node.
3414/// Returns Top (default) or Bottom.
3415fn get_caption_side_property<T: ParsedFontTrait>(
3416    ctx: &LayoutContext<'_, T>,
3417    node: &LayoutNode,
3418) -> StyleCaptionSide {
3419    if let Some(dom_id) = node.dom_node_id {
3420        let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
3421        let node_state = StyledNodeState::default();
3422
3423        if let Some(prop) =
3424            ctx.styled_dom
3425                .css_property_cache
3426                .ptr
3427                .get_caption_side(node_data, &dom_id, &node_state)
3428        {
3429            if let Some(value) = prop.get_property() {
3430                return *value;
3431            }
3432        }
3433    }
3434
3435    StyleCaptionSide::Top // Default per CSS 2.2
3436}
3437
3438/// CSS 2.2 Section 17.6 - Dynamic row and column effects:
3439///
3440/// "The 'visibility' value 'collapse' removes a row or column from display,
3441/// but it has a different effect than 'visibility: hidden' on other elements.
3442/// When a row or column is collapsed, the space normally occupied by the row
3443/// or column is removed."
3444///
3445/// Check if a node has visibility:collapse set.
3446///
3447/// This is used for table rows and columns to optimize dynamic hiding.
3448fn is_visibility_collapsed<T: ParsedFontTrait>(
3449    ctx: &LayoutContext<'_, T>,
3450    node: &LayoutNode,
3451) -> bool {
3452    if let Some(dom_id) = node.dom_node_id {
3453        let node_state = StyledNodeState::default();
3454
3455        if let MultiValue::Exact(value) = get_visibility(ctx.styled_dom, dom_id, &node_state) {
3456            return matches!(value, StyleVisibility::Collapse);
3457        }
3458    }
3459
3460    false
3461}
3462
3463/// CSS 2.2 Section 17.6.1.1 - Borders and Backgrounds around empty cells
3464///
3465/// In the separated borders model, the 'empty-cells' property controls the rendering of
3466/// borders and backgrounds around cells that have no visible content. Empty means it has no
3467/// children, or has children that are only collapsed whitespace."
3468///
3469/// Check if a table cell is empty (has no visible content).
3470///
3471/// This is used by the rendering pipeline to decide whether to paint borders/backgrounds
3472/// when empty-cells: hide is set in separated border model.
3473///
3474/// A cell is considered empty if:
3475///
3476/// - It has no children, OR
3477/// - It has children but no inline_layout_result (no rendered content)
3478///
3479/// Note: Full whitespace detection would require checking text content during rendering.
3480/// This function provides a basic check suitable for layout phase.
3481fn is_cell_empty(tree: &LayoutTree, cell_index: usize) -> bool {
3482    let cell_node = match tree.get(cell_index) {
3483        Some(node) => node,
3484        None => return true, // Invalid cell is considered empty
3485    };
3486
3487    // No children = empty
3488    if cell_node.children.is_empty() {
3489        return true;
3490    }
3491
3492    // If cell has an inline layout result, check if it's empty
3493    if let Some(ref cached_layout) = cell_node.inline_layout_result {
3494        // Check if inline layout has any rendered content
3495        // Empty inline layouts have no items (glyphs/fragments)
3496        // Note: This is a heuristic - full detection requires text content analysis
3497        return cached_layout.layout.items.is_empty();
3498    }
3499
3500    // Check if all children have no content
3501    // A more thorough check would recursively examine all descendants
3502    //
3503    // For now, we use a simple heuristic: if there are children, assume not empty
3504    // unless proven otherwise by inline_layout_result
3505
3506    // Cell with children but no inline layout = likely has block-level content = not empty
3507    false
3508}
3509
3510/// Main function to layout a table formatting context
3511pub fn layout_table_fc<T: ParsedFontTrait>(
3512    ctx: &mut LayoutContext<'_, T>,
3513    tree: &mut LayoutTree,
3514    text_cache: &mut crate::font_traits::TextLayoutCache,
3515    node_index: usize,
3516    constraints: &LayoutConstraints,
3517) -> Result<LayoutOutput> {
3518    debug_log!(ctx, "Laying out table");
3519
3520    debug_table_layout!(
3521        ctx,
3522        "node_index={}, available_size={:?}, writing_mode={:?}",
3523        node_index,
3524        constraints.available_size,
3525        constraints.writing_mode
3526    );
3527
3528    // Multi-pass table layout algorithm:
3529    //
3530    // 1. Analyze table structure - identify rows, cells, columns
3531    // 2. Determine table-layout property (fixed vs auto)
3532    // 3. Calculate column widths
3533    // 4. Layout cells and calculate row heights
3534    // 5. Position cells in final grid
3535
3536    // Get the table node to read CSS properties
3537    let table_node = tree
3538        .get(node_index)
3539        .ok_or(LayoutError::InvalidTree)?
3540        .clone();
3541
3542    // Calculate the table's border-box width for column distribution
3543    // This accounts for the table's own width property (e.g., width: 100%)
3544    let table_border_box_width = if let Some(dom_id) = table_node.dom_node_id {
3545        // Use calculate_used_size_for_node to resolve table width (respects width:100%)
3546        let intrinsic = table_node.intrinsic_sizes.clone().unwrap_or_default();
3547        let containing_block_size = LogicalSize {
3548            width: constraints.available_size.width,
3549            height: constraints.available_size.height,
3550        };
3551
3552        let table_size = crate::solver3::sizing::calculate_used_size_for_node(
3553            ctx.styled_dom,
3554            Some(dom_id),
3555            containing_block_size,
3556            intrinsic,
3557            &table_node.box_props,
3558            ctx.viewport_size,
3559        )?;
3560
3561        table_size.width
3562    } else {
3563        constraints.available_size.width
3564    };
3565
3566    // Subtract padding and border to get content-box width for column distribution
3567    let table_content_box_width = {
3568        let padding_width = table_node.box_props.padding.left + table_node.box_props.padding.right;
3569        let border_width = table_node.box_props.border.left + table_node.box_props.border.right;
3570        (table_border_box_width - padding_width - border_width).max(0.0)
3571    };
3572
3573    debug_table_layout!(ctx, "Table Layout Debug");
3574    debug_table_layout!(ctx, "Node index: {}", node_index);
3575    debug_table_layout!(
3576        ctx,
3577        "Available size from parent: {:.2} x {:.2}",
3578        constraints.available_size.width,
3579        constraints.available_size.height
3580    );
3581    debug_table_layout!(ctx, "Table border-box width: {:.2}", table_border_box_width);
3582    debug_table_layout!(
3583        ctx,
3584        "Table content-box width: {:.2}",
3585        table_content_box_width
3586    );
3587    debug_table_layout!(
3588        ctx,
3589        "Table padding: L={:.2} R={:.2}",
3590        table_node.box_props.padding.left,
3591        table_node.box_props.padding.right
3592    );
3593    debug_table_layout!(
3594        ctx,
3595        "Table border: L={:.2} R={:.2}",
3596        table_node.box_props.border.left,
3597        table_node.box_props.border.right
3598    );
3599    debug_table_layout!(ctx, "=");
3600
3601    // Phase 1: Analyze table structure
3602    let mut table_ctx = analyze_table_structure(tree, node_index, ctx)?;
3603
3604    // Phase 2: Read CSS properties and determine layout algorithm
3605    let table_layout = get_table_layout_property(ctx, &table_node);
3606    table_ctx.use_fixed_layout = matches!(table_layout, LayoutTableLayout::Fixed);
3607
3608    // Read border properties
3609    table_ctx.border_collapse = get_border_collapse_property(ctx, &table_node);
3610    table_ctx.border_spacing = get_border_spacing_property(ctx, &table_node);
3611
3612    debug_log!(
3613        ctx,
3614        "Table layout: {:?}, border-collapse: {:?}, border-spacing: {:?}",
3615        table_layout,
3616        table_ctx.border_collapse,
3617        table_ctx.border_spacing
3618    );
3619
3620    // Phase 3: Calculate column widths
3621    if table_ctx.use_fixed_layout {
3622        // DEBUG: Log available width passed into fixed column calculation
3623        debug_table_layout!(
3624            ctx,
3625            "FIXED layout: table_content_box_width={:.2}",
3626            table_content_box_width
3627        );
3628        calculate_column_widths_fixed(ctx, &mut table_ctx, table_content_box_width);
3629    } else {
3630        // Pass table_content_box_width for column distribution in auto layout
3631        calculate_column_widths_auto_with_width(
3632            &mut table_ctx,
3633            tree,
3634            text_cache,
3635            ctx,
3636            constraints,
3637            table_content_box_width,
3638        )?;
3639    }
3640
3641    debug_table_layout!(ctx, "After column width calculation:");
3642    debug_table_layout!(ctx, "  Number of columns: {}", table_ctx.columns.len());
3643    for (i, col) in table_ctx.columns.iter().enumerate() {
3644        debug_table_layout!(
3645            ctx,
3646            "  Column {}: width={:.2}",
3647            i,
3648            col.computed_width.unwrap_or(0.0)
3649        );
3650    }
3651    let total_col_width: f32 = table_ctx
3652        .columns
3653        .iter()
3654        .filter_map(|c| c.computed_width)
3655        .sum();
3656    debug_table_layout!(ctx, "  Total column width: {:.2}", total_col_width);
3657
3658    // Phase 4: Calculate row heights based on cell content
3659    calculate_row_heights(&mut table_ctx, tree, text_cache, ctx, constraints)?;
3660
3661    // Phase 5: Position cells in final grid and collect positions
3662    let mut cell_positions =
3663        position_table_cells(&mut table_ctx, tree, ctx, node_index, constraints)?;
3664
3665    // Calculate final table size including border-spacing
3666    let mut table_width: f32 = table_ctx
3667        .columns
3668        .iter()
3669        .filter_map(|col| col.computed_width)
3670        .sum();
3671    let mut table_height: f32 = table_ctx.row_heights.iter().sum();
3672
3673    debug_table_layout!(
3674        ctx,
3675        "After calculate_row_heights: table_height={:.2}, row_heights={:?}",
3676        table_height,
3677        table_ctx.row_heights
3678    );
3679
3680    // Add border-spacing to table size if border-collapse is separate
3681    if table_ctx.border_collapse == StyleBorderCollapse::Separate {
3682        use get_element_font_size;
3683        use get_parent_font_size;
3684        use get_root_font_size;
3685        use PhysicalSize;
3686        use PropertyContext;
3687        use ResolutionContext;
3688
3689        let styled_dom = ctx.styled_dom;
3690        let table_id = tree.nodes[node_index].dom_node_id.unwrap();
3691        let table_state = &styled_dom.styled_nodes.as_container()[table_id].styled_node_state;
3692
3693        let spacing_context = ResolutionContext {
3694            element_font_size: get_element_font_size(styled_dom, table_id, table_state),
3695            parent_font_size: get_parent_font_size(styled_dom, table_id, table_state),
3696            root_font_size: get_root_font_size(styled_dom, table_state),
3697            containing_block_size: PhysicalSize::new(0.0, 0.0),
3698            element_size: None,
3699            // TODO: Get actual DPI scale from ctx
3700            viewport_size: PhysicalSize::new(0.0, 0.0),
3701        };
3702
3703        let h_spacing = table_ctx
3704            .border_spacing
3705            .horizontal
3706            .resolve_with_context(&spacing_context, PropertyContext::Other);
3707        let v_spacing = table_ctx
3708            .border_spacing
3709            .vertical
3710            .resolve_with_context(&spacing_context, PropertyContext::Other);
3711
3712        // Add spacing: left + (n-1 between columns) + right = n+1 spacings
3713        let num_cols = table_ctx.columns.len();
3714        if num_cols > 0 {
3715            table_width += h_spacing * (num_cols + 1) as f32;
3716        }
3717
3718        // Add spacing: top + (n-1 between rows) + bottom = n+1 spacings
3719        if table_ctx.num_rows > 0 {
3720            table_height += v_spacing * (table_ctx.num_rows + 1) as f32;
3721        }
3722    }
3723
3724    // CSS 2.2 Section 17.4: Layout and position the caption if present
3725    //
3726    // "The caption box is a block box that retains its own content,
3727    // padding, border, and margin areas."
3728    let caption_side = get_caption_side_property(ctx, &table_node);
3729    let mut caption_height = 0.0;
3730    let mut table_y_offset = 0.0;
3731
3732    if let Some(caption_idx) = table_ctx.caption_index {
3733        debug_log!(
3734            ctx,
3735            "Laying out caption with caption-side: {:?}",
3736            caption_side
3737        );
3738
3739        // Layout caption as a block with the table's width as available width
3740        let caption_constraints = LayoutConstraints {
3741            available_size: LogicalSize {
3742                width: table_width,
3743                height: constraints.available_size.height,
3744            },
3745            writing_mode: constraints.writing_mode,
3746            bfc_state: None, // Caption creates its own BFC
3747            text_align: constraints.text_align,
3748            containing_block_size: constraints.containing_block_size,
3749            available_width_type: Text3AvailableSpace::Definite(table_width),
3750        };
3751
3752        // Layout the caption node
3753        let mut empty_float_cache = std::collections::BTreeMap::new();
3754        let caption_result = layout_formatting_context(
3755            ctx,
3756            tree,
3757            text_cache,
3758            caption_idx,
3759            &caption_constraints,
3760            &mut empty_float_cache,
3761        )?;
3762        caption_height = caption_result.output.overflow_size.height;
3763
3764        // Position caption based on caption-side property
3765        let caption_position = match caption_side {
3766            StyleCaptionSide::Top => {
3767                // Caption on top: position at y=0, table starts below caption
3768                table_y_offset = caption_height;
3769                LogicalPosition { x: 0.0, y: 0.0 }
3770            }
3771            StyleCaptionSide::Bottom => {
3772                // Caption on bottom: table starts at y=0, caption below table
3773                LogicalPosition {
3774                    x: 0.0,
3775                    y: table_height,
3776                }
3777            }
3778        };
3779
3780        // Add caption position to the positions map
3781        cell_positions.insert(caption_idx, caption_position);
3782
3783        debug_log!(
3784            ctx,
3785            "Caption positioned at x={:.2}, y={:.2}, height={:.2}",
3786            caption_position.x,
3787            caption_position.y,
3788            caption_height
3789        );
3790    }
3791
3792    // Adjust all table cell positions if caption is on top
3793    if table_y_offset > 0.0 {
3794        debug_log!(
3795            ctx,
3796            "Adjusting table cells by y offset: {:.2}",
3797            table_y_offset
3798        );
3799
3800        // Adjust cell positions in the map
3801        for cell_info in &table_ctx.cells {
3802            if let Some(pos) = cell_positions.get_mut(&cell_info.node_index) {
3803                pos.y += table_y_offset;
3804            }
3805        }
3806    }
3807
3808    // Total table height includes caption
3809    let total_height = table_height + caption_height;
3810
3811    debug_table_layout!(ctx, "Final table dimensions:");
3812    debug_table_layout!(ctx, "  Content width (columns): {:.2}", table_width);
3813    debug_table_layout!(ctx, "  Content height (rows): {:.2}", table_height);
3814    debug_table_layout!(ctx, "  Caption height: {:.2}", caption_height);
3815    debug_table_layout!(ctx, "  Total height: {:.2}", total_height);
3816    debug_table_layout!(ctx, "End Table Debug");
3817
3818    // Create output with the table's final size and cell positions
3819    let output = LayoutOutput {
3820        overflow_size: LogicalSize {
3821            width: table_width,
3822            height: total_height,
3823        },
3824        // Cell positions calculated in position_table_cells
3825        positions: cell_positions,
3826        // Tables don't have a baseline
3827        baseline: None,
3828    };
3829
3830    Ok(output)
3831}
3832
3833/// Analyze the table structure to identify rows, cells, and columns
3834fn analyze_table_structure<T: ParsedFontTrait>(
3835    tree: &LayoutTree,
3836    table_index: usize,
3837    ctx: &mut LayoutContext<'_, T>,
3838) -> Result<TableLayoutContext> {
3839    let mut table_ctx = TableLayoutContext::new();
3840
3841    let table_node = tree.get(table_index).ok_or(LayoutError::InvalidTree)?;
3842
3843    // CSS 2.2 Section 17.4: A table may have one table-caption child.
3844    // Traverse children to find caption, columns/colgroups, rows, and row groups
3845    for &child_idx in &table_node.children {
3846        if let Some(child) = tree.get(child_idx) {
3847            // Check if this is a table caption
3848            if matches!(child.formatting_context, FormattingContext::TableCaption) {
3849                debug_log!(ctx, "Found table caption at index {}", child_idx);
3850                table_ctx.caption_index = Some(child_idx);
3851                continue;
3852            }
3853
3854            // CSS 2.2 Section 17.2: Check for column groups
3855            if matches!(
3856                child.formatting_context,
3857                FormattingContext::TableColumnGroup
3858            ) {
3859                analyze_table_colgroup(tree, child_idx, &mut table_ctx, ctx)?;
3860                continue;
3861            }
3862
3863            // Check if this is a table row or row group
3864            match child.formatting_context {
3865                FormattingContext::TableRow => {
3866                    analyze_table_row(tree, child_idx, &mut table_ctx, ctx)?;
3867                }
3868                FormattingContext::TableRowGroup => {
3869                    // Process rows within the row group
3870                    for &row_idx in &child.children {
3871                        if let Some(row) = tree.get(row_idx) {
3872                            if matches!(row.formatting_context, FormattingContext::TableRow) {
3873                                analyze_table_row(tree, row_idx, &mut table_ctx, ctx)?;
3874                            }
3875                        }
3876                    }
3877                }
3878                _ => {}
3879            }
3880        }
3881    }
3882
3883    debug_log!(
3884        ctx,
3885        "Table structure: {} rows, {} columns, {} cells{}",
3886        table_ctx.num_rows,
3887        table_ctx.columns.len(),
3888        table_ctx.cells.len(),
3889        if table_ctx.caption_index.is_some() {
3890            ", has caption"
3891        } else {
3892            ""
3893        }
3894    );
3895
3896    Ok(table_ctx)
3897}
3898
3899/// Analyze a table column group to identify columns and track collapsed columns
3900///
3901/// - CSS 2.2 Section 17.2: Column groups contain columns
3902/// - CSS 2.2 Section 17.6: Columns can have visibility:collapse
3903fn analyze_table_colgroup<T: ParsedFontTrait>(
3904    tree: &LayoutTree,
3905    colgroup_index: usize,
3906    table_ctx: &mut TableLayoutContext,
3907    ctx: &mut LayoutContext<'_, T>,
3908) -> Result<()> {
3909    let colgroup_node = tree.get(colgroup_index).ok_or(LayoutError::InvalidTree)?;
3910
3911    // Check if the colgroup itself has visibility:collapse
3912    if is_visibility_collapsed(ctx, colgroup_node) {
3913        // All columns in this group should be collapsed
3914        // TODO: For now, just mark the group (actual column indices will be determined later)
3915        debug_log!(
3916            ctx,
3917            "Column group at index {} has visibility:collapse",
3918            colgroup_index
3919        );
3920    }
3921
3922    // Check for individual column elements within the group
3923    for &col_idx in &colgroup_node.children {
3924        if let Some(col_node) = tree.get(col_idx) {
3925            // Note: Individual columns don't have a FormattingContext::TableColumn
3926            // They are represented as children of TableColumnGroup
3927            // Check visibility:collapse on each column
3928            if is_visibility_collapsed(ctx, col_node) {
3929                // We need to determine the actual column index this represents
3930                // For now, we'll track it during cell analysis
3931                debug_log!(ctx, "Column at index {} has visibility:collapse", col_idx);
3932            }
3933        }
3934    }
3935
3936    Ok(())
3937}
3938
3939/// Analyze a table row to identify cells and update column count
3940fn analyze_table_row<T: ParsedFontTrait>(
3941    tree: &LayoutTree,
3942    row_index: usize,
3943    table_ctx: &mut TableLayoutContext,
3944    ctx: &mut LayoutContext<'_, T>,
3945) -> Result<()> {
3946    let row_node = tree.get(row_index).ok_or(LayoutError::InvalidTree)?;
3947    let row_num = table_ctx.num_rows;
3948    table_ctx.num_rows += 1;
3949
3950    // CSS 2.2 Section 17.6: Check if this row has visibility:collapse
3951    if is_visibility_collapsed(ctx, row_node) {
3952        debug_log!(ctx, "Row {} has visibility:collapse", row_num);
3953        table_ctx.collapsed_rows.insert(row_num);
3954    }
3955
3956    let mut col_index = 0;
3957
3958    for &cell_idx in &row_node.children {
3959        if let Some(cell) = tree.get(cell_idx) {
3960            if matches!(cell.formatting_context, FormattingContext::TableCell) {
3961                // Get colspan and rowspan (TODO: from CSS properties)
3962                let colspan = 1; // TODO: Get from CSS
3963                let rowspan = 1; // TODO: Get from CSS
3964
3965                let cell_info = TableCellInfo {
3966                    node_index: cell_idx,
3967                    column: col_index,
3968                    colspan,
3969                    row: row_num,
3970                    rowspan,
3971                };
3972
3973                table_ctx.cells.push(cell_info);
3974
3975                // Update column count
3976                let max_col = col_index + colspan;
3977                while table_ctx.columns.len() < max_col {
3978                    table_ctx.columns.push(TableColumnInfo {
3979                        min_width: 0.0,
3980                        max_width: 0.0,
3981                        computed_width: None,
3982                    });
3983                }
3984
3985                col_index += colspan;
3986            }
3987        }
3988    }
3989
3990    Ok(())
3991}
3992
3993/// Calculate column widths using the fixed table layout algorithm
3994///
3995/// CSS 2.2 Section 17.5.2.1: In fixed table layout, the table width is
3996/// not dependent on cell contents
3997///
3998/// CSS 2.2 Section 17.6: Columns with visibility:collapse are excluded
3999/// from width calculations
4000fn calculate_column_widths_fixed<T: ParsedFontTrait>(
4001    ctx: &mut LayoutContext<'_, T>,
4002    table_ctx: &mut TableLayoutContext,
4003    available_width: f32,
4004) {
4005    debug_table_layout!(
4006        ctx,
4007        "calculate_column_widths_fixed: num_cols={}, available_width={:.2}",
4008        table_ctx.columns.len(),
4009        available_width
4010    );
4011
4012    // Fixed layout: distribute width equally among non-collapsed columns
4013    // TODO: Respect column width properties and first-row cell widths
4014    let num_cols = table_ctx.columns.len();
4015    if num_cols == 0 {
4016        return;
4017    }
4018
4019    // Count non-collapsed columns
4020    let num_visible_cols = num_cols - table_ctx.collapsed_columns.len();
4021    if num_visible_cols == 0 {
4022        // All columns collapsed - set all to zero width
4023        for col in &mut table_ctx.columns {
4024            col.computed_width = Some(0.0);
4025        }
4026        return;
4027    }
4028
4029    // Distribute width only among visible columns
4030    let col_width = available_width / num_visible_cols as f32;
4031    for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
4032        if table_ctx.collapsed_columns.contains(&col_idx) {
4033            col.computed_width = Some(0.0);
4034        } else {
4035            col.computed_width = Some(col_width);
4036        }
4037    }
4038}
4039
4040/// Measure a cell's minimum content width (with maximum wrapping)
4041fn measure_cell_min_content_width<T: ParsedFontTrait>(
4042    ctx: &mut LayoutContext<'_, T>,
4043    tree: &mut LayoutTree,
4044    text_cache: &mut crate::font_traits::TextLayoutCache,
4045    cell_index: usize,
4046    constraints: &LayoutConstraints,
4047) -> Result<f32> {
4048    // CSS 2.2 Section 17.5.2.2: "Calculate the minimum content width (MCW) of each cell"
4049    //
4050    // Min-content width is the width with maximum wrapping.
4051    // Use AvailableSpace::MinContent to signal intrinsic min-content sizing to the
4052    // text layout engine.
4053    use crate::text3::cache::AvailableSpace;
4054    let min_constraints = LayoutConstraints {
4055        available_size: LogicalSize {
4056            width: AvailableSpace::MinContent.to_f32_for_layout(),
4057            height: f32::INFINITY,
4058        },
4059        writing_mode: constraints.writing_mode,
4060        bfc_state: None, // Don't propagate BFC state for measurement
4061        text_align: constraints.text_align,
4062        containing_block_size: constraints.containing_block_size,
4063        // CRITICAL: Mark this as min-content measurement, not definite width!
4064        // This ensures the cached layout won't be incorrectly reused for final rendering.
4065        available_width_type: Text3AvailableSpace::MinContent,
4066    };
4067
4068    let mut temp_positions: super::PositionVec = Vec::new();
4069    let mut temp_scrollbar_reflow = false;
4070    let mut temp_float_cache = std::collections::BTreeMap::new();
4071
4072    crate::solver3::cache::calculate_layout_for_subtree(
4073        ctx,
4074        tree,
4075        text_cache,
4076        cell_index,
4077        LogicalPosition::zero(),
4078        min_constraints.available_size,
4079        &mut temp_positions,
4080        &mut temp_scrollbar_reflow,
4081        &mut temp_float_cache,
4082        // ComputeSize: we only need the resulting size, not final positions
4083        crate::solver3::cache::ComputeMode::ComputeSize,
4084    )?;
4085
4086    let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4087    let size = cell_node.used_size.unwrap_or_default();
4088
4089    // Add padding and border to get the total minimum width
4090    let padding = &cell_node.box_props.padding;
4091    let border = &cell_node.box_props.border;
4092    let writing_mode = constraints.writing_mode;
4093
4094    let min_width = size.width
4095        + padding.cross_start(writing_mode)
4096        + padding.cross_end(writing_mode)
4097        + border.cross_start(writing_mode)
4098        + border.cross_end(writing_mode);
4099
4100    Ok(min_width)
4101}
4102
4103/// Measure a cell's maximum content width (without wrapping)
4104fn measure_cell_max_content_width<T: ParsedFontTrait>(
4105    ctx: &mut LayoutContext<'_, T>,
4106    tree: &mut LayoutTree,
4107    text_cache: &mut crate::font_traits::TextLayoutCache,
4108    cell_index: usize,
4109    constraints: &LayoutConstraints,
4110) -> Result<f32> {
4111    // CSS 2.2 Section 17.5.2.2: "Calculate the maximum content width (MCW) of each cell"
4112    //
4113    // Max-content width is the width without any wrapping.
4114    // Use AvailableSpace::MaxContent to signal intrinsic max-content sizing to
4115    // the text layout engine.
4116    use crate::text3::cache::AvailableSpace;
4117    let max_constraints = LayoutConstraints {
4118        available_size: LogicalSize {
4119            width: AvailableSpace::MaxContent.to_f32_for_layout(),
4120            height: f32::INFINITY,
4121        },
4122        writing_mode: constraints.writing_mode,
4123        bfc_state: None, // Don't propagate BFC state for measurement
4124        text_align: constraints.text_align,
4125        containing_block_size: constraints.containing_block_size,
4126        // CRITICAL: Mark this as max-content measurement, not definite width!
4127        // This ensures the cached layout won't be incorrectly reused for final rendering.
4128        available_width_type: Text3AvailableSpace::MaxContent,
4129    };
4130
4131    let mut temp_positions: super::PositionVec = Vec::new();
4132    let mut temp_scrollbar_reflow = false;
4133    let mut temp_float_cache = std::collections::BTreeMap::new();
4134
4135    crate::solver3::cache::calculate_layout_for_subtree(
4136        ctx,
4137        tree,
4138        text_cache,
4139        cell_index,
4140        LogicalPosition::zero(),
4141        max_constraints.available_size,
4142        &mut temp_positions,
4143        &mut temp_scrollbar_reflow,
4144        &mut temp_float_cache,
4145        // ComputeSize: we only need the resulting size, not final positions
4146        crate::solver3::cache::ComputeMode::ComputeSize,
4147    )?;
4148
4149    let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4150    let size = cell_node.used_size.unwrap_or_default();
4151
4152    // Add padding and border to get the total maximum width
4153    let padding = &cell_node.box_props.padding;
4154    let border = &cell_node.box_props.border;
4155    let writing_mode = constraints.writing_mode;
4156
4157    let max_width = size.width
4158        + padding.cross_start(writing_mode)
4159        + padding.cross_end(writing_mode)
4160        + border.cross_start(writing_mode)
4161        + border.cross_end(writing_mode);
4162
4163    Ok(max_width)
4164}
4165
4166/// Calculate column widths using the auto table layout algorithm
4167fn calculate_column_widths_auto<T: ParsedFontTrait>(
4168    table_ctx: &mut TableLayoutContext,
4169    tree: &mut LayoutTree,
4170    text_cache: &mut crate::font_traits::TextLayoutCache,
4171    ctx: &mut LayoutContext<'_, T>,
4172    constraints: &LayoutConstraints,
4173) -> Result<()> {
4174    calculate_column_widths_auto_with_width(
4175        table_ctx,
4176        tree,
4177        text_cache,
4178        ctx,
4179        constraints,
4180        constraints.available_size.width,
4181    )
4182}
4183
4184/// Calculate column widths using the auto table layout algorithm with explicit table width
4185fn calculate_column_widths_auto_with_width<T: ParsedFontTrait>(
4186    table_ctx: &mut TableLayoutContext,
4187    tree: &mut LayoutTree,
4188    text_cache: &mut crate::font_traits::TextLayoutCache,
4189    ctx: &mut LayoutContext<'_, T>,
4190    constraints: &LayoutConstraints,
4191    table_width: f32,
4192) -> Result<()> {
4193    // Auto layout: calculate min/max content width for each cell
4194    let num_cols = table_ctx.columns.len();
4195    if num_cols == 0 {
4196        return Ok(());
4197    }
4198
4199    // Step 1: Measure all cells to determine column min/max widths
4200    // CSS 2.2 Section 17.6: Skip cells in collapsed columns
4201    for cell_info in &table_ctx.cells {
4202        // Skip cells in collapsed columns
4203        if table_ctx.collapsed_columns.contains(&cell_info.column) {
4204            continue;
4205        }
4206
4207        // Skip cells that span into collapsed columns
4208        let mut spans_collapsed = false;
4209        for col_offset in 0..cell_info.colspan {
4210            if table_ctx
4211                .collapsed_columns
4212                .contains(&(cell_info.column + col_offset))
4213            {
4214                spans_collapsed = true;
4215                break;
4216            }
4217        }
4218        if spans_collapsed {
4219            continue;
4220        }
4221
4222        let min_width = measure_cell_min_content_width(
4223            ctx,
4224            tree,
4225            text_cache,
4226            cell_info.node_index,
4227            constraints,
4228        )?;
4229
4230        let max_width = measure_cell_max_content_width(
4231            ctx,
4232            tree,
4233            text_cache,
4234            cell_info.node_index,
4235            constraints,
4236        )?;
4237
4238        // Handle single-column cells
4239        if cell_info.colspan == 1 {
4240            let col = &mut table_ctx.columns[cell_info.column];
4241            col.min_width = col.min_width.max(min_width);
4242            col.max_width = col.max_width.max(max_width);
4243        } else {
4244            // Handle multi-column cells (colspan > 1)
4245            // Distribute the cell's min/max width across the spanned columns
4246            distribute_cell_width_across_columns(
4247                &mut table_ctx.columns,
4248                cell_info.column,
4249                cell_info.colspan,
4250                min_width,
4251                max_width,
4252                &table_ctx.collapsed_columns,
4253            );
4254        }
4255    }
4256
4257    // Step 2: Calculate final column widths based on available space
4258    // Exclude collapsed columns from total width calculations
4259    let total_min_width: f32 = table_ctx
4260        .columns
4261        .iter()
4262        .enumerate()
4263        .filter(|(idx, _)| !table_ctx.collapsed_columns.contains(idx))
4264        .map(|(_, c)| c.min_width)
4265        .sum();
4266    let total_max_width: f32 = table_ctx
4267        .columns
4268        .iter()
4269        .enumerate()
4270        .filter(|(idx, _)| !table_ctx.collapsed_columns.contains(idx))
4271        .map(|(_, c)| c.max_width)
4272        .sum();
4273    let available_width = table_width; // Use table's content-box width, not constraints
4274
4275    debug_table_layout!(
4276        ctx,
4277        "calculate_column_widths_auto: min={:.2}, max={:.2}, table_width={:.2}",
4278        total_min_width,
4279        total_max_width,
4280        table_width
4281    );
4282
4283    // Handle infinity and NaN cases
4284    if !total_max_width.is_finite() || !available_width.is_finite() {
4285        // If max_width is infinite or unavailable, distribute available width equally
4286        let num_non_collapsed = table_ctx.columns.len() - table_ctx.collapsed_columns.len();
4287        let width_per_column = if num_non_collapsed > 0 {
4288            available_width / num_non_collapsed as f32
4289        } else {
4290            0.0
4291        };
4292
4293        for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
4294            if table_ctx.collapsed_columns.contains(&col_idx) {
4295                col.computed_width = Some(0.0);
4296            } else {
4297                // Use the larger of min_width and equal distribution
4298                col.computed_width = Some(col.min_width.max(width_per_column));
4299            }
4300        }
4301    } else if available_width >= total_max_width {
4302        // Case 1: More space than max-content - distribute excess proportionally
4303        //
4304        // CSS 2.1 Section 17.5.2.2: Distribute extra space proportionally to
4305        // max-content widths
4306        let excess_width = available_width - total_max_width;
4307
4308        // First pass: collect column info (max_width) to avoid borrowing issues
4309        let column_info: Vec<(usize, f32, bool)> = table_ctx
4310            .columns
4311            .iter()
4312            .enumerate()
4313            .map(|(idx, c)| (idx, c.max_width, table_ctx.collapsed_columns.contains(&idx)))
4314            .collect();
4315
4316        // Calculate total weight for proportional distribution (use max_width as weight)
4317        let total_weight: f32 = column_info.iter()
4318            .filter(|(_, _, is_collapsed)| !is_collapsed)
4319            .map(|(_, max_w, _)| max_w.max(1.0)) // Avoid division by zero
4320            .sum();
4321
4322        let num_non_collapsed = column_info
4323            .iter()
4324            .filter(|(_, _, is_collapsed)| !is_collapsed)
4325            .count();
4326
4327        // Second pass: set computed widths
4328        for (col_idx, max_width, is_collapsed) in column_info {
4329            let col = &mut table_ctx.columns[col_idx];
4330            if is_collapsed {
4331                col.computed_width = Some(0.0);
4332            } else {
4333                // Start with max-content width, then add proportional share of excess
4334                let weight_factor = if total_weight > 0.0 {
4335                    max_width.max(1.0) / total_weight
4336                } else {
4337                    // If all columns have 0 max_width, distribute equally
4338                    1.0 / num_non_collapsed.max(1) as f32
4339                };
4340
4341                let final_width = max_width + (excess_width * weight_factor);
4342                col.computed_width = Some(final_width);
4343            }
4344        }
4345    } else if available_width >= total_min_width {
4346        // Case 2: Between min and max - interpolate proportionally
4347        // Avoid division by zero if min == max
4348        let scale = if total_max_width > total_min_width {
4349            (available_width - total_min_width) / (total_max_width - total_min_width)
4350        } else {
4351            0.0 // If min == max, just use min width
4352        };
4353        for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
4354            if table_ctx.collapsed_columns.contains(&col_idx) {
4355                col.computed_width = Some(0.0);
4356            } else {
4357                let interpolated = col.min_width + (col.max_width - col.min_width) * scale;
4358                col.computed_width = Some(interpolated);
4359            }
4360        }
4361    } else {
4362        // Case 3: Not enough space - scale down from min widths
4363        let scale = available_width / total_min_width;
4364        for (col_idx, col) in table_ctx.columns.iter_mut().enumerate() {
4365            if table_ctx.collapsed_columns.contains(&col_idx) {
4366                col.computed_width = Some(0.0);
4367            } else {
4368                col.computed_width = Some(col.min_width * scale);
4369            }
4370        }
4371    }
4372
4373    Ok(())
4374}
4375
4376/// Distribute a multi-column cell's width across the columns it spans
4377fn distribute_cell_width_across_columns(
4378    columns: &mut [TableColumnInfo],
4379    start_col: usize,
4380    colspan: usize,
4381    cell_min_width: f32,
4382    cell_max_width: f32,
4383    collapsed_columns: &std::collections::HashSet<usize>,
4384) {
4385    let end_col = start_col + colspan;
4386    if end_col > columns.len() {
4387        return;
4388    }
4389
4390    // Calculate current total of spanned non-collapsed columns
4391    let current_min_total: f32 = columns[start_col..end_col]
4392        .iter()
4393        .enumerate()
4394        .filter(|(idx, _)| !collapsed_columns.contains(&(start_col + idx)))
4395        .map(|(_, c)| c.min_width)
4396        .sum();
4397    let current_max_total: f32 = columns[start_col..end_col]
4398        .iter()
4399        .enumerate()
4400        .filter(|(idx, _)| !collapsed_columns.contains(&(start_col + idx)))
4401        .map(|(_, c)| c.max_width)
4402        .sum();
4403
4404    // Count non-collapsed columns in the span
4405    let num_visible_cols = (start_col..end_col)
4406        .filter(|idx| !collapsed_columns.contains(idx))
4407        .count();
4408
4409    if num_visible_cols == 0 {
4410        return; // All spanned columns are collapsed
4411    }
4412
4413    // Only distribute if the cell needs more space than currently available
4414    if cell_min_width > current_min_total {
4415        let extra_min = cell_min_width - current_min_total;
4416        let per_col = extra_min / num_visible_cols as f32;
4417        for (idx, col) in columns[start_col..end_col].iter_mut().enumerate() {
4418            if !collapsed_columns.contains(&(start_col + idx)) {
4419                col.min_width += per_col;
4420            }
4421        }
4422    }
4423
4424    if cell_max_width > current_max_total {
4425        let extra_max = cell_max_width - current_max_total;
4426        let per_col = extra_max / num_visible_cols as f32;
4427        for (idx, col) in columns[start_col..end_col].iter_mut().enumerate() {
4428            if !collapsed_columns.contains(&(start_col + idx)) {
4429                col.max_width += per_col;
4430            }
4431        }
4432    }
4433}
4434
4435/// Layout a cell with its computed column width to determine its content height
4436fn layout_cell_for_height<T: ParsedFontTrait>(
4437    ctx: &mut LayoutContext<'_, T>,
4438    tree: &mut LayoutTree,
4439    text_cache: &mut crate::font_traits::TextLayoutCache,
4440    cell_index: usize,
4441    cell_width: f32,
4442    constraints: &LayoutConstraints,
4443) -> Result<f32> {
4444    let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4445    let cell_dom_id = cell_node.dom_node_id.ok_or(LayoutError::InvalidTree)?;
4446
4447    // Check if cell has text content directly in DOM (not in LayoutTree)
4448    // Text nodes are intentionally not included in LayoutTree per CSS spec,
4449    // but we need to measure them for table cell height calculation.
4450    let has_text_children = cell_dom_id
4451        .az_children(&ctx.styled_dom.node_hierarchy.as_container())
4452        .any(|child_id| {
4453            let node_data = &ctx.styled_dom.node_data.as_container()[child_id];
4454            matches!(node_data.get_node_type(), NodeType::Text(_))
4455        });
4456
4457    debug_table_layout!(
4458        ctx,
4459        "layout_cell_for_height: cell_index={}, has_text_children={}",
4460        cell_index,
4461        has_text_children
4462    );
4463
4464    // Get padding and border to calculate content width
4465    let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4466    let padding = &cell_node.box_props.padding;
4467    let border = &cell_node.box_props.border;
4468    let writing_mode = constraints.writing_mode;
4469
4470    // cell_width is the border-box width (includes padding/border from column
4471    // width calculation) but layout functions need content-box width
4472    let content_width = cell_width
4473        - padding.cross_start(writing_mode)
4474        - padding.cross_end(writing_mode)
4475        - border.cross_start(writing_mode)
4476        - border.cross_end(writing_mode);
4477
4478    debug_table_layout!(
4479        ctx,
4480        "Cell width: border_box={:.2}, content_box={:.2}",
4481        cell_width,
4482        content_width
4483    );
4484
4485    let content_height = if has_text_children {
4486        // Cell contains text - use IFC to measure it
4487        debug_table_layout!(ctx, "Using IFC to measure text content");
4488
4489        let cell_constraints = LayoutConstraints {
4490            available_size: LogicalSize {
4491                width: content_width, // Use content width, not border-box width
4492                height: f32::INFINITY,
4493            },
4494            writing_mode: constraints.writing_mode,
4495            bfc_state: None,
4496            text_align: constraints.text_align,
4497            containing_block_size: constraints.containing_block_size,
4498            // Use definite width for final cell layout!
4499            // This replaces any previous MinContent/MaxContent measurement.
4500            available_width_type: Text3AvailableSpace::Definite(content_width),
4501        };
4502
4503        let output = layout_ifc(ctx, text_cache, tree, cell_index, &cell_constraints)?;
4504
4505        debug_table_layout!(
4506            ctx,
4507            "IFC returned height={:.2}",
4508            output.overflow_size.height
4509        );
4510
4511        output.overflow_size.height
4512    } else {
4513        // Cell contains block-level children or is empty - use regular layout
4514        debug_table_layout!(ctx, "Using regular layout for block children");
4515
4516        let cell_constraints = LayoutConstraints {
4517            available_size: LogicalSize {
4518                width: content_width, // Use content width, not border-box width
4519                height: f32::INFINITY,
4520            },
4521            writing_mode: constraints.writing_mode,
4522            bfc_state: None,
4523            text_align: constraints.text_align,
4524            containing_block_size: constraints.containing_block_size,
4525            // Use Definite width for final cell layout!
4526            available_width_type: Text3AvailableSpace::Definite(content_width),
4527        };
4528
4529        let mut temp_positions: super::PositionVec = Vec::new();
4530        let mut temp_scrollbar_reflow = false;
4531        let mut temp_float_cache = std::collections::BTreeMap::new();
4532
4533        crate::solver3::cache::calculate_layout_for_subtree(
4534            ctx,
4535            tree,
4536            text_cache,
4537            cell_index,
4538            LogicalPosition::zero(),
4539            cell_constraints.available_size,
4540            &mut temp_positions,
4541            &mut temp_scrollbar_reflow,
4542            &mut temp_float_cache,
4543            // PerformLayout: final table cell layout with definite width
4544            crate::solver3::cache::ComputeMode::PerformLayout,
4545        )?;
4546
4547        let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4548        cell_node.used_size.unwrap_or_default().height
4549    };
4550
4551    // Add padding and border to get the total height
4552    let cell_node = tree.get(cell_index).ok_or(LayoutError::InvalidTree)?;
4553    let padding = &cell_node.box_props.padding;
4554    let border = &cell_node.box_props.border;
4555    let writing_mode = constraints.writing_mode;
4556
4557    let total_height = content_height
4558        + padding.main_start(writing_mode)
4559        + padding.main_end(writing_mode)
4560        + border.main_start(writing_mode)
4561        + border.main_end(writing_mode);
4562
4563    debug_table_layout!(
4564        ctx,
4565        "Cell total height: cell_index={}, content={:.2}, padding/border={:.2}, total={:.2}",
4566        cell_index,
4567        content_height,
4568        padding.main_start(writing_mode)
4569            + padding.main_end(writing_mode)
4570            + border.main_start(writing_mode)
4571            + border.main_end(writing_mode),
4572        total_height
4573    );
4574
4575    Ok(total_height)
4576}
4577
4578/// Calculate row heights based on cell content after column widths are determined
4579fn calculate_row_heights<T: ParsedFontTrait>(
4580    table_ctx: &mut TableLayoutContext,
4581    tree: &mut LayoutTree,
4582    text_cache: &mut crate::font_traits::TextLayoutCache,
4583    ctx: &mut LayoutContext<'_, T>,
4584    constraints: &LayoutConstraints,
4585) -> Result<()> {
4586    debug_table_layout!(
4587        ctx,
4588        "calculate_row_heights: num_rows={}, available_size={:?}",
4589        table_ctx.num_rows,
4590        constraints.available_size
4591    );
4592
4593    // Initialize row heights
4594    table_ctx.row_heights = vec![0.0; table_ctx.num_rows];
4595
4596    // CSS 2.2 Section 17.6: Set collapsed rows to height 0
4597    for &row_idx in &table_ctx.collapsed_rows {
4598        if row_idx < table_ctx.row_heights.len() {
4599            table_ctx.row_heights[row_idx] = 0.0;
4600        }
4601    }
4602
4603    // First pass: Calculate heights for cells that don't span multiple rows
4604    for cell_info in &table_ctx.cells {
4605        // Skip cells in collapsed rows
4606        if table_ctx.collapsed_rows.contains(&cell_info.row) {
4607            continue;
4608        }
4609
4610        // Get the cell's width (sum of column widths if colspan > 1)
4611        let mut cell_width = 0.0;
4612        for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
4613            if let Some(col) = table_ctx.columns.get(col_idx) {
4614                if let Some(width) = col.computed_width {
4615                    cell_width += width;
4616                }
4617            }
4618        }
4619
4620        debug_table_layout!(
4621            ctx,
4622            "Cell layout: node_index={}, row={}, col={}, width={:.2}",
4623            cell_info.node_index,
4624            cell_info.row,
4625            cell_info.column,
4626            cell_width
4627        );
4628
4629        // Layout the cell to get its height
4630        let cell_height = layout_cell_for_height(
4631            ctx,
4632            tree,
4633            text_cache,
4634            cell_info.node_index,
4635            cell_width,
4636            constraints,
4637        )?;
4638
4639        debug_table_layout!(
4640            ctx,
4641            "Cell height calculated: node_index={}, height={:.2}",
4642            cell_info.node_index,
4643            cell_height
4644        );
4645
4646        // For single-row cells, update the row height
4647        if cell_info.rowspan == 1 {
4648            let current_height = table_ctx.row_heights[cell_info.row];
4649            table_ctx.row_heights[cell_info.row] = current_height.max(cell_height);
4650        }
4651    }
4652
4653    // Second pass: Handle cells that span multiple rows (rowspan > 1)
4654    for cell_info in &table_ctx.cells {
4655        // Skip cells that start in collapsed rows
4656        if table_ctx.collapsed_rows.contains(&cell_info.row) {
4657            continue;
4658        }
4659
4660        if cell_info.rowspan > 1 {
4661            // Get the cell's width
4662            let mut cell_width = 0.0;
4663            for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
4664                if let Some(col) = table_ctx.columns.get(col_idx) {
4665                    if let Some(width) = col.computed_width {
4666                        cell_width += width;
4667                    }
4668                }
4669            }
4670
4671            // Layout the cell to get its height
4672            let cell_height = layout_cell_for_height(
4673                ctx,
4674                tree,
4675                text_cache,
4676                cell_info.node_index,
4677                cell_width,
4678                constraints,
4679            )?;
4680
4681            // Calculate the current total height of spanned rows (excluding collapsed rows)
4682            let end_row = cell_info.row + cell_info.rowspan;
4683            let current_total: f32 = table_ctx.row_heights[cell_info.row..end_row]
4684                .iter()
4685                .enumerate()
4686                .filter(|(idx, _)| !table_ctx.collapsed_rows.contains(&(cell_info.row + idx)))
4687                .map(|(_, height)| height)
4688                .sum();
4689
4690            // If the cell needs more height, distribute extra height across
4691            // non-collapsed spanned rows
4692            if cell_height > current_total {
4693                let extra_height = cell_height - current_total;
4694
4695                // Count non-collapsed rows in span
4696                let non_collapsed_rows = (cell_info.row..end_row)
4697                    .filter(|row_idx| !table_ctx.collapsed_rows.contains(row_idx))
4698                    .count();
4699
4700                if non_collapsed_rows > 0 {
4701                    let per_row = extra_height / non_collapsed_rows as f32;
4702
4703                    for row_idx in cell_info.row..end_row {
4704                        if !table_ctx.collapsed_rows.contains(&row_idx) {
4705                            table_ctx.row_heights[row_idx] += per_row;
4706                        }
4707                    }
4708                }
4709            }
4710        }
4711    }
4712
4713    // CSS 2.2 Section 17.6: Final pass - ensure collapsed rows have height 0
4714    for &row_idx in &table_ctx.collapsed_rows {
4715        if row_idx < table_ctx.row_heights.len() {
4716            table_ctx.row_heights[row_idx] = 0.0;
4717        }
4718    }
4719
4720    Ok(())
4721}
4722
4723/// Position all cells in the table grid with calculated widths and heights
4724fn position_table_cells<T: ParsedFontTrait>(
4725    table_ctx: &mut TableLayoutContext,
4726    tree: &mut LayoutTree,
4727    ctx: &mut LayoutContext<'_, T>,
4728    table_index: usize,
4729    constraints: &LayoutConstraints,
4730) -> Result<BTreeMap<usize, LogicalPosition>> {
4731    debug_log!(ctx, "Positioning table cells in grid");
4732
4733    let mut positions = BTreeMap::new();
4734
4735    // Get border spacing values if border-collapse is separate
4736    let (h_spacing, v_spacing) = if table_ctx.border_collapse == StyleBorderCollapse::Separate {
4737        let styled_dom = ctx.styled_dom;
4738        let table_id = tree.nodes[table_index].dom_node_id.unwrap();
4739        let table_state = &styled_dom.styled_nodes.as_container()[table_id].styled_node_state;
4740
4741        let spacing_context = ResolutionContext {
4742            element_font_size: get_element_font_size(styled_dom, table_id, table_state),
4743            parent_font_size: get_parent_font_size(styled_dom, table_id, table_state),
4744            root_font_size: get_root_font_size(styled_dom, table_state),
4745            containing_block_size: PhysicalSize::new(0.0, 0.0),
4746            element_size: None,
4747            viewport_size: PhysicalSize::new(0.0, 0.0), // TODO: Get actual DPI scale from ctx
4748        };
4749
4750        let h = table_ctx
4751            .border_spacing
4752            .horizontal
4753            .resolve_with_context(&spacing_context, PropertyContext::Other);
4754
4755        let v = table_ctx
4756            .border_spacing
4757            .vertical
4758            .resolve_with_context(&spacing_context, PropertyContext::Other);
4759
4760        (h, v)
4761    } else {
4762        (0.0, 0.0)
4763    };
4764
4765    debug_log!(
4766        ctx,
4767        "Border spacing: h={:.2}, v={:.2}",
4768        h_spacing,
4769        v_spacing
4770    );
4771
4772    // Calculate cumulative column positions (x-offsets) with spacing
4773    let mut col_positions = vec![0.0; table_ctx.columns.len()];
4774    let mut x_offset = h_spacing; // Start with spacing on the left
4775    for (i, col) in table_ctx.columns.iter().enumerate() {
4776        col_positions[i] = x_offset;
4777        if let Some(width) = col.computed_width {
4778            x_offset += width + h_spacing; // Add spacing between columns
4779        }
4780    }
4781
4782    // Calculate cumulative row positions (y-offsets) with spacing
4783    let mut row_positions = vec![0.0; table_ctx.num_rows];
4784    let mut y_offset = v_spacing; // Start with spacing on the top
4785    for (i, &height) in table_ctx.row_heights.iter().enumerate() {
4786        row_positions[i] = y_offset;
4787        y_offset += height + v_spacing; // Add spacing between rows
4788    }
4789
4790    // Position each cell
4791    for cell_info in &table_ctx.cells {
4792        let cell_node = tree
4793            .get_mut(cell_info.node_index)
4794            .ok_or(LayoutError::InvalidTree)?;
4795
4796        // Calculate cell position
4797        let x = col_positions.get(cell_info.column).copied().unwrap_or(0.0);
4798        let y = row_positions.get(cell_info.row).copied().unwrap_or(0.0);
4799
4800        // Calculate cell size (sum of spanned columns/rows)
4801        let mut width = 0.0;
4802        debug_info!(
4803            ctx,
4804            "[position_table_cells] Cell {}: calculating width from cols {}..{}",
4805            cell_info.node_index,
4806            cell_info.column,
4807            cell_info.column + cell_info.colspan
4808        );
4809        for col_idx in cell_info.column..(cell_info.column + cell_info.colspan) {
4810            if let Some(col) = table_ctx.columns.get(col_idx) {
4811                debug_info!(
4812                    ctx,
4813                    "[position_table_cells]   Col {}: computed_width={:?}",
4814                    col_idx,
4815                    col.computed_width
4816                );
4817                if let Some(col_width) = col.computed_width {
4818                    width += col_width;
4819                    // Add spacing between spanned columns (but not after the last one)
4820                    if col_idx < cell_info.column + cell_info.colspan - 1 {
4821                        width += h_spacing;
4822                    }
4823                } else {
4824                    debug_info!(
4825                        ctx,
4826                        "[position_table_cells]   WARN:  Col {} has NO computed_width!",
4827                        col_idx
4828                    );
4829                }
4830            } else {
4831                debug_info!(
4832                    ctx,
4833                    "[position_table_cells]   WARN:  Col {} not found in table_ctx.columns!",
4834                    col_idx
4835                );
4836            }
4837        }
4838
4839        let mut height = 0.0;
4840        let end_row = cell_info.row + cell_info.rowspan;
4841        for row_idx in cell_info.row..end_row {
4842            if let Some(&row_height) = table_ctx.row_heights.get(row_idx) {
4843                height += row_height;
4844                // Add spacing between spanned rows (but not after the last one)
4845                if row_idx < end_row - 1 {
4846                    height += v_spacing;
4847                }
4848            }
4849        }
4850
4851        // Update cell's used size and position
4852        let writing_mode = constraints.writing_mode;
4853        // Table layout works in main/cross axes, must convert back to logical width/height
4854
4855        debug_info!(
4856            ctx,
4857            "[position_table_cells] Cell {}: BEFORE from_main_cross: width={}, height={}, \
4858             writing_mode={:?}",
4859            cell_info.node_index,
4860            width,
4861            height,
4862            writing_mode
4863        );
4864
4865        cell_node.used_size = Some(LogicalSize::from_main_cross(height, width, writing_mode));
4866
4867        debug_info!(
4868            ctx,
4869            "[position_table_cells] Cell {}: AFTER from_main_cross: used_size={:?}",
4870            cell_info.node_index,
4871            cell_node.used_size
4872        );
4873
4874        debug_info!(
4875            ctx,
4876            "[position_table_cells] Cell {}: setting used_size to {}x{} (row_heights={:?})",
4877            cell_info.node_index,
4878            width,
4879            height,
4880            table_ctx.row_heights
4881        );
4882
4883        // Apply vertical-align to cell content if it has inline layout
4884        if let Some(ref cached_layout) = cell_node.inline_layout_result {
4885            let inline_result = &cached_layout.layout;
4886            use StyleVerticalAlign;
4887
4888            // Get vertical-align property from styled_dom
4889            let vertical_align = if let Some(dom_id) = cell_node.dom_node_id {
4890                let node_state = StyledNodeState::default();
4891
4892                match get_vertical_align_property(ctx.styled_dom, dom_id, &node_state) {
4893                    MultiValue::Exact(v) => v,
4894                    _ => StyleVerticalAlign::Top,
4895                }
4896            } else {
4897                StyleVerticalAlign::Top
4898            };
4899
4900            // Calculate content height from inline layout bounds
4901            let content_bounds = inline_result.bounds();
4902            let content_height = content_bounds.height;
4903
4904            // Get padding and border to calculate content-box height
4905            // height is border-box, but vertical alignment should be within content-box
4906            let padding = &cell_node.box_props.padding;
4907            let border = &cell_node.box_props.border;
4908            let content_box_height = height
4909                - padding.main_start(writing_mode)
4910                - padding.main_end(writing_mode)
4911                - border.main_start(writing_mode)
4912                - border.main_end(writing_mode);
4913
4914            // Calculate vertical offset based on alignment within content-box
4915            let align_factor = match vertical_align {
4916                StyleVerticalAlign::Top => 0.0,
4917                StyleVerticalAlign::Middle => 0.5,
4918                StyleVerticalAlign::Bottom => 1.0,
4919                // For inline text alignments within table cells, default to middle
4920                StyleVerticalAlign::Baseline
4921                | StyleVerticalAlign::Sub
4922                | StyleVerticalAlign::Superscript
4923                | StyleVerticalAlign::TextTop
4924                | StyleVerticalAlign::TextBottom => 0.5,
4925            };
4926            let y_offset = (content_box_height - content_height) * align_factor;
4927
4928            debug_info!(
4929                ctx,
4930                "[position_table_cells] Cell {}: vertical-align={:?}, border_box_height={}, \
4931                 content_box_height={}, content_height={}, y_offset={}",
4932                cell_info.node_index,
4933                vertical_align,
4934                height,
4935                content_box_height,
4936                content_height,
4937                y_offset
4938            );
4939
4940            // Create new layout with adjusted positions
4941            if y_offset.abs() > 0.01 {
4942                // Only adjust if offset is significant
4943                use std::sync::Arc;
4944
4945                use crate::text3::cache::{PositionedItem, UnifiedLayout};
4946
4947                let adjusted_items: Vec<PositionedItem> = inline_result
4948                    .items
4949                    .iter()
4950                    .map(|item| PositionedItem {
4951                        item: item.item.clone(),
4952                        position: crate::text3::cache::Point {
4953                            x: item.position.x,
4954                            y: item.position.y + y_offset,
4955                        },
4956                        line_index: item.line_index,
4957                    })
4958                    .collect();
4959
4960                let adjusted_layout = UnifiedLayout {
4961                    items: adjusted_items,
4962                    overflow: inline_result.overflow.clone(),
4963                };
4964
4965                // Keep the same constraint type from the cached layout
4966                cell_node.inline_layout_result = Some(CachedInlineLayout::new(
4967                    Arc::new(adjusted_layout),
4968                    cached_layout.available_width,
4969                    cached_layout.has_floats,
4970                ));
4971            }
4972        }
4973
4974        // Store position relative to table origin
4975        let position = LogicalPosition::from_main_cross(y, x, writing_mode);
4976
4977        // Insert position into map so cache module can position the cell
4978        positions.insert(cell_info.node_index, position);
4979
4980        debug_log!(
4981            ctx,
4982            "Cell at row={}, col={}: pos=({:.2}, {:.2}), size=({:.2}x{:.2})",
4983            cell_info.row,
4984            cell_info.column,
4985            x,
4986            y,
4987            width,
4988            height
4989        );
4990    }
4991
4992    Ok(positions)
4993}
4994
4995/// Gathers all inline content for `text3`, recursively laying out `inline-block` children
4996/// to determine their size and baseline before passing them to the text engine.
4997///
4998/// This function also assigns IFC membership to all participating nodes:
4999/// - The IFC root gets an `ifc_id` assigned
5000/// - Each text/inline child gets `ifc_membership` set with a reference back to the IFC root
5001///
5002/// This mapping enables efficient cursor hit-testing: when a text node is clicked,
5003/// we can find its parent IFC's `inline_layout_result` via `ifc_membership.ifc_root_layout_index`.
5004fn collect_and_measure_inline_content<T: ParsedFontTrait>(
5005    ctx: &mut LayoutContext<'_, T>,
5006    text_cache: &mut TextLayoutCache,
5007    tree: &mut LayoutTree,
5008    ifc_root_index: usize,
5009    constraints: &LayoutConstraints,
5010) -> Result<(Vec<InlineContent>, HashMap<ContentIndex, usize>)> {
5011    use crate::solver3::layout_tree::{IfcId, IfcMembership};
5012    use crate::text3::cache::InlineContent;
5013
5014    let result = collect_and_measure_inline_content_impl(ctx, text_cache, tree, ifc_root_index, constraints)?;
5015    Ok(result)
5016}
5017
5018fn collect_and_measure_inline_content_impl<T: ParsedFontTrait>(
5019    ctx: &mut LayoutContext<'_, T>,
5020    text_cache: &mut TextLayoutCache,
5021    tree: &mut LayoutTree,
5022    ifc_root_index: usize,
5023    constraints: &LayoutConstraints,
5024) -> Result<(Vec<InlineContent>, HashMap<ContentIndex, usize>)> {
5025    use crate::solver3::layout_tree::{IfcId, IfcMembership};
5026
5027    debug_ifc_layout!(
5028        ctx,
5029        "collect_and_measure_inline_content: node_index={}",
5030        ifc_root_index
5031    );
5032
5033    // Generate a unique IFC ID for this inline formatting context
5034    let ifc_id = IfcId::unique();
5035
5036    // Store IFC ID on the IFC root node
5037    if let Some(ifc_root_node) = tree.get_mut(ifc_root_index) {
5038        ifc_root_node.ifc_id = Some(ifc_id);
5039    }
5040
5041    let mut content = Vec::new();
5042    // Maps the `ContentIndex` used by text3 back to the `LayoutNode` index.
5043    let mut child_map = HashMap::new();
5044    // Track the current run index for IFC membership assignment
5045    let mut current_run_index: u32 = 0;
5046
5047    let ifc_root_node = tree.get(ifc_root_index).ok_or(LayoutError::InvalidTree)?;
5048
5049    // Check if this is an anonymous IFC wrapper (has no DOM ID)
5050    let is_anonymous = ifc_root_node.dom_node_id.is_none();
5051
5052    // Get the DOM node ID of the IFC root, or find it from parent/children for anonymous boxes
5053    // CSS 2.2 § 9.2.1.1: Anonymous boxes inherit properties from their enclosing box
5054    let ifc_root_dom_id = match ifc_root_node.dom_node_id {
5055        Some(id) => id,
5056        None => {
5057            // Anonymous box - get DOM ID from parent or first child with DOM ID
5058            let parent_dom_id = ifc_root_node
5059                .parent
5060                .and_then(|p| tree.get(p))
5061                .and_then(|n| n.dom_node_id);
5062
5063            if let Some(id) = parent_dom_id {
5064                id
5065            } else {
5066                // Try to find DOM ID from first child
5067                match ifc_root_node
5068                    .children
5069                    .iter()
5070                    .filter_map(|&child_idx| tree.get(child_idx))
5071                    .filter_map(|n| n.dom_node_id)
5072                    .next()
5073                {
5074                    Some(id) => id,
5075                    None => {
5076                        debug_warning!(ctx, "IFC root and all ancestors/children have no DOM ID");
5077                        return Ok((content, child_map));
5078                    }
5079                }
5080            }
5081        }
5082    };
5083
5084    // Collect children to avoid holding an immutable borrow during iteration
5085    let children: Vec<_> = ifc_root_node.children.clone();
5086    drop(ifc_root_node);
5087
5088    debug_ifc_layout!(
5089        ctx,
5090        "Node {} has {} layout children, is_anonymous={}",
5091        ifc_root_index,
5092        children.len(),
5093        is_anonymous
5094    );
5095
5096    // For anonymous IFC wrappers, we collect content from layout tree children
5097    // For regular IFC roots, we also check DOM children for text nodes
5098    if is_anonymous {
5099        // Anonymous IFC wrapper - iterate over layout tree children and collect their content
5100        for (item_idx, &child_index) in children.iter().enumerate() {
5101            let content_index = ContentIndex {
5102                run_index: ifc_root_index as u32,
5103                item_index: item_idx as u32,
5104            };
5105
5106            let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
5107            let Some(dom_id) = child_node.dom_node_id else {
5108                debug_warning!(
5109                    ctx,
5110                    "Anonymous IFC child at index {} has no DOM ID",
5111                    child_index
5112                );
5113                continue;
5114            };
5115
5116            let node_data = &ctx.styled_dom.node_data.as_container()[dom_id];
5117
5118            // Check if this is a text node
5119            if let NodeType::Text(ref text_content) = node_data.get_node_type() {
5120                debug_info!(
5121                    ctx,
5122                    "[collect_and_measure_inline_content] OK: Found text node (DOM {:?}) in anonymous wrapper: '{}'",
5123                    dom_id,
5124                    text_content.as_str()
5125                );
5126                // Get style from the TEXT NODE itself (dom_id), not the IFC root
5127                // This ensures inline styles like color: #666666 are applied to the text
5128                // Uses split_text_for_whitespace to correctly handle white-space: pre with \n
5129                let style = Arc::new(get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref()));
5130                let text_items = split_text_for_whitespace(
5131                    ctx.styled_dom,
5132                    dom_id,
5133                    text_content.as_str(),
5134                    style,
5135                );
5136                content.extend(text_items);
5137                child_map.insert(content_index, child_index);
5138                
5139                // Set IFC membership on the text node - drop child_node borrow first
5140                drop(child_node);
5141                if let Some(child_node_mut) = tree.get_mut(child_index) {
5142                    child_node_mut.ifc_membership = Some(IfcMembership {
5143                        ifc_id,
5144                        ifc_root_layout_index: ifc_root_index,
5145                        run_index: current_run_index,
5146                    });
5147                }
5148                current_run_index += 1;
5149                
5150                continue;
5151            }
5152
5153            // Non-text inline child - add as shape for inline-block
5154            let display = get_display_property(ctx.styled_dom, Some(dom_id)).unwrap_or_default();
5155
5156            if display != LayoutDisplay::Inline {
5157                // This is an atomic inline-level box (e.g., inline-block, image).
5158                // We must determine its size and baseline before passing it to text3.
5159
5160                // The intrinsic sizing pass has already calculated its preferred size.
5161                let intrinsic_size = child_node.intrinsic_sizes.clone().unwrap_or_default();
5162                let box_props = child_node.box_props.clone();
5163
5164                let styled_node_state = ctx
5165                    .styled_dom
5166                    .styled_nodes
5167                    .as_container()
5168                    .get(dom_id)
5169                    .map(|n| n.styled_node_state.clone())
5170                    .unwrap_or_default();
5171
5172                // Calculate tentative border-box size based on CSS properties
5173                let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
5174                    ctx.styled_dom,
5175                    Some(dom_id),
5176                    constraints.containing_block_size,
5177                    intrinsic_size,
5178                    &box_props,
5179                    ctx.viewport_size,
5180                )?;
5181
5182                let writing_mode = get_writing_mode(ctx.styled_dom, dom_id, &styled_node_state)
5183                    .unwrap_or_default();
5184
5185                // Determine content-box size for laying out children
5186                let content_box_size = box_props.inner_size(tentative_size, writing_mode);
5187
5188                // To find its height and baseline, we must lay out its contents.
5189                let child_constraints = LayoutConstraints {
5190                    available_size: LogicalSize::new(content_box_size.width, f32::INFINITY),
5191                    writing_mode,
5192                    bfc_state: None,
5193                    text_align: TextAlign::Start,
5194                    containing_block_size: constraints.containing_block_size,
5195                    available_width_type: Text3AvailableSpace::Definite(content_box_size.width),
5196                };
5197
5198                // Drop the immutable borrow before calling layout_formatting_context
5199                drop(child_node);
5200
5201                // Recursively lay out the inline-block to get its final height and baseline.
5202                let mut empty_float_cache = std::collections::BTreeMap::new();
5203                let layout_result = layout_formatting_context(
5204                    ctx,
5205                    tree,
5206                    text_cache,
5207                    child_index,
5208                    &child_constraints,
5209                    &mut empty_float_cache,
5210                )?;
5211
5212                let css_height = get_css_height(ctx.styled_dom, dom_id, &styled_node_state);
5213
5214                // Determine final border-box height
5215                let final_height = match css_height.unwrap_or_default() {
5216                    LayoutHeight::Auto => {
5217                        let content_height = layout_result.output.overflow_size.height;
5218                        content_height
5219                            + box_props.padding.main_sum(writing_mode)
5220                            + box_props.border.main_sum(writing_mode)
5221                    }
5222                    _ => tentative_size.height,
5223                };
5224
5225                let final_size = LogicalSize::new(tentative_size.width, final_height);
5226
5227                // Update the node in the tree with its now-known used size.
5228                tree.get_mut(child_index).unwrap().used_size = Some(final_size);
5229
5230                let baseline_offset = layout_result.output.baseline.unwrap_or(final_height);
5231
5232                // Get margins for inline-block positioning in the inline flow
5233                // The margin-box size is used so text3 positions inline-blocks with proper spacing
5234                let margin = &box_props.margin;
5235                let margin_box_width = final_size.width + margin.left + margin.right;
5236                let margin_box_height = final_size.height + margin.top + margin.bottom;
5237
5238                // For inline-block shapes, text3 uses the content array index as run_index
5239                // and always item_index=0 for objects. We must match this when inserting into child_map.
5240                let shape_content_index = ContentIndex {
5241                    run_index: content.len() as u32,
5242                    item_index: 0,
5243                };
5244                content.push(InlineContent::Shape(InlineShape {
5245                    shape_def: ShapeDefinition::Rectangle {
5246                        size: crate::text3::cache::Size {
5247                            // Use margin-box size for positioning in inline flow
5248                            width: margin_box_width,
5249                            height: margin_box_height,
5250                        },
5251                        corner_radius: None,
5252                    },
5253                    fill: None,
5254                    stroke: None,
5255                    // Adjust baseline offset by top margin
5256                    baseline_offset: baseline_offset + margin.top,
5257                    alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, dom_id),
5258                    source_node_id: Some(dom_id),
5259                }));
5260                child_map.insert(shape_content_index, child_index);
5261            } else {
5262                // Regular inline element - collect its text children
5263                let span_style = get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref());
5264                collect_inline_span_recursive(
5265                    ctx,
5266                    tree,
5267                    dom_id,
5268                    span_style,
5269                    &mut content,
5270                    &mut child_map,
5271                    &children,
5272                    constraints,
5273                )?;
5274            }
5275        }
5276
5277        return Ok((content, child_map));
5278    }
5279
5280    // Regular (non-anonymous) IFC root - check for list markers and use DOM traversal
5281
5282    // Check if this IFC root OR its parent is a list-item and needs a marker
5283    // Case 1: IFC root itself is list-item (e.g., <li> with display: list-item)
5284    // Case 2: IFC root's parent is list-item (e.g., <li><text>...</text></li>)
5285    let ifc_root_node = tree.get(ifc_root_index).ok_or(LayoutError::InvalidTree)?;
5286    let mut list_item_dom_id: Option<NodeId> = None;
5287
5288    // Check IFC root itself
5289    if let Some(dom_id) = ifc_root_node.dom_node_id {
5290        use crate::solver3::getters::get_display_property;
5291        if let MultiValue::Exact(display) = get_display_property(ctx.styled_dom, Some(dom_id)) {
5292            use LayoutDisplay;
5293            if display == LayoutDisplay::ListItem {
5294                debug_ifc_layout!(ctx, "IFC root NodeId({:?}) is list-item", dom_id);
5295                list_item_dom_id = Some(dom_id);
5296            }
5297        }
5298    }
5299
5300    // Check IFC root's parent
5301    if list_item_dom_id.is_none() {
5302        if let Some(parent_idx) = ifc_root_node.parent {
5303            if let Some(parent_node) = tree.get(parent_idx) {
5304                if let Some(parent_dom_id) = parent_node.dom_node_id {
5305                    use crate::solver3::getters::get_display_property;
5306                    if let MultiValue::Exact(display) = get_display_property(ctx.styled_dom, Some(parent_dom_id)) {
5307                        use LayoutDisplay;
5308                        if display == LayoutDisplay::ListItem {
5309                            debug_ifc_layout!(
5310                                ctx,
5311                                "IFC root parent NodeId({:?}) is list-item",
5312                                parent_dom_id
5313                            );
5314                            list_item_dom_id = Some(parent_dom_id);
5315                        }
5316                    }
5317                }
5318            }
5319        }
5320    }
5321
5322    // If we found a list-item, generate markers
5323    if let Some(list_dom_id) = list_item_dom_id {
5324        debug_ifc_layout!(
5325            ctx,
5326            "Found list-item (NodeId({:?})), generating marker",
5327            list_dom_id
5328        );
5329
5330        // Find the layout node index for the list-item DOM node
5331        let list_item_layout_idx = tree
5332            .nodes
5333            .iter()
5334            .enumerate()
5335            .find(|(_, node)| {
5336                node.dom_node_id == Some(list_dom_id) && node.pseudo_element.is_none()
5337            })
5338            .map(|(idx, _)| idx);
5339
5340        if let Some(list_idx) = list_item_layout_idx {
5341            // Per CSS spec, the ::marker pseudo-element is the first child of the list-item
5342            // Find the ::marker pseudo-element in the list-item's children
5343            let list_item_node = tree.get(list_idx).ok_or(LayoutError::InvalidTree)?;
5344            let marker_idx = list_item_node
5345                .children
5346                .iter()
5347                .find(|&&child_idx| {
5348                    tree.get(child_idx)
5349                        .map(|child| child.pseudo_element == Some(PseudoElement::Marker))
5350                        .unwrap_or(false)
5351                })
5352                .copied();
5353
5354            if let Some(marker_idx) = marker_idx {
5355                debug_ifc_layout!(ctx, "Found ::marker pseudo-element at index {}", marker_idx);
5356
5357                // Get the DOM ID for style resolution (marker references the same DOM node as
5358                // list-item)
5359                let list_dom_id_for_style = tree
5360                    .get(marker_idx)
5361                    .and_then(|n| n.dom_node_id)
5362                    .unwrap_or(list_dom_id);
5363
5364                // Get list-style-position to determine marker positioning
5365                // Default is 'outside' per CSS Lists Module Level 3
5366
5367                let list_style_position =
5368                    get_list_style_position(ctx.styled_dom, Some(list_dom_id));
5369                let position_outside =
5370                    matches!(list_style_position, StyleListStylePosition::Outside);
5371
5372                debug_ifc_layout!(
5373                    ctx,
5374                    "List marker list-style-position: {:?} (outside={})",
5375                    list_style_position,
5376                    position_outside
5377                );
5378
5379                // Generate marker text segments - font fallback happens during shaping
5380                let base_style =
5381                    Arc::new(get_style_properties(ctx.styled_dom, list_dom_id_for_style, ctx.system_style.as_ref()));
5382                let marker_segments = generate_list_marker_segments(
5383                    tree,
5384                    ctx.styled_dom,
5385                    marker_idx, // Pass the marker index, not the list-item index
5386                    ctx.counters,
5387                    base_style,
5388                    ctx.debug_messages,
5389                );
5390
5391                debug_ifc_layout!(
5392                    ctx,
5393                    "Generated {} list marker segments",
5394                    marker_segments.len()
5395                );
5396
5397                // Add markers as InlineContent::Marker with position information
5398                // Outside markers will be positioned in the padding gutter by the layout engine
5399                for segment in marker_segments {
5400                    content.push(InlineContent::Marker {
5401                        run: segment,
5402                        position_outside,
5403                    });
5404                }
5405            } else {
5406                debug_ifc_layout!(
5407                    ctx,
5408                    "WARNING: List-item at index {} has no ::marker pseudo-element",
5409                    list_idx
5410                );
5411            }
5412        }
5413    }
5414
5415    drop(ifc_root_node);
5416
5417    // IMPORTANT: We need to traverse the DOM, not just the layout tree!
5418    //
5419    // According to CSS spec, a block container with inline-level children establishes
5420    // an IFC and should collect ALL inline content, including text nodes.
5421    // Text nodes exist in the DOM but might not have their own layout tree nodes.
5422
5423    // Debug: Check what the node_hierarchy says about this node
5424    let node_hier_item = &ctx.styled_dom.node_hierarchy.as_container()[ifc_root_dom_id];
5425    debug_info!(
5426        ctx,
5427        "[collect_and_measure_inline_content] DEBUG: node_hier_item.first_child={:?}, \
5428         last_child={:?}",
5429        node_hier_item.first_child_id(ifc_root_dom_id),
5430        node_hier_item.last_child_id()
5431    );
5432
5433    let dom_children: Vec<NodeId> = ifc_root_dom_id
5434        .az_children(&ctx.styled_dom.node_hierarchy.as_container())
5435        .collect();
5436
5437    let ifc_root_node_data = &ctx.styled_dom.node_data.as_container()[ifc_root_dom_id];
5438
5439    // SPECIAL CASE: If the IFC root itself is a text node (leaf node),
5440    // add its text content directly instead of iterating over children
5441    // Uses split_text_for_whitespace to correctly handle white-space: pre with \n
5442    if let NodeType::Text(ref text_content) = ifc_root_node_data.get_node_type() {
5443        let style = Arc::new(get_style_properties(ctx.styled_dom, ifc_root_dom_id, ctx.system_style.as_ref()));
5444        let text_items = split_text_for_whitespace(
5445            ctx.styled_dom,
5446            ifc_root_dom_id,
5447            text_content.as_str(),
5448            style,
5449        );
5450        content.extend(text_items);
5451        return Ok((content, child_map));
5452    }
5453
5454    let ifc_root_node_type = match ifc_root_node_data.get_node_type() {
5455        NodeType::Div => "Div",
5456        NodeType::Text(_) => "Text",
5457        NodeType::Body => "Body",
5458        _ => "Other",
5459    };
5460
5461    debug_info!(
5462        ctx,
5463        "[collect_and_measure_inline_content] IFC root has {} DOM children",
5464        dom_children.len()
5465    );
5466
5467    for (item_idx, &dom_child_id) in dom_children.iter().enumerate() {
5468        let content_index = ContentIndex {
5469            run_index: ifc_root_index as u32,
5470            item_index: item_idx as u32,
5471        };
5472
5473        let node_data = &ctx.styled_dom.node_data.as_container()[dom_child_id];
5474
5475        // Check if this is a text node
5476        if let NodeType::Text(ref text_content) = node_data.get_node_type() {
5477            debug_info!(
5478                ctx,
5479                "[collect_and_measure_inline_content] OK: Found text node (DOM child {:?}): '{}'",
5480                dom_child_id,
5481                text_content.as_str()
5482            );
5483            
5484            // Get style from the TEXT NODE itself (dom_child_id), not the IFC root
5485            // This ensures inline styles like color: #666666 are applied to the text
5486            // Uses split_text_for_whitespace to correctly handle white-space: pre with \n
5487            let style = Arc::new(get_style_properties(ctx.styled_dom, dom_child_id, ctx.system_style.as_ref()));
5488            let text_items = split_text_for_whitespace(
5489                ctx.styled_dom,
5490                dom_child_id,
5491                text_content.as_str(),
5492                style,
5493            );
5494            content.extend(text_items);
5495            
5496            // Set IFC membership on the text node's layout node (if it exists)
5497            // Text nodes may or may not have their own layout tree entry depending on
5498            // whether they're wrapped in an anonymous IFC wrapper
5499            if let Some(&layout_idx) = tree.dom_to_layout.get(&dom_child_id).and_then(|v| v.first()) {
5500                if let Some(text_layout_node) = tree.get_mut(layout_idx) {
5501                    text_layout_node.ifc_membership = Some(IfcMembership {
5502                        ifc_id,
5503                        ifc_root_layout_index: ifc_root_index,
5504                        run_index: current_run_index,
5505                    });
5506                }
5507            }
5508            current_run_index += 1;
5509            
5510            continue;
5511        }
5512
5513        // For non-text nodes, find their corresponding layout tree node
5514        let child_index = children
5515            .iter()
5516            .find(|&&idx| {
5517                tree.get(idx)
5518                    .and_then(|n| n.dom_node_id)
5519                    .map(|id| id == dom_child_id)
5520                    .unwrap_or(false)
5521            })
5522            .copied();
5523
5524        let Some(child_index) = child_index else {
5525            debug_info!(
5526                ctx,
5527                "[collect_and_measure_inline_content] WARN: DOM child {:?} has no layout node",
5528                dom_child_id
5529            );
5530            continue;
5531        };
5532
5533        let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
5534        // At this point we have a non-text DOM child with a layout node
5535        let dom_id = child_node.dom_node_id.unwrap();
5536
5537        let display = get_display_property(ctx.styled_dom, Some(dom_id)).unwrap_or_default();
5538        if display != LayoutDisplay::Inline {
5539            // This is an atomic inline-level box (e.g., inline-block, image).
5540            // We must determine its size and baseline before passing it to text3.
5541
5542            // The intrinsic sizing pass has already calculated its preferred size.
5543            let intrinsic_size = child_node.intrinsic_sizes.clone().unwrap_or_default();
5544            let box_props = child_node.box_props.clone();
5545
5546            let styled_node_state = ctx
5547                .styled_dom
5548                .styled_nodes
5549                .as_container()
5550                .get(dom_id)
5551                .map(|n| n.styled_node_state.clone())
5552                .unwrap_or_default();
5553
5554            // Calculate tentative border-box size based on CSS properties
5555            // This correctly handles explicit width/height, box-sizing, and constraints
5556            let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
5557                ctx.styled_dom,
5558                Some(dom_id),
5559                constraints.containing_block_size,
5560                intrinsic_size,
5561                &box_props,
5562                ctx.viewport_size,
5563            )?;
5564
5565            let writing_mode =
5566                get_writing_mode(ctx.styled_dom, dom_id, &styled_node_state).unwrap_or_default();
5567
5568            // Determine content-box size for laying out children
5569            let content_box_size = box_props.inner_size(tentative_size, writing_mode);
5570
5571            debug_info!(
5572                ctx,
5573                "[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
5574                 tentative_border_box={:?}, content_box={:?}",
5575                dom_id,
5576                tentative_size,
5577                content_box_size
5578            );
5579
5580            // To find its height and baseline, we must lay out its contents.
5581            let child_constraints = LayoutConstraints {
5582                available_size: LogicalSize::new(content_box_size.width, f32::INFINITY),
5583                writing_mode,
5584                // Inline-blocks establish a new BFC, so no state is passed in.
5585                bfc_state: None,
5586                // Does not affect size/baseline of the container.
5587                text_align: TextAlign::Start,
5588                containing_block_size: constraints.containing_block_size,
5589                available_width_type: Text3AvailableSpace::Definite(content_box_size.width),
5590            };
5591
5592            // Drop the immutable borrow before calling layout_formatting_context
5593            drop(child_node);
5594
5595            // Recursively lay out the inline-block to get its final height and baseline.
5596            // Note: This does not affect its final position, only its dimensions.
5597            let mut empty_float_cache = std::collections::BTreeMap::new();
5598            let layout_result = layout_formatting_context(
5599                ctx,
5600                tree,
5601                text_cache,
5602                child_index,
5603                &child_constraints,
5604                &mut empty_float_cache,
5605            )?;
5606
5607            let css_height = get_css_height(ctx.styled_dom, dom_id, &styled_node_state);
5608
5609            // Determine final border-box height
5610            let final_height = match css_height.clone().unwrap_or_default() {
5611                LayoutHeight::Auto => {
5612                    // For auto height, add padding and border to the content height
5613                    let content_height = layout_result.output.overflow_size.height;
5614                    content_height
5615                        + box_props.padding.main_sum(writing_mode)
5616                        + box_props.border.main_sum(writing_mode)
5617                }
5618                // For explicit height, calculate_used_size_for_node already gave us the correct border-box height
5619                _ => tentative_size.height,
5620            };
5621
5622            debug_info!(
5623                ctx,
5624                "[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
5625                 layout_content_height={}, css_height={:?}, final_border_box_height={}",
5626                dom_id,
5627                layout_result.output.overflow_size.height,
5628                css_height,
5629                final_height
5630            );
5631
5632            let final_size = LogicalSize::new(tentative_size.width, final_height);
5633
5634            // Update the node in the tree with its now-known used size.
5635            tree.get_mut(child_index).unwrap().used_size = Some(final_size);
5636
5637            // CSS 2.2 § 10.8.1: For inline-block elements, the baseline is the baseline of the
5638            // last line box in the normal flow, unless it has no in-flow line boxes, in which
5639            // case the baseline is the bottom margin edge.
5640            //
5641            // `layout_result.output.baseline` returns the Y-position of the baseline measured
5642            // from the TOP of the content box. But `get_item_vertical_metrics` expects
5643            // `baseline_offset` to be the distance from the BOTTOM to the baseline.
5644            //
5645            // Conversion: baseline_offset_from_bottom = height - baseline_from_top
5646            //
5647            // If no baseline is found (e.g., the inline-block has no text), we fall back to
5648            // the bottom margin edge (baseline_offset = 0, meaning baseline at bottom).
5649            let baseline_from_top = layout_result.output.baseline;
5650            let baseline_offset = match baseline_from_top {
5651                Some(baseline_y) => {
5652                    // baseline_y is measured from top of content box
5653                    // We need to add padding and border to get the position within the border-box
5654                    let content_box_top = box_props.padding.top + box_props.border.top;
5655                    let baseline_from_border_box_top = baseline_y + content_box_top;
5656                    // Convert to distance from bottom
5657                    (final_height - baseline_from_border_box_top).max(0.0)
5658                }
5659                None => {
5660                    // No baseline found - use bottom margin edge (baseline at bottom)
5661                    0.0
5662                }
5663            };
5664            
5665            debug_info!(
5666                ctx,
5667                "[collect_and_measure_inline_content] Inline-block NodeId({:?}): \
5668                 baseline_from_top={:?}, final_height={}, baseline_offset_from_bottom={}",
5669                dom_id,
5670                baseline_from_top,
5671                final_height,
5672                baseline_offset
5673            );
5674
5675            // Get margins for inline-block positioning
5676            // For inline-blocks, we need to include margins in the shape size
5677            // so that text3 positions them correctly with spacing
5678            let margin = &box_props.margin;
5679            let margin_box_width = final_size.width + margin.left + margin.right;
5680            let margin_box_height = final_size.height + margin.top + margin.bottom;
5681
5682            // For inline-block shapes, text3 uses the content array index as run_index
5683            // and always item_index=0 for objects. We must match this when inserting into child_map.
5684            let shape_content_index = ContentIndex {
5685                run_index: content.len() as u32,
5686                item_index: 0,
5687            };
5688            content.push(InlineContent::Shape(InlineShape {
5689                shape_def: ShapeDefinition::Rectangle {
5690                    size: crate::text3::cache::Size {
5691                        // Use margin-box size for positioning in inline flow
5692                        width: margin_box_width,
5693                        height: margin_box_height,
5694                    },
5695                    corner_radius: None,
5696                },
5697                fill: None,
5698                stroke: None,
5699                // Adjust baseline offset by top margin
5700                baseline_offset: baseline_offset + margin.top,
5701                alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, dom_id),
5702                source_node_id: Some(dom_id),
5703            }));
5704            child_map.insert(shape_content_index, child_index);
5705        } else if let NodeType::Image(image_ref) =
5706            ctx.styled_dom.node_data.as_container()[dom_id].get_node_type()
5707        {
5708            // Images are replaced elements - they have intrinsic dimensions
5709            // and CSS width/height can constrain them
5710            
5711            // Re-get child_node since we dropped it earlier for the inline-block case
5712            let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
5713            let box_props = child_node.box_props.clone();
5714
5715            // Get intrinsic size from the image data or fall back to layout node
5716            let intrinsic_size = child_node
5717                .intrinsic_sizes
5718                .clone()
5719                .unwrap_or(IntrinsicSizes {
5720                    max_content_width: 50.0,
5721                    max_content_height: 50.0,
5722                    ..Default::default()
5723                });
5724            
5725            // Get styled node state for CSS property lookup
5726            let styled_node_state = ctx
5727                .styled_dom
5728                .styled_nodes
5729                .as_container()
5730                .get(dom_id)
5731                .map(|n| n.styled_node_state.clone())
5732                .unwrap_or_default();
5733            
5734            // Calculate the used size respecting CSS width/height constraints
5735            let tentative_size = crate::solver3::sizing::calculate_used_size_for_node(
5736                ctx.styled_dom,
5737                Some(dom_id),
5738                constraints.containing_block_size,
5739                intrinsic_size.clone(),
5740                &box_props,
5741                ctx.viewport_size,
5742            )?;
5743            
5744            // Drop immutable borrow before mutable access
5745            drop(child_node);
5746            
5747            // Set the used_size on the layout node so paint_rect works correctly
5748            let final_size = LogicalSize::new(tentative_size.width, tentative_size.height);
5749            tree.get_mut(child_index).unwrap().used_size = Some(final_size);
5750            
5751            // Calculate display size for text3 (this is what text3 uses for positioning)
5752            let display_width = if final_size.width > 0.0 { 
5753                Some(final_size.width) 
5754            } else { 
5755                None 
5756            };
5757            let display_height = if final_size.height > 0.0 { 
5758                Some(final_size.height) 
5759            } else { 
5760                None 
5761            };
5762            
5763            content.push(InlineContent::Image(InlineImage {
5764                source: ImageSource::Ref(image_ref.clone()),
5765                intrinsic_size: crate::text3::cache::Size {
5766                    width: intrinsic_size.max_content_width,
5767                    height: intrinsic_size.max_content_height,
5768                },
5769                display_size: if display_width.is_some() || display_height.is_some() {
5770                    Some(crate::text3::cache::Size {
5771                        width: display_width.unwrap_or(intrinsic_size.max_content_width),
5772                        height: display_height.unwrap_or(intrinsic_size.max_content_height),
5773                    })
5774                } else {
5775                    None
5776                },
5777                // Images are bottom-aligned with the baseline by default
5778                baseline_offset: 0.0,
5779                alignment: crate::text3::cache::VerticalAlign::Baseline,
5780                object_fit: ObjectFit::Fill,
5781            }));
5782            // For images, text3 uses the content array index as run_index
5783            // and always item_index=0 for objects. We must match this.
5784            let image_content_index = ContentIndex {
5785                run_index: (content.len() - 1) as u32,  // -1 because we just pushed
5786                item_index: 0,
5787            };
5788            child_map.insert(image_content_index, child_index);
5789        } else {
5790            // This is a regular inline box (display: inline) - e.g., <span>, <em>, <strong>
5791            //
5792            // According to CSS Inline-3 spec §2, inline boxes are "transparent" wrappers
5793            // We must recursively collect their text children with inherited style
5794            debug_info!(
5795                ctx,
5796                "[collect_and_measure_inline_content] Found inline span (DOM {:?}), recursing",
5797                dom_id
5798            );
5799
5800            let span_style = get_style_properties(ctx.styled_dom, dom_id, ctx.system_style.as_ref());
5801            collect_inline_span_recursive(
5802                ctx,
5803                tree,
5804                dom_id,
5805                span_style,
5806                &mut content,
5807                &mut child_map,
5808                &children,
5809                constraints,
5810            )?;
5811        }
5812    }
5813    Ok((content, child_map))
5814}
5815
5816/// Recursively collects inline content from an inline span (display: inline) element.
5817///
5818/// According to CSS Inline Layout Module Level 3 §2:
5819///
5820/// "Inline boxes are transparent wrappers that wrap their content."
5821///
5822/// They don't create a new formatting context - their children participate in the
5823/// same IFC as the parent. This function processes:
5824///
5825/// - Text nodes: collected with the span's inherited style
5826/// - Nested inline spans: recursively descended
5827/// - Inline-blocks, images: measured and added as shapes
5828fn collect_inline_span_recursive<T: ParsedFontTrait>(
5829    ctx: &mut LayoutContext<'_, T>,
5830    tree: &mut LayoutTree,
5831    span_dom_id: NodeId,
5832    span_style: StyleProperties,
5833    content: &mut Vec<InlineContent>,
5834    child_map: &mut HashMap<ContentIndex, usize>,
5835    parent_children: &[usize], // Layout tree children of parent IFC
5836    constraints: &LayoutConstraints,
5837) -> Result<()> {
5838    debug_info!(
5839        ctx,
5840        "[collect_inline_span_recursive] Processing inline span {:?}",
5841        span_dom_id
5842    );
5843
5844    // Get DOM children of this span
5845    let span_dom_children: Vec<NodeId> = span_dom_id
5846        .az_children(&ctx.styled_dom.node_hierarchy.as_container())
5847        .collect();
5848
5849    debug_info!(
5850        ctx,
5851        "[collect_inline_span_recursive] Span has {} DOM children",
5852        span_dom_children.len()
5853    );
5854
5855    for &child_dom_id in &span_dom_children {
5856        let node_data = &ctx.styled_dom.node_data.as_container()[child_dom_id];
5857
5858        // CASE 1: Text node - collect with span's style
5859        // Use split_text_for_whitespace to correctly handle white-space: pre-wrap with \n
5860        if let NodeType::Text(ref text_content) = node_data.get_node_type() {
5861            debug_info!(
5862                ctx,
5863                "[collect_inline_span_recursive] ✓ Found text in span: '{}'",
5864                text_content.as_str()
5865            );
5866            // Use split_text_for_whitespace to correctly handle white-space: pre with \n
5867            let text_items = split_text_for_whitespace(
5868                ctx.styled_dom,
5869                child_dom_id,
5870                text_content.as_str(),
5871                Arc::new(span_style.clone()),
5872            );
5873            content.extend(text_items);
5874            continue;
5875        }
5876
5877        // CASE 2: Element node - check its display type
5878        let child_display =
5879            get_display_property(ctx.styled_dom, Some(child_dom_id)).unwrap_or_default();
5880
5881        // Find the corresponding layout tree node
5882        let child_index = parent_children
5883            .iter()
5884            .find(|&&idx| {
5885                tree.get(idx)
5886                    .and_then(|n| n.dom_node_id)
5887                    .map(|id| id == child_dom_id)
5888                    .unwrap_or(false)
5889            })
5890            .copied();
5891
5892        match child_display {
5893            LayoutDisplay::Inline => {
5894                // Nested inline span - recurse with child's style
5895                debug_info!(
5896                    ctx,
5897                    "[collect_inline_span_recursive] Found nested inline span {:?}",
5898                    child_dom_id
5899                );
5900                let child_style = get_style_properties(ctx.styled_dom, child_dom_id, ctx.system_style.as_ref());
5901                collect_inline_span_recursive(
5902                    ctx,
5903                    tree,
5904                    child_dom_id,
5905                    child_style,
5906                    content,
5907                    child_map,
5908                    parent_children,
5909                    constraints,
5910                )?;
5911            }
5912            LayoutDisplay::InlineBlock => {
5913                // Inline-block inside span - measure and add as shape
5914                let Some(child_index) = child_index else {
5915                    debug_info!(
5916                        ctx,
5917                        "[collect_inline_span_recursive] WARNING: inline-block {:?} has no layout \
5918                         node",
5919                        child_dom_id
5920                    );
5921                    continue;
5922                };
5923
5924                let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
5925                let intrinsic_size = child_node.intrinsic_sizes.clone().unwrap_or_default();
5926                let width = intrinsic_size.max_content_width;
5927
5928                let styled_node_state = ctx
5929                    .styled_dom
5930                    .styled_nodes
5931                    .as_container()
5932                    .get(child_dom_id)
5933                    .map(|n| n.styled_node_state.clone())
5934                    .unwrap_or_default();
5935                let writing_mode =
5936                    get_writing_mode(ctx.styled_dom, child_dom_id, &styled_node_state)
5937                        .unwrap_or_default();
5938                let child_constraints = LayoutConstraints {
5939                    available_size: LogicalSize::new(width, f32::INFINITY),
5940                    writing_mode,
5941                    bfc_state: None,
5942                    text_align: TextAlign::Start,
5943                    containing_block_size: constraints.containing_block_size,
5944                    available_width_type: Text3AvailableSpace::Definite(width),
5945                };
5946
5947                drop(child_node);
5948
5949                let mut empty_float_cache = std::collections::BTreeMap::new();
5950                let layout_result = layout_formatting_context(
5951                    ctx,
5952                    tree,
5953                    &mut TextLayoutCache::default(),
5954                    child_index,
5955                    &child_constraints,
5956                    &mut empty_float_cache,
5957                )?;
5958                let final_height = layout_result.output.overflow_size.height;
5959                let final_size = LogicalSize::new(width, final_height);
5960
5961                tree.get_mut(child_index).unwrap().used_size = Some(final_size);
5962                let baseline_offset = layout_result.output.baseline.unwrap_or(final_height);
5963
5964                content.push(InlineContent::Shape(InlineShape {
5965                    shape_def: ShapeDefinition::Rectangle {
5966                        size: crate::text3::cache::Size {
5967                            width,
5968                            height: final_height,
5969                        },
5970                        corner_radius: None,
5971                    },
5972                    fill: None,
5973                    stroke: None,
5974                    baseline_offset,
5975                    alignment: crate::solver3::getters::get_vertical_align_for_node(ctx.styled_dom, child_dom_id),
5976                    source_node_id: Some(child_dom_id),
5977                }));
5978
5979                // Note: We don't add to child_map here because this is inside a span
5980                debug_info!(
5981                    ctx,
5982                    "[collect_inline_span_recursive] Added inline-block shape {}x{}",
5983                    width,
5984                    final_height
5985                );
5986            }
5987            _ => {
5988                // Other display types inside span (shouldn't normally happen)
5989                debug_info!(
5990                    ctx,
5991                    "[collect_inline_span_recursive] WARNING: Unsupported display type {:?} \
5992                     inside inline span",
5993                    child_display
5994                );
5995            }
5996        }
5997    }
5998
5999    Ok(())
6000}
6001
6002/// Positions a floated child within the BFC and updates the floating context.
6003/// This function is fully writing-mode aware.
6004fn position_floated_child(
6005    _child_index: usize,
6006    child_margin_box_size: LogicalSize,
6007    float_type: LayoutFloat,
6008    constraints: &LayoutConstraints,
6009    _bfc_content_box: LogicalRect,
6010    current_main_offset: f32,
6011    floating_context: &mut FloatingContext,
6012) -> Result<LogicalPosition> {
6013    let wm = constraints.writing_mode;
6014    let child_main_size = child_margin_box_size.main(wm);
6015    let child_cross_size = child_margin_box_size.cross(wm);
6016    let bfc_cross_size = constraints.available_size.cross(wm);
6017    let mut placement_main_offset = current_main_offset;
6018
6019    loop {
6020        // 1. Determine the available cross-axis space at the current
6021        // `placement_main_offset`.
6022        let (available_cross_start, available_cross_end) = floating_context
6023            .available_line_box_space(
6024                placement_main_offset,
6025                placement_main_offset + child_main_size,
6026                bfc_cross_size,
6027                wm,
6028            );
6029
6030        let available_cross_width = available_cross_end - available_cross_start;
6031
6032        // 2. Check if the new float can fit in the available space.
6033        if child_cross_size <= available_cross_width {
6034            // It fits! Determine the final position and add it to the context.
6035            let final_cross_pos = match float_type {
6036                LayoutFloat::Left => available_cross_start,
6037                LayoutFloat::Right => available_cross_end - child_cross_size,
6038                LayoutFloat::None => unreachable!(),
6039            };
6040            let final_pos =
6041                LogicalPosition::from_main_cross(placement_main_offset, final_cross_pos, wm);
6042
6043            let new_float_box = FloatBox {
6044                kind: float_type,
6045                rect: LogicalRect::new(final_pos, child_margin_box_size),
6046                margin: EdgeSizes::default(), // TODO: Pass actual margin if this function is used
6047            };
6048            floating_context.floats.push(new_float_box);
6049            return Ok(final_pos);
6050        } else {
6051            // It doesn't fit. We must move the float down past an obstacle.
6052            // Find the lowest main-axis end of all floats that are blocking
6053            // the current line.
6054            let mut next_main_offset = f32::INFINITY;
6055            for existing_float in &floating_context.floats {
6056                let float_main_start = existing_float.rect.origin.main(wm);
6057                let float_main_end = float_main_start + existing_float.rect.size.main(wm);
6058
6059                // Consider only floats that are above or at the current placement line.
6060                if placement_main_offset < float_main_end {
6061                    next_main_offset = next_main_offset.min(float_main_end);
6062                }
6063            }
6064
6065            if next_main_offset.is_infinite() {
6066                // This indicates an unrecoverable state, e.g., a float wider
6067                // than the container.
6068                return Err(LayoutError::PositioningFailed);
6069            }
6070            placement_main_offset = next_main_offset;
6071        }
6072    }
6073}
6074
6075// CSS Property Getters
6076
6077/// Get the CSS `float` property for a node.
6078fn get_float_property(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutFloat {
6079    let Some(id) = dom_id else {
6080        return LayoutFloat::None;
6081    };
6082    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
6083    get_float(styled_dom, id, node_state).unwrap_or(LayoutFloat::None)
6084}
6085
6086fn get_clear_property(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> LayoutClear {
6087    let Some(id) = dom_id else {
6088        return LayoutClear::None;
6089    };
6090    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
6091    get_clear(styled_dom, id, node_state).unwrap_or(LayoutClear::None)
6092}
6093/// Helper to determine if scrollbars are needed.
6094///
6095/// # CSS Spec Reference
6096/// CSS Overflow Module Level 3 § 3: Scrollable overflow
6097pub fn check_scrollbar_necessity(
6098    content_size: LogicalSize,
6099    container_size: LogicalSize,
6100    overflow_x: OverflowBehavior,
6101    overflow_y: OverflowBehavior,
6102    scrollbar_width_px: f32,
6103) -> ScrollbarRequirements {
6104    // Use epsilon for float comparisons to avoid showing scrollbars due to 
6105    // floating-point rounding errors. Without this, content that exactly fits
6106    // may show scrollbars due to sub-pixel differences (e.g., 299.9999 vs 300.0).
6107    const EPSILON: f32 = 1.0;
6108
6109    // Determine if scrolling is needed based on overflow properties.
6110    // Note: scrollbar_width_px can be 0 for overlay scrollbars (e.g. macOS),
6111    // but we still need to register scroll nodes so that scrolling works —
6112    // overlay scrollbars just don't reserve any layout space.
6113    let mut needs_horizontal = match overflow_x {
6114        OverflowBehavior::Visible | OverflowBehavior::Hidden | OverflowBehavior::Clip => false,
6115        OverflowBehavior::Scroll => true,
6116        OverflowBehavior::Auto => content_size.width > container_size.width + EPSILON,
6117    };
6118
6119    let mut needs_vertical = match overflow_y {
6120        OverflowBehavior::Visible | OverflowBehavior::Hidden | OverflowBehavior::Clip => false,
6121        OverflowBehavior::Scroll => true,
6122        OverflowBehavior::Auto => content_size.height > container_size.height + EPSILON,
6123    };
6124
6125    // A classic layout problem: a vertical scrollbar can reduce horizontal space,
6126    // causing a horizontal scrollbar to appear, which can reduce vertical space...
6127    // A full solution involves a loop, but this two-pass check handles most cases.
6128    // Only relevant when scrollbars reserve layout space (non-overlay).
6129    if scrollbar_width_px > 0.0 {
6130        if needs_vertical && !needs_horizontal && overflow_x == OverflowBehavior::Auto {
6131            if content_size.width > (container_size.width - scrollbar_width_px) + EPSILON {
6132                needs_horizontal = true;
6133            }
6134        }
6135        if needs_horizontal && !needs_vertical && overflow_y == OverflowBehavior::Auto {
6136            if content_size.height > (container_size.height - scrollbar_width_px) + EPSILON {
6137                needs_vertical = true;
6138            }
6139        }
6140    }
6141
6142    ScrollbarRequirements {
6143        needs_horizontal,
6144        needs_vertical,
6145        scrollbar_width: if needs_vertical {
6146            scrollbar_width_px
6147        } else {
6148            0.0
6149        },
6150        scrollbar_height: if needs_horizontal {
6151            scrollbar_width_px
6152        } else {
6153            0.0
6154        },
6155    }
6156}
6157
6158/// Calculates a single collapsed margin from two adjoining vertical margins.
6159///
6160/// Implements the rules from CSS 2.1 section 8.3.1:
6161/// - If both margins are positive, the result is the larger of the two.
6162/// - If both margins are negative, the result is the more negative of the two.
6163/// - If the margins have mixed signs, they are effectively summed.
6164pub(crate) fn collapse_margins(a: f32, b: f32) -> f32 {
6165    if a.is_sign_positive() && b.is_sign_positive() {
6166        a.max(b)
6167    } else if a.is_sign_negative() && b.is_sign_negative() {
6168        a.min(b)
6169    } else {
6170        a + b
6171    }
6172}
6173
6174/// Helper function to advance the pen position with margin collapsing.
6175///
6176/// This implements CSS 2.1 margin collapsing for adjacent block-level boxes in a BFC.
6177///
6178/// - `pen` - Current main-axis position (will be modified)
6179/// - `last_margin_bottom` - The bottom margin of the previous in-flow element
6180/// - `current_margin_top` - The top margin of the current element
6181///
6182/// # Returns
6183///
6184/// The new `last_margin_bottom` value (the bottom margin of the current element)
6185///
6186/// # CSS Spec Compliance
6187///
6188/// Per CSS 2.1 Section 8.3.1 "Collapsing margins":
6189///
6190/// - Adjacent vertical margins of block boxes collapse
6191/// - The resulting margin width is the maximum of the adjoining margins (if both positive)
6192/// - Or the sum of the most positive and most negative (if signs differ)
6193fn advance_pen_with_margin_collapse(
6194    pen: &mut f32,
6195    last_margin_bottom: f32,
6196    current_margin_top: f32,
6197) -> f32 {
6198    // Collapse the previous element's bottom margin with current element's top margin
6199    let collapsed_margin = collapse_margins(last_margin_bottom, current_margin_top);
6200
6201    // Advance pen by the collapsed margin
6202    *pen += collapsed_margin;
6203
6204    // Return collapsed_margin so caller knows how much space was actually added
6205    collapsed_margin
6206}
6207
6208/// Checks if an element's border or padding prevents margin collapsing.
6209///
6210/// Per CSS 2.1 Section 8.3.1:
6211///
6212/// - Border between margins prevents collapsing
6213/// - Padding between margins prevents collapsing
6214///
6215/// # Arguments
6216///
6217/// - `box_props` - The box properties containing border and padding
6218/// - `writing_mode` - The writing mode to determine main axis
6219/// - `check_start` - If true, check main-start (top); if false, check main-end (bottom)
6220///
6221/// # Returns
6222///
6223/// `true` if border or padding exists and prevents collapsing
6224fn has_margin_collapse_blocker(
6225    box_props: &crate::solver3::geometry::BoxProps,
6226    writing_mode: LayoutWritingMode,
6227    check_start: bool, // true = check top/start, false = check bottom/end
6228) -> bool {
6229    if check_start {
6230        // Check if there's border-top or padding-top
6231        let border_start = box_props.border.main_start(writing_mode);
6232        let padding_start = box_props.padding.main_start(writing_mode);
6233        border_start > 0.0 || padding_start > 0.0
6234    } else {
6235        // Check if there's border-bottom or padding-bottom
6236        let border_end = box_props.border.main_end(writing_mode);
6237        let padding_end = box_props.padding.main_end(writing_mode);
6238        border_end > 0.0 || padding_end > 0.0
6239    }
6240}
6241
6242/// Checks if an element is empty (has no content).
6243///
6244/// Per CSS 2.1 Section 8.3.1:
6245///
6246/// > If a block element has no border, padding, inline content, height, or min-height,
6247/// > then its top and bottom margins collapse with each other.
6248///
6249/// # Arguments
6250///
6251/// - `node` - The layout node to check
6252///
6253/// # Returns
6254///
6255/// `true` if the element is empty and its margins can collapse internally
6256fn is_empty_block(node: &LayoutNode) -> bool {
6257    // Per CSS 2.2 § 8.3.1: An empty block is one that:
6258    // - Has zero computed 'min-height'
6259    // - Has zero or 'auto' computed 'height'
6260    // - Has no in-flow children
6261    // - Has no line boxes (no text/inline content)
6262
6263    // Check if node has children
6264    if !node.children.is_empty() {
6265        return false;
6266    }
6267
6268    // Check if node has inline content (text)
6269    if node.inline_layout_result.is_some() {
6270        return false;
6271    }
6272
6273    // Check if node has explicit height > 0
6274    // CSS 2.2 § 8.3.1: Elements with explicit height are NOT empty
6275    if let Some(size) = node.used_size {
6276        if size.height > 0.0 {
6277            return false;
6278        }
6279    }
6280
6281    // Empty block: no children, no inline content, no height
6282    true
6283}
6284
6285/// Generates marker text for a list item marker.
6286///
6287/// This function looks up the counter value from the cache and formats it
6288/// according to the list-style-type property.
6289///
6290/// Per CSS Lists Module Level 3, the ::marker pseudo-element is the first child
6291/// of the list-item, and references the same DOM node. Counter resolution happens
6292/// on the list-item (parent) node.
6293fn generate_list_marker_text(
6294    tree: &LayoutTree,
6295    styled_dom: &StyledDom,
6296    marker_index: usize,
6297    counters: &BTreeMap<(usize, String), i32>,
6298    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6299) -> String {
6300    use crate::solver3::counters::format_counter;
6301
6302    // Get the marker node
6303    let marker_node = match tree.get(marker_index) {
6304        Some(n) => n,
6305        None => return String::new(),
6306    };
6307
6308    // Verify this is actually a ::marker pseudo-element
6309    // Per spec, markers must be pseudo-elements, not anonymous boxes
6310    if marker_node.pseudo_element != Some(PseudoElement::Marker) {
6311        if let Some(msgs) = debug_messages {
6312            msgs.push(LayoutDebugMessage::warning(format!(
6313                "[generate_list_marker_text] WARNING: Node {} is not a ::marker pseudo-element \
6314                 (pseudo={:?}, anonymous_type={:?})",
6315                marker_index, marker_node.pseudo_element, marker_node.anonymous_type
6316            )));
6317        }
6318        // Fallback for old-style anonymous markers during transition
6319        if marker_node.anonymous_type != Some(AnonymousBoxType::ListItemMarker) {
6320            return String::new();
6321        }
6322    }
6323
6324    // Get the parent list-item node (::marker is first child of list-item)
6325    let list_item_index = match marker_node.parent {
6326        Some(p) => p,
6327        None => {
6328            if let Some(msgs) = debug_messages {
6329                msgs.push(LayoutDebugMessage::error(
6330                    "[generate_list_marker_text] ERROR: Marker has no parent".to_string(),
6331                ));
6332            }
6333            return String::new();
6334        }
6335    };
6336
6337    let list_item_node = match tree.get(list_item_index) {
6338        Some(n) => n,
6339        None => return String::new(),
6340    };
6341
6342    let list_item_dom_id = match list_item_node.dom_node_id {
6343        Some(id) => id,
6344        None => {
6345            if let Some(msgs) = debug_messages {
6346                msgs.push(LayoutDebugMessage::error(
6347                    "[generate_list_marker_text] ERROR: List-item has no DOM ID".to_string(),
6348                ));
6349            }
6350            return String::new();
6351        }
6352    };
6353
6354    if let Some(msgs) = debug_messages {
6355        msgs.push(LayoutDebugMessage::info(format!(
6356            "[generate_list_marker_text] marker_index={}, list_item_index={}, \
6357             list_item_dom_id={:?}",
6358            marker_index, list_item_index, list_item_dom_id
6359        )));
6360    }
6361
6362    // Get list-style-type from the list-item or its container
6363    let list_container_dom_id = if let Some(grandparent_index) = list_item_node.parent {
6364        if let Some(grandparent) = tree.get(grandparent_index) {
6365            grandparent.dom_node_id
6366        } else {
6367            None
6368        }
6369    } else {
6370        None
6371    };
6372
6373    // Try to get list-style-type from the list container first,
6374    // then fall back to the list-item
6375    let list_style_type = if let Some(container_id) = list_container_dom_id {
6376        let container_type = get_list_style_type(styled_dom, Some(container_id));
6377        if container_type != StyleListStyleType::default() {
6378            container_type
6379        } else {
6380            get_list_style_type(styled_dom, Some(list_item_dom_id))
6381        }
6382    } else {
6383        get_list_style_type(styled_dom, Some(list_item_dom_id))
6384    };
6385
6386    // Get the counter value for "list-item" counter from the LIST-ITEM node
6387    // Per CSS spec, counters are scoped to elements, and the list-item counter
6388    // is incremented at the list-item element, not the marker pseudo-element
6389    let counter_value = counters
6390        .get(&(list_item_index, "list-item".to_string()))
6391        .copied()
6392        .unwrap_or_else(|| {
6393            if let Some(msgs) = debug_messages {
6394                msgs.push(LayoutDebugMessage::warning(format!(
6395                    "[generate_list_marker_text] WARNING: No counter found for list-item at index \
6396                     {}, defaulting to 1",
6397                    list_item_index
6398                )));
6399            }
6400            1
6401        });
6402
6403    if let Some(msgs) = debug_messages {
6404        msgs.push(LayoutDebugMessage::info(format!(
6405            "[generate_list_marker_text] counter_value={} for list_item_index={}",
6406            counter_value, list_item_index
6407        )));
6408    }
6409
6410    // Format the counter according to the list-style-type
6411    let marker_text = format_counter(counter_value, list_style_type);
6412
6413    // For ordered lists (non-symbolic markers), add a period and space
6414    // For unordered lists (symbolic markers like •, ◦, ▪), just add a space
6415    if matches!(
6416        list_style_type,
6417        StyleListStyleType::Decimal
6418            | StyleListStyleType::DecimalLeadingZero
6419            | StyleListStyleType::LowerAlpha
6420            | StyleListStyleType::UpperAlpha
6421            | StyleListStyleType::LowerRoman
6422            | StyleListStyleType::UpperRoman
6423            | StyleListStyleType::LowerGreek
6424            | StyleListStyleType::UpperGreek
6425    ) {
6426        format!("{}. ", marker_text)
6427    } else {
6428        format!("{} ", marker_text)
6429    }
6430}
6431
6432/// Generates marker text segments for a list item marker.
6433///
6434/// Simply returns a single StyledRun with the marker text using the base_style.
6435/// The font stack in base_style already includes fallbacks with 100% Unicode coverage,
6436/// so font resolution happens during text shaping, not here.
6437fn generate_list_marker_segments(
6438    tree: &LayoutTree,
6439    styled_dom: &StyledDom,
6440    marker_index: usize,
6441    counters: &BTreeMap<(usize, String), i32>,
6442    base_style: Arc<StyleProperties>,
6443    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
6444) -> Vec<StyledRun> {
6445    // Generate the marker text
6446    let marker_text =
6447        generate_list_marker_text(tree, styled_dom, marker_index, counters, debug_messages);
6448    if marker_text.is_empty() {
6449        return Vec::new();
6450    }
6451
6452    if let Some(msgs) = debug_messages {
6453        let font_families: Vec<&str> = match &base_style.font_stack {
6454            crate::text3::cache::FontStack::Stack(selectors) => {
6455                selectors.iter().map(|f| f.family.as_str()).collect()
6456            }
6457            crate::text3::cache::FontStack::Ref(_) => vec!["<embedded-font>"],
6458        };
6459        msgs.push(LayoutDebugMessage::info(format!(
6460            "[generate_list_marker_segments] Marker text: '{}' with font stack: {:?}",
6461            marker_text,
6462            font_families
6463        )));
6464    }
6465
6466    // Return single segment - font fallback happens during shaping
6467    // List markers are generated content, not from DOM nodes
6468    vec![StyledRun {
6469        text: marker_text,
6470        style: base_style,
6471        logical_start_byte: 0,
6472        source_node_id: None,
6473    }]
6474}
6475
6476/// Splits text content into InlineContent items based on white-space CSS property.
6477///
6478/// For `white-space: pre`, `pre-wrap`, and `pre-line`, newlines (`\n`) are treated as
6479/// forced line breaks per CSS Text Level 3 specification:
6480/// https://www.w3.org/TR/css-text-3/#white-space-property
6481///
6482/// This function:
6483/// 1. Checks the white-space property of the node (or its parent for text nodes)
6484/// 2. If `pre`, `pre-wrap`, or `pre-line`: splits text by `\n` and inserts `InlineContent::LineBreak`
6485/// 3. Otherwise: returns the text as a single `InlineContent::Text`
6486///
6487/// Returns a Vec of InlineContent items that correctly represent line breaks.
6488pub(crate) fn split_text_for_whitespace(
6489    styled_dom: &StyledDom,
6490    dom_id: NodeId,
6491    text: &str,
6492    style: Arc<StyleProperties>,
6493) -> Vec<InlineContent> {
6494    use crate::text3::cache::{BreakType, ClearType, InlineBreak};
6495    
6496    // Get the white-space property - TEXT NODES inherit from parent!
6497    // We need to check the parent element's white-space, not the text node itself
6498    let node_hierarchy = styled_dom.node_hierarchy.as_container();
6499    let parent_id = node_hierarchy[dom_id].parent_id();
6500    
6501    // Try parent first, then fall back to the node itself
6502    let white_space = if let Some(parent) = parent_id {
6503        let styled_nodes = styled_dom.styled_nodes.as_container();
6504        let parent_state = styled_nodes
6505            .get(parent)
6506            .map(|n| n.styled_node_state.clone())
6507            .unwrap_or_default();
6508        
6509        match get_white_space_property(styled_dom, parent, &parent_state) {
6510            MultiValue::Exact(ws) => ws,
6511            _ => StyleWhiteSpace::Normal,
6512        }
6513    } else {
6514        StyleWhiteSpace::Normal
6515    };
6516    
6517    let mut result = Vec::new();
6518    
6519    // For `pre`, `pre-wrap`, `pre-line`, and `break-spaces`, newlines must be preserved as forced breaks
6520    // CSS Text Level 3: "Newlines in the source will be honored as forced line breaks."
6521    match white_space {
6522        StyleWhiteSpace::Pre | StyleWhiteSpace::PreWrap | StyleWhiteSpace::BreakSpaces => {
6523            // Pre, pre-wrap, break-spaces: preserve whitespace and honor newlines
6524            // Split by newlines and insert LineBreak between parts
6525            // Also handle tab characters (\t) by inserting InlineContent::Tab
6526            let mut lines = text.split('\n').peekable();
6527            let mut content_index = 0;
6528            
6529            while let Some(line) = lines.next() {
6530                // Split the line by tab characters and insert Tab elements
6531                let mut tab_parts = line.split('\t').peekable();
6532                while let Some(part) = tab_parts.next() {
6533                    // Add the text part if not empty
6534                    if !part.is_empty() {
6535                        result.push(InlineContent::Text(StyledRun {
6536                            text: part.to_string(),
6537                            style: Arc::clone(&style),
6538                            logical_start_byte: 0,
6539                            source_node_id: Some(dom_id),
6540                        }));
6541                    }
6542                    
6543                    // If there's more content after this part, insert a Tab
6544                    if tab_parts.peek().is_some() {
6545                        result.push(InlineContent::Tab { style: Arc::clone(&style) });
6546                    }
6547                }
6548                
6549                // If there's more content, insert a forced line break
6550                if lines.peek().is_some() {
6551                    result.push(InlineContent::LineBreak(InlineBreak {
6552                        break_type: BreakType::Hard,
6553                        clear: ClearType::None,
6554                        content_index,
6555                    }));
6556                    content_index += 1;
6557                }
6558            }
6559        }
6560        StyleWhiteSpace::PreLine => {
6561            // Pre-line: collapse whitespace but honor newlines
6562            let mut lines = text.split('\n').peekable();
6563            let mut content_index = 0;
6564            
6565            while let Some(line) = lines.next() {
6566                // Collapse whitespace within the line
6567                let collapsed: String = line.split_whitespace().collect::<Vec<_>>().join(" ");
6568                
6569                if !collapsed.is_empty() {
6570                    result.push(InlineContent::Text(StyledRun {
6571                        text: collapsed,
6572                        style: Arc::clone(&style),
6573                        logical_start_byte: 0,
6574                        source_node_id: Some(dom_id),
6575                    }));
6576                }
6577                
6578                // If there's more content, insert a forced line break
6579                if lines.peek().is_some() {
6580                    result.push(InlineContent::LineBreak(InlineBreak {
6581                        break_type: BreakType::Hard,
6582                        clear: ClearType::None,
6583                        content_index,
6584                    }));
6585                    content_index += 1;
6586                }
6587            }
6588        }
6589        StyleWhiteSpace::Normal | StyleWhiteSpace::Nowrap => {
6590            // CSS Text Level 3, Section 4.1.1 - Phase I: Collapsing and Transformation
6591            // https://www.w3.org/TR/css-text-3/#white-space-phase-1
6592            //
6593            // For `white-space: normal` and `nowrap`:
6594            // 1. All newlines are converted to spaces
6595            // 2. Any sequence of consecutive spaces/tabs is collapsed to a single space
6596            // 3. Leading/trailing spaces at line boundaries are handled during line layout
6597            //
6598            // Note: We perform basic collapsing here. Full inter-element collapsing 
6599            // (removing spaces at start/end of lines) happens during line breaking.
6600            
6601            // Step 1: Replace all whitespace (including newlines, tabs) with spaces
6602            // Step 2: Collapse consecutive spaces to a single space
6603            let collapsed: String = text
6604                .chars()
6605                .map(|c| if c.is_whitespace() { ' ' } else { c })
6606                .collect::<String>()
6607                .split(' ')
6608                .filter(|s| !s.is_empty())
6609                .collect::<Vec<_>>()
6610                .join(" ");
6611            
6612            // Preserve a single space if the original text was whitespace-only
6613            // This is important for inter-element spacing: "<span>Hello</span> <span>World</span>"
6614            // The space between the spans should be preserved as a single space
6615            let final_text = if collapsed.is_empty() && !text.is_empty() {
6616                // Original had whitespace but collapsed to empty - preserve one space
6617                // This handles cases like "<span> </span>" which should render as " "
6618                " ".to_string()
6619            } else if !collapsed.is_empty() {
6620                // Check if original had leading/trailing whitespace and preserve them
6621                let had_leading = text.chars().next().map(|c| c.is_whitespace()).unwrap_or(false);
6622                let had_trailing = text.chars().last().map(|c| c.is_whitespace()).unwrap_or(false);
6623                
6624                let mut result = String::new();
6625                if had_leading { result.push(' '); }
6626                result.push_str(&collapsed);
6627                if had_trailing && !had_leading { result.push(' '); }
6628                else if had_trailing && had_leading && collapsed.is_empty() { /* already have one space */ }
6629                else if had_trailing { result.push(' '); }
6630                result
6631            } else {
6632                collapsed
6633            };
6634            
6635            if !final_text.is_empty() {
6636                result.push(InlineContent::Text(StyledRun {
6637                    text: final_text,
6638                    style,
6639                    logical_start_byte: 0,
6640                    source_node_id: Some(dom_id),
6641                }));
6642            }
6643        }
6644    }
6645    
6646    result
6647}