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