Skip to main content

azul_layout/solver3/
layout_tree.rs

1//! solver3/layout_tree.rs
2//!
3//! Layout tree generation and anonymous box handling
4use std::{
5    collections::BTreeMap,
6    hash::{Hash, Hasher},
7    sync::{
8        atomic::{AtomicU32, Ordering},
9        Arc,
10    },
11};
12
13use crate::text3::cache::UnifiedConstraints;
14
15/// Global counter for IFC IDs. Resets to 0 when layout() callback is invoked.
16static IFC_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
17
18/// Unique identifier for an Inline Formatting Context (IFC).
19///
20/// An IFC represents a region where inline content (text, inline-blocks, images)
21/// is laid out together. One IFC can contain content from multiple DOM nodes
22/// (e.g., `<p>Hello <span>world</span>!</p>` is one IFC with 3 text runs).
23///
24/// The ID is generated using a global atomic counter that resets at the start
25/// of each layout pass. This ensures:
26/// - IDs are unique within a layout pass
27/// - The same logical IFC gets the same ID across frames (for selection stability)
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
29pub struct IfcId(pub u32);
30
31impl IfcId {
32    /// Generate a new unique IFC ID.
33    pub fn unique() -> Self {
34        Self(IFC_ID_COUNTER.fetch_add(1, Ordering::Relaxed))
35    }
36
37    /// Reset the IFC ID counter. Called at the start of each layout pass.
38    pub fn reset_counter() {
39        IFC_ID_COUNTER.store(0, Ordering::Relaxed);
40    }
41}
42
43/// Tracks a layout node's membership in an Inline Formatting Context.
44///
45/// Text nodes don't store their own `inline_layout_result` - instead, they
46/// participate in their parent's IFC. This struct provides the link from
47/// a text node back to its IFC's layout data.
48///
49/// # Architecture
50///
51/// ```text
52/// DOM:  <p>Hello <span>world</span>!</p>
53///
54/// Layout Tree:
55/// ├── LayoutNode (p) - IFC root
56/// │   └── inline_layout_result: Some(UnifiedLayout)
57/// │   └── ifc_id: IfcId(5)
58/// │
59/// ├── LayoutNode (::text "Hello ")
60/// │   └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 0 })
61/// │
62/// ├── LayoutNode (span)
63/// │   └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 1 })
64/// │   └── LayoutNode (::text "world")
65/// │       └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 1 })
66/// │
67/// └── LayoutNode (::text "!")
68///     └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 2 })
69/// ```
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct IfcMembership {
72    /// The IFC ID this node's content was laid out in.
73    pub ifc_id: IfcId,
74    /// The index of the IFC root LayoutNode in the layout tree.
75    /// Used to quickly find the node with `inline_layout_result`.
76    pub ifc_root_layout_index: usize,
77    /// Which run index within the IFC corresponds to this node's text.
78    /// Maps to `ContentIndex::run_index` in the shaped items.
79    pub run_index: u32,
80}
81
82use azul_core::{
83    dom::{FormattingContext, NodeId, NodeType},
84    geom::{LogicalPosition, LogicalRect, LogicalSize},
85    styled_dom::StyledDom,
86};
87use azul_css::{
88    corety::LayoutDebugMessage,
89    css::CssPropertyValue,
90    format_rust_code::GetHash,
91    props::{
92        basic::{
93            pixel::DEFAULT_FONT_SIZE, PhysicalSize, PixelValue, PropertyContext, ResolutionContext,
94        },
95        layout::{
96            LayoutDisplay, LayoutFloat, LayoutHeight, LayoutMaxHeight, LayoutMaxWidth,
97            LayoutMinHeight, LayoutMinWidth, LayoutOverflow, LayoutPosition, LayoutWidth,
98            LayoutWritingMode,
99        },
100        property::{CssProperty, CssPropertyType},
101        style::StyleTextAlign,
102    },
103};
104use taffy::{Cache as TaffyCache, Layout, LayoutInput, LayoutOutput};
105
106#[cfg(feature = "text_layout")]
107use crate::text3;
108use crate::{
109    debug_log,
110    font::parsed::ParsedFont,
111    font_traits::{FontLoaderTrait, ParsedFontTrait, UnifiedLayout},
112    solver3::{
113        geometry::{BoxProps, IntrinsicSizes, PositionedRectangle},
114        getters::{
115            get_css_height, get_css_max_height, get_css_max_width, get_css_min_height,
116            get_css_min_width, get_css_width, get_display_property, get_float, get_overflow_x,
117            get_overflow_y, get_position, get_text_align, get_writing_mode, MultiValue,
118        },
119        scrollbar::ScrollbarRequirements,
120        LayoutContext, Result,
121    },
122    text3::cache::AvailableSpace,
123};
124
125/// Represents the invalidation state of a layout node.
126///
127/// The states are ordered by severity, allowing for easy "upgrading" of the dirty state.
128/// A node marked for `Layout` does not also need to be marked for `Paint`.
129///
130/// Because this enum derives `PartialOrd` and `Ord`, you can directly compare variants:
131///
132/// - `DirtyFlag::Layout > DirtyFlag::Paint` is `true`
133/// - `DirtyFlag::Paint >= DirtyFlag::None` is `true`
134/// - `DirtyFlag::Paint < DirtyFlag::Layout` is `true`
135#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
136pub enum DirtyFlag {
137    /// The node's layout is valid and no repaint is needed. This is the "clean" state.
138    #[default]
139    None,
140    /// The node's geometry is valid, but its appearance (e.g., color) has changed.
141    /// Requires a display list update only.
142    Paint,
143    /// The node's geometry (size or position) is invalid.
144    /// Requires a full layout pass and a display list update.
145    Layout,
146}
147
148/// A hash that represents the content and style of a node PLUS all of its descendants.
149/// If two SubtreeHashes are equal, their entire subtrees are considered identical for layout
150/// purposes.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
152pub struct SubtreeHash(pub u64);
153
154/// Per-item metrics cached from the last IFC layout.
155///
156/// These metrics enable incremental IFC relayout (Phase 2 optimization):
157/// when a single inline item changes, we can check whether its advance width
158/// changed and potentially skip full line-breaking for unaffected lines.
159///
160/// Index in `CachedInlineLayout::item_metrics` matches the item order in
161/// `UnifiedLayout::items`.
162#[derive(Debug, Clone)]
163pub struct InlineItemMetrics {
164    /// The DOM NodeId of the source node for this item (for dirty checking).
165    /// `None` for generated content (list markers, hyphens, etc.)
166    pub source_node_id: Option<NodeId>,
167    /// Advance width of this item (glyph run width, inline-block width, etc.)
168    pub advance_width: f32,
169    /// Advance height contribution from this item to its line box.
170    pub line_height_contribution: f32,
171    /// Whether this item can participate in line breaking.
172    /// `false` for items inside `white-space: nowrap` or `white-space: pre`.
173    pub can_break: bool,
174    /// Which line this item was placed on (0-indexed).
175    pub line_index: u32,
176    /// X offset within its line.
177    pub x_offset: f32,
178}
179
180/// Cached inline layout result with the constraints used to compute it.
181///
182/// This structure solves a fundamental architectural problem: inline layouts
183/// (text wrapping, inline-block positioning) depend on the available width.
184/// Different layout phases may compute the layout with different widths:
185///
186/// 1. **Min-content measurement**: width = MinContent (effectively 0)
187/// 2. **Max-content measurement**: width = MaxContent (effectively infinite)
188/// 3. **Final layout**: width = Definite(actual_column_width)
189///
190/// Without tracking which constraints were used, a cached result from phase 1
191/// would incorrectly be reused in phase 3, causing text to wrap at the wrong
192/// positions (the root cause of table cell width bugs).
193///
194/// By storing the constraints alongside the result, we can:
195/// - Invalidate the cache when constraints change
196/// - Keep multiple cached results for different constraint types if needed
197/// - Ensure the final render always uses a layout computed with correct widths
198#[derive(Debug, Clone)]
199pub struct CachedInlineLayout {
200    /// The computed inline layout
201    pub layout: Arc<UnifiedLayout>,
202    /// The available width constraint used to compute this layout.
203    /// This is the key for cache validity checking.
204    pub available_width: AvailableSpace,
205    /// Whether this layout was computed with float exclusions.
206    /// Float-aware layouts should not be overwritten by non-float layouts.
207    pub has_floats: bool,
208    /// The full constraints used to compute this layout.
209    /// Used for quick relayout after text edits without rebuilding from CSS.
210    pub constraints: Option<UnifiedConstraints>,
211    /// Per-item metrics for incremental IFC relayout (Phase 2).
212    ///
213    /// Each entry corresponds to one `PositionedItem` in `layout.items`.
214    /// These metrics enable the IFC relayout decision tree:
215    /// - Check if a dirty node's advance_width changed → skip repositioning if not
216    /// - Use `can_break` + `line_index` for the nowrap fast path
217    /// - Use `x_offset` for shifting subsequent items without full line-breaking
218    pub item_metrics: Vec<InlineItemMetrics>,
219}
220
221impl CachedInlineLayout {
222    /// Creates a new cached inline layout.
223    pub fn new(
224        layout: Arc<UnifiedLayout>,
225        available_width: AvailableSpace,
226        has_floats: bool,
227    ) -> Self {
228        let item_metrics = Self::extract_item_metrics(&layout);
229        Self {
230            layout,
231            available_width,
232            has_floats,
233            constraints: None,
234            item_metrics,
235        }
236    }
237
238    /// Creates a new cached inline layout with full constraints.
239    pub fn new_with_constraints(
240        layout: Arc<UnifiedLayout>,
241        available_width: AvailableSpace,
242        has_floats: bool,
243        constraints: UnifiedConstraints,
244    ) -> Self {
245        let item_metrics = Self::extract_item_metrics(&layout);
246        Self {
247            layout,
248            available_width,
249            has_floats,
250            constraints: Some(constraints),
251            item_metrics,
252        }
253    }
254
255    /// Extracts per-item metrics from a computed `UnifiedLayout`.
256    ///
257    /// This is called automatically by the constructors. The metrics
258    /// enable incremental IFC relayout in Phase 2c/2d by providing
259    /// cached advance widths, line assignments, and break information
260    /// for each positioned item.
261    fn extract_item_metrics(layout: &UnifiedLayout) -> Vec<InlineItemMetrics> {
262        use crate::text3::cache::{ShapedItem, get_item_vertical_metrics};
263
264        layout.items.iter().map(|positioned_item| {
265            let bounds = positioned_item.item.bounds();
266            let (ascent, descent) = get_item_vertical_metrics(&positioned_item.item);
267
268            let source_node_id = match &positioned_item.item {
269                ShapedItem::Cluster(c) => c.source_node_id,
270                // Objects (inline-blocks, images) and other generated items
271                // don't expose source_node_id directly on ShapedItem.
272                // Phase 2c will refine this via the ContentIndex mapping.
273                ShapedItem::Object { .. }
274                | ShapedItem::CombinedBlock { .. }
275                | ShapedItem::Tab { .. }
276                | ShapedItem::Break { .. } => None,
277            };
278
279            // For Phase 2a, default can_break = true for all items.
280            // Phase 2c will refine this by checking the white-space property
281            // on the IFC root's style or the item's own style context.
282            // (Note: text3::StyleProperties doesn't carry white-space;
283            //  that's resolved at the IFC/BFC boundary level.)
284            let can_break = !matches!(&positioned_item.item, ShapedItem::Break { .. });
285
286            InlineItemMetrics {
287                source_node_id,
288                advance_width: bounds.width,
289                line_height_contribution: ascent + descent,
290                can_break,
291                line_index: positioned_item.line_index as u32,
292                x_offset: positioned_item.position.x,
293            }
294        }).collect()
295    }
296
297    /// Checks if this cached layout is valid for the given constraints.
298    ///
299    /// A cached layout is valid if:
300    /// 1. The available width matches (definite widths must be equal, or both are the same
301    ///    indefinite type)
302    /// 2. OR the new request doesn't have floats but the cached one does (keep float-aware layout)
303    ///
304    /// The second condition preserves float-aware layouts, which are more "correct" than
305    /// non-float layouts and shouldn't be overwritten.
306    pub fn is_valid_for(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
307        // If we have a float-aware layout and the new request doesn't have floats,
308        // keep the float-aware layout (it's more accurate)
309        if self.has_floats && !new_has_floats {
310            // But only if the width constraint type matches
311            return self.width_constraint_matches(new_width);
312        }
313
314        // Otherwise, require exact width match
315        self.width_constraint_matches(new_width)
316    }
317
318    /// Checks if the width constraint matches.
319    fn width_constraint_matches(&self, new_width: AvailableSpace) -> bool {
320        match (self.available_width, new_width) {
321            // Definite widths must match within a small epsilon
322            (AvailableSpace::Definite(old), AvailableSpace::Definite(new)) => {
323                (old - new).abs() < 0.1
324            }
325            // MinContent matches MinContent
326            (AvailableSpace::MinContent, AvailableSpace::MinContent) => true,
327            // MaxContent matches MaxContent
328            (AvailableSpace::MaxContent, AvailableSpace::MaxContent) => true,
329            // Different constraint types don't match
330            _ => false,
331        }
332    }
333
334    /// Determines if this cached layout should be replaced by a new layout.
335    ///
336    /// Returns true if the new layout should replace this one.
337    pub fn should_replace_with(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
338        // Always replace if we gain float information
339        if new_has_floats && !self.has_floats {
340            return true;
341        }
342
343        // Replace if width constraint changed
344        !self.width_constraint_matches(new_width)
345    }
346
347    /// Returns a reference to the inner UnifiedLayout.
348    ///
349    /// This is a convenience method for code that only needs the layout data
350    /// and doesn't care about the caching metadata.
351    #[inline]
352    pub fn get_layout(&self) -> &Arc<UnifiedLayout> {
353        &self.layout
354    }
355
356    /// Returns a clone of the inner Arc<UnifiedLayout>.
357    ///
358    /// This is useful for APIs that need to return an owned reference
359    /// to the layout without exposing the caching metadata.
360    #[inline]
361    pub fn clone_layout(&self) -> Arc<UnifiedLayout> {
362        self.layout.clone()
363    }
364}
365
366/// A layout tree node representing the CSS box model
367///
368/// Note: An absolute position is a final paint-time value and shouldn't be
369/// cached on the node itself, as it can change even if the node's
370/// layout is clean (e.g., if a sibling changes size). We will calculate
371/// it in a separate map.
372#[derive(Debug, Clone)]
373pub struct LayoutNode {
374    /// Reference back to the original DOM node (None for anonymous boxes)
375    pub dom_node_id: Option<NodeId>,
376    /// Pseudo-element type (::marker, ::before, ::after) if this node is a pseudo-element
377    pub pseudo_element: Option<PseudoElement>,
378    /// Whether this is an anonymous box generated by the layout engine
379    pub is_anonymous: bool,
380    /// Type of anonymous box (if applicable)
381    pub anonymous_type: Option<AnonymousBoxType>,
382    /// Children indices in the layout tree
383    pub children: Vec<usize>,
384    /// Parent index (None for root)
385    pub parent: Option<usize>,
386    /// Dirty flags to track what needs recalculation.
387    pub dirty_flag: DirtyFlag,
388    /// Unresolved box model properties (raw CSS values).
389    /// These are resolved lazily during layout when containing block is known.
390    pub unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps,
391    /// The resolved box model properties (margin, border, padding)
392    /// in logical pixels. Cached after first resolution.
393    pub box_props: BoxProps,
394    /// Cache for Taffy layout computations for this node.
395    pub taffy_cache: TaffyCache, // NEW FIELD
396    /// A hash of this node's data (style, text content, etc.) used for
397    /// fast reconciliation.
398    pub node_data_hash: u64,
399    /// A hash of this node's data and all of its descendants. Used for
400    /// fast reconciliation.
401    pub subtree_hash: SubtreeHash,
402    /// The formatting context this node establishes or participates in.
403    pub formatting_context: FormattingContext,
404    /// Parent's formatting context (needed to determine if stretch applies)
405    pub parent_formatting_context: Option<FormattingContext>,
406    /// Cached intrinsic sizes (min-content, max-content, etc.)
407    pub intrinsic_sizes: Option<IntrinsicSizes>,
408    /// The size used during the last layout pass.
409    pub used_size: Option<LogicalSize>,
410    /// The position of this node *relative to its parent's content box*.
411    pub relative_position: Option<LogicalPosition>,
412    /// The baseline of this box, if applicable, measured from its content-box top edge.
413    pub baseline: Option<f32>,
414    /// Cached inline layout result with the constraints used to compute it.
415    ///
416    /// This field stores both the computed layout AND the constraints (available width,
417    /// float state) under which it was computed. This is essential for correctness:
418    /// - Table cells are measured multiple times with different widths
419    /// - Min-content/max-content intrinsic sizing uses special constraint values
420    /// - The final layout must use the actual available width, not a measurement width
421    ///
422    /// By tracking the constraints, we avoid the bug where a min-content measurement
423    /// (with width=0) would be incorrectly reused for final rendering.
424    pub inline_layout_result: Option<CachedInlineLayout>,
425    /// Escaped top margin (CSS 2.1 margin collapsing)
426    /// If this BFC's first child's top margin "escaped" the BFC, this contains
427    /// the collapsed margin that should be applied by the parent.
428    pub escaped_top_margin: Option<f32>,
429    /// Escaped bottom margin (CSS 2.1 margin collapsing)  
430    /// If this BFC's last child's bottom margin "escaped" the BFC, this contains
431    /// the collapsed margin that should be applied by the parent.
432    pub escaped_bottom_margin: Option<f32>,
433    /// Cached scrollbar information (calculated during layout)
434    /// Used to determine if scrollbars appeared/disappeared requiring reflow
435    pub scrollbar_info: Option<ScrollbarRequirements>,
436    /// The actual content size (children overflow size) for scrollable containers.
437    /// This is the size of all content that might need to be scrolled, which can
438    /// be larger than `used_size` when content overflows the container.
439    pub overflow_content_size: Option<LogicalSize>,
440    /// If this node is an IFC root, stores the IFC ID.
441    /// Used to identify which IFC this node's `inline_layout_result` belongs to.
442    pub ifc_id: Option<IfcId>,
443    /// If this node participates in an IFC (is inline content like text),
444    /// stores the reference back to the IFC root and the run index.
445    /// This allows text nodes to find their layout data in the parent's IFC.
446    pub ifc_membership: Option<IfcMembership>,
447    /// Pre-computed CSS properties needed during layout.
448    /// Computed once during layout tree build to avoid repeated style lookups.
449    pub computed_style: ComputedLayoutStyle,
450}
451
452/// Pre-computed CSS properties needed during layout.
453/// 
454/// This struct stores resolved CSS values that are frequently accessed during
455/// layout calculations. By computing these once during layout tree construction,
456/// we avoid O(n * m) style lookups where n = nodes and m = layout passes.
457///
458/// All values are resolved to their final form (no 'inherit', 'initial', etc.)
459#[derive(Debug, Clone, Default)]
460pub struct ComputedLayoutStyle {
461    /// CSS `display` property
462    pub display: LayoutDisplay,
463    /// CSS `position` property
464    pub position: LayoutPosition,
465    /// CSS `float` property
466    pub float: LayoutFloat,
467    /// CSS `overflow-x` property
468    pub overflow_x: LayoutOverflow,
469    /// CSS `overflow-y` property  
470    pub overflow_y: LayoutOverflow,
471    /// CSS `writing-mode` property
472    pub writing_mode: azul_css::props::layout::LayoutWritingMode,
473    /// CSS `width` property (None = auto)
474    pub width: Option<azul_css::props::layout::LayoutWidth>,
475    /// CSS `height` property (None = auto)
476    pub height: Option<azul_css::props::layout::LayoutHeight>,
477    /// CSS `min-width` property
478    pub min_width: Option<azul_css::props::layout::LayoutMinWidth>,
479    /// CSS `min-height` property
480    pub min_height: Option<azul_css::props::layout::LayoutMinHeight>,
481    /// CSS `max-width` property
482    pub max_width: Option<azul_css::props::layout::LayoutMaxWidth>,
483    /// CSS `max-height` property
484    pub max_height: Option<azul_css::props::layout::LayoutMaxHeight>,
485    /// CSS `text-align` property
486    pub text_align: azul_css::props::style::StyleTextAlign,
487}
488
489impl LayoutNode {
490    /// Re-resolve box properties with the actual containing block size.
491    ///
492    /// This should be called during layout when the containing block is known.
493    /// It updates `self.box_props` with correctly resolved values for percentage-based
494    /// margins and padding.
495    ///
496    /// # Arguments
497    /// * `containing_block` - The size of the containing block
498    /// * `viewport_size` - The viewport size for vh/vw units
499    /// * `element_font_size` - The element's computed font-size for em units
500    /// * `root_font_size` - The root element's font-size for rem units
501    pub fn resolve_box_props_with_containing_block(
502        &mut self,
503        containing_block: LogicalSize,
504        viewport_size: LogicalSize,
505        element_font_size: f32,
506        root_font_size: f32,
507    ) {
508        let params = crate::solver3::geometry::ResolutionParams {
509            containing_block,
510            viewport_size,
511            element_font_size,
512            root_font_size,
513        };
514        self.box_props = self.unresolved_box_props.resolve(&params);
515    }
516
517    /// Calculates the actual content size of this node, including all children and text.
518    /// This is used to determine if scrollbars should appear for overflow: auto.
519    pub fn get_content_size(&self) -> LogicalSize {
520        // First, check if we have overflow_content_size from layout computation
521        if let Some(content_size) = self.overflow_content_size {
522            return content_size;
523        }
524
525        // Fall back to computing from used_size and text layout
526        let mut content_size = self.used_size.unwrap_or_default();
527
528        // If this node has text layout, calculate the bounds of all text items
529        if let Some(ref cached_layout) = self.inline_layout_result {
530            let text_layout = &cached_layout.layout;
531            // Find the maximum extent of all positioned items
532            let mut max_x: f32 = 0.0;
533            let mut max_y: f32 = 0.0;
534
535            for positioned_item in &text_layout.items {
536                let item_bounds = positioned_item.item.bounds();
537                let item_right = positioned_item.position.x + item_bounds.width;
538                let item_bottom = positioned_item.position.y + item_bounds.height;
539
540                max_x = max_x.max(item_right);
541                max_y = max_y.max(item_bottom);
542            }
543
544            // Use the maximum extent as content size if it's larger
545            content_size.width = content_size.width.max(max_x);
546            content_size.height = content_size.height.max(max_y);
547        }
548
549        // TODO: Also check children positions to get max content bounds
550        // For now, this handles the most common case (text overflowing)
551
552        content_size
553    }
554}
555
556/// CSS pseudo-elements that can be generated
557#[derive(Debug, Clone, Copy, PartialEq, Eq)]
558pub enum PseudoElement {
559    /// ::marker pseudo-element for list items
560    Marker,
561    /// ::before pseudo-element
562    Before,
563    /// ::after pseudo-element
564    After,
565}
566
567/// Types of anonymous boxes that can be generated
568#[derive(Debug, Clone, Copy, PartialEq)]
569pub enum AnonymousBoxType {
570    /// Anonymous block box wrapping inline content
571    InlineWrapper,
572    /// Anonymous box for a list item marker (bullet or number)
573    /// DEPRECATED: Use PseudoElement::Marker instead
574    ListItemMarker,
575    /// Anonymous table wrapper
576    TableWrapper,
577    /// Anonymous table row group (tbody)
578    TableRowGroup,
579    /// Anonymous table row
580    TableRow,
581    /// Anonymous table cell
582    TableCell,
583}
584
585/// The complete layout tree structure
586#[derive(Debug, Clone)]
587pub struct LayoutTree {
588    /// Arena-style storage for layout nodes
589    pub nodes: Vec<LayoutNode>,
590    /// Root node index
591    pub root: usize,
592    /// Mapping from DOM node IDs to layout node indices
593    pub dom_to_layout: BTreeMap<NodeId, Vec<usize>>,
594}
595
596impl LayoutTree {
597    pub fn get(&self, index: usize) -> Option<&LayoutNode> {
598        self.nodes.get(index)
599    }
600
601    pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNode> {
602        self.nodes.get_mut(index)
603    }
604
605    pub fn root_node(&self) -> &LayoutNode {
606        &self.nodes[self.root]
607    }
608
609    /// Re-resolve box properties for a node with the actual containing block size.
610    ///
611    /// This should be called during layout when the containing block is known.
612    /// It updates `box_props` with correctly resolved values for percentage-based
613    /// margins and padding.
614    pub fn resolve_box_props(
615        &mut self,
616        node_index: usize,
617        containing_block: LogicalSize,
618        viewport_size: LogicalSize,
619        element_font_size: f32,
620        root_font_size: f32,
621    ) {
622        if let Some(node) = self.nodes.get_mut(node_index) {
623            node.resolve_box_props_with_containing_block(
624                containing_block,
625                viewport_size,
626                element_font_size,
627                root_font_size,
628            );
629        }
630    }
631
632    /// Marks a node and its ancestors as dirty with the given flag.
633    ///
634    /// The dirty state is "upgraded" if the new flag is more severe than the
635    /// existing one (e.g., upgrading from `Paint` to `Layout`). Propagation stops
636    /// if an ancestor is already marked with an equal or more severe flag.
637    pub fn mark_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
638        // A "None" flag is a no-op for marking dirty.
639        if flag == DirtyFlag::None {
640            return;
641        }
642
643        let mut current_index = Some(start_index);
644        while let Some(index) = current_index {
645            if let Some(node) = self.get_mut(index) {
646                // If the node's current flag is already as dirty or dirtier,
647                // then all ancestors are also sufficiently marked, so we can stop.
648                if node.dirty_flag >= flag {
649                    break;
650                }
651
652                // Upgrade the flag to the new, more severe state.
653                node.dirty_flag = flag;
654                current_index = node.parent;
655            } else {
656                break;
657            }
658        }
659    }
660
661    /// Marks a node and its entire subtree of descendants with the given dirty flag.
662    ///
663    /// This is used for inherited CSS properties. Each node in the subtree
664    /// will be upgraded to at least the new flag's severity.
665    pub fn mark_subtree_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
666        // A "None" flag is a no-op.
667        if flag == DirtyFlag::None {
668            return;
669        }
670
671        // Using a stack for an iterative traversal to avoid deep recursion
672        // on large subtrees.
673        let mut stack = vec![start_index];
674        while let Some(index) = stack.pop() {
675            if let Some(node) = self.get_mut(index) {
676                // Only update if the new flag is an upgrade.
677                if node.dirty_flag < flag {
678                    node.dirty_flag = flag;
679                }
680                // Add all children to be processed.
681                stack.extend_from_slice(&node.children);
682            }
683        }
684    }
685
686    /// Resets the dirty flags of all nodes in the tree to `None` after layout is complete.
687    pub fn clear_all_dirty_flags(&mut self) {
688        for node in &mut self.nodes {
689            node.dirty_flag = DirtyFlag::None;
690        }
691    }
692
693    /// Get inline layout for a node, navigating through IFC membership if needed.
694    ///
695    /// For text nodes that participate in an IFC (Inline Formatting Context),
696    /// the actual `inline_layout_result` is stored on the IFC root node (the block
697    /// container), not on the text node itself. This method handles both cases:
698    ///
699    /// 1. If the node has its own `inline_layout_result`, return it directly
700    /// 2. If the node has `ifc_membership`, navigate to the IFC root and return its layout
701    ///
702    /// This mirrors the W3C Selection model where:
703    /// - Selection.focusNode points to the TEXT node
704    /// - But the layout data is owned by the containing block
705    ///
706    /// # Arguments
707    /// * `layout_index` - The index of the layout node in the tree
708    ///
709    /// # Returns
710    /// The inline layout for the node's IFC, or `None` if no layout is available
711    pub fn get_inline_layout_for_node(&self, layout_index: usize) -> Option<&std::sync::Arc<UnifiedLayout>> {
712        let layout_node = self.nodes.get(layout_index)?;
713
714        // First, check if this node has its own inline_layout_result (it's an IFC root)
715        if let Some(cached) = &layout_node.inline_layout_result {
716            return Some(cached.get_layout());
717        }
718
719        // For text nodes, check if they have ifc_membership pointing to the IFC root
720        if let Some(ifc_membership) = &layout_node.ifc_membership {
721            let ifc_root_node = self.nodes.get(ifc_membership.ifc_root_layout_index)?;
722            if let Some(cached) = &ifc_root_node.inline_layout_result {
723                return Some(cached.get_layout());
724            }
725        }
726
727        None
728    }
729}
730
731/// Generate layout tree from styled DOM with proper anonymous box generation
732pub fn generate_layout_tree<T: ParsedFontTrait>(
733    ctx: &mut LayoutContext<'_, T>,
734) -> Result<LayoutTree> {
735    let mut builder = LayoutTreeBuilder::new(ctx.viewport_size);
736    let root_id = ctx
737        .styled_dom
738        .root
739        .into_crate_internal()
740        .unwrap_or(NodeId::ZERO);
741    let root_index =
742        builder.process_node(ctx.styled_dom, root_id, None, &mut ctx.debug_messages)?;
743    let layout_tree = builder.build(root_index);
744
745    debug_log!(
746        ctx,
747        "Generated layout tree with {} nodes (incl. anonymous)",
748        layout_tree.nodes.len()
749    );
750
751    Ok(layout_tree)
752}
753
754pub struct LayoutTreeBuilder {
755    nodes: Vec<LayoutNode>,
756    dom_to_layout: BTreeMap<NodeId, Vec<usize>>,
757    viewport_size: LogicalSize,
758}
759
760impl LayoutTreeBuilder {
761    pub fn new(viewport_size: LogicalSize) -> Self {
762        Self {
763            nodes: Vec::new(),
764            dom_to_layout: BTreeMap::new(),
765            viewport_size,
766        }
767    }
768
769    pub fn get(&self, index: usize) -> Option<&LayoutNode> {
770        self.nodes.get(index)
771    }
772
773    pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNode> {
774        self.nodes.get_mut(index)
775    }
776
777    /// Main entry point for recursively building the layout tree.
778    /// This function dispatches to specialized handlers based on the node's
779    /// `display` property to correctly generate anonymous boxes.
780    pub fn process_node(
781        &mut self,
782        styled_dom: &StyledDom,
783        dom_id: NodeId,
784        parent_idx: Option<usize>,
785        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
786    ) -> Result<usize> {
787        let node_data = &styled_dom.node_data.as_container()[dom_id];
788        let node_idx = self.create_node_from_dom(styled_dom, dom_id, parent_idx, debug_messages)?;
789        let display_type = get_display_type(styled_dom, dom_id);
790
791        // If this is a list-item, inject a ::marker pseudo-element as its first child
792        // Per CSS spec, the ::marker is generated as the first child of the list-item
793        if display_type == LayoutDisplay::ListItem {
794            self.create_marker_pseudo_element(styled_dom, dom_id, node_idx);
795        }
796
797        match display_type {
798            LayoutDisplay::Block
799            | LayoutDisplay::InlineBlock
800            | LayoutDisplay::FlowRoot
801            | LayoutDisplay::ListItem => {
802                self.process_block_children(styled_dom, dom_id, node_idx, debug_messages)?
803            }
804            LayoutDisplay::Table => {
805                self.process_table_children(styled_dom, dom_id, node_idx, debug_messages)?
806            }
807            LayoutDisplay::TableRowGroup => {
808                self.process_table_row_group_children(styled_dom, dom_id, node_idx, debug_messages)?
809            }
810            LayoutDisplay::TableRow => {
811                self.process_table_row_children(styled_dom, dom_id, node_idx, debug_messages)?
812            }
813            // Inline, TableCell, etc., have their children processed as part of their
814            // formatting context layout and don't require anonymous box generation at this stage.
815            _ => {
816                // Filter out display: none children - they don't participate in layout
817                // ALSO filter out whitespace-only text nodes for Flex/Grid/etc containers
818                // to prevent them from becoming unwanted anonymous items.
819                let children: Vec<NodeId> = dom_id
820                    .az_children(&styled_dom.node_hierarchy.as_container())
821                    .filter(|&child_id| {
822                        if get_display_type(styled_dom, child_id) == LayoutDisplay::None {
823                            return false;
824                        }
825                        // Check for whitespace-only text
826                        let node_data = &styled_dom.node_data.as_container()[child_id];
827                        if let NodeType::Text(text) = node_data.get_node_type() {
828                            // Skip if text is empty or just whitespace
829                            return !text.as_str().trim().is_empty();
830                        }
831                        true
832                    })
833                    .collect();
834
835                for child_dom_id in children {
836                    self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
837                }
838            }
839        }
840        Ok(node_idx)
841    }
842
843    /// Handles children of a block-level element, creating anonymous block
844    /// wrappers for consecutive runs of inline-level children if necessary.
845    fn process_block_children(
846        &mut self,
847        styled_dom: &StyledDom,
848        parent_dom_id: NodeId,
849        parent_idx: usize,
850        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
851    ) -> Result<()> {
852        // Filter out display: none children - they don't participate in layout
853        let children: Vec<NodeId> = parent_dom_id
854            .az_children(&styled_dom.node_hierarchy.as_container())
855            .filter(|&child_id| get_display_type(styled_dom, child_id) != LayoutDisplay::None)
856            .collect();
857
858        // Debug: log which children we found
859        if let Some(msgs) = debug_messages.as_mut() {
860            msgs.push(LayoutDebugMessage::info(format!(
861                "[process_block_children] DOM node {} has {} children: {:?}",
862                parent_dom_id.index(),
863                children.len(),
864                children.iter().map(|c| c.index()).collect::<Vec<_>>()
865            )));
866        }
867
868        let has_block_child = children.iter().any(|&id| is_block_level(styled_dom, id));
869
870        if let Some(msgs) = debug_messages.as_mut() {
871            msgs.push(LayoutDebugMessage::info(format!(
872                "[process_block_children] has_block_child={}, children display types: {:?}",
873                has_block_child,
874                children
875                    .iter()
876                    .map(|c| {
877                        let dt = get_display_type(styled_dom, *c);
878                        let is_block = is_block_level(styled_dom, *c);
879                        format!("{}:{:?}(block={})", c.index(), dt, is_block)
880                    })
881                    .collect::<Vec<_>>()
882            )));
883        }
884
885        if !has_block_child {
886            // All children are inline, no anonymous boxes needed.
887            if let Some(msgs) = debug_messages.as_mut() {
888                msgs.push(LayoutDebugMessage::info(format!(
889                    "[process_block_children] All inline, processing {} children directly",
890                    children.len()
891                )));
892            }
893            for child_id in children {
894                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
895            }
896            return Ok(());
897        }
898
899        // Mixed block and inline content requires anonymous wrappers.
900        let mut inline_run = Vec::new();
901
902        for child_id in children {
903            if is_block_level(styled_dom, child_id) {
904                // End the current inline run
905                if !inline_run.is_empty() {
906                    if let Some(msgs) = debug_messages.as_mut() {
907                        msgs.push(LayoutDebugMessage::info(format!(
908                            "[process_block_children] Creating anon wrapper for inline run: {:?}",
909                            inline_run
910                                .iter()
911                                .map(|c: &NodeId| c.index())
912                                .collect::<Vec<_>>()
913                        )));
914                    }
915                    let anon_idx = self.create_anonymous_node(
916                        parent_idx,
917                        AnonymousBoxType::InlineWrapper,
918                        FormattingContext::Block {
919                            // Anonymous wrappers are BFC roots
920                            establishes_new_context: true,
921                        },
922                    );
923                    for inline_child_id in inline_run.drain(..) {
924                        self.process_node(
925                            styled_dom,
926                            inline_child_id,
927                            Some(anon_idx),
928                            debug_messages,
929                        )?;
930                    }
931                }
932                // Process the block-level child directly
933                if let Some(msgs) = debug_messages.as_mut() {
934                    msgs.push(LayoutDebugMessage::info(format!(
935                        "[process_block_children] Processing block child DOM {}",
936                        child_id.index()
937                    )));
938                }
939                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
940            } else {
941                inline_run.push(child_id);
942            }
943        }
944        // Process any remaining inline children at the end
945        if !inline_run.is_empty() {
946            if let Some(msgs) = debug_messages.as_mut() {
947                msgs.push(LayoutDebugMessage::info(format!(
948                    "[process_block_children] Creating anon wrapper for remaining inline run: {:?}",
949                    inline_run.iter().map(|c| c.index()).collect::<Vec<_>>()
950                )));
951            }
952            let anon_idx = self.create_anonymous_node(
953                parent_idx,
954                AnonymousBoxType::InlineWrapper,
955                FormattingContext::Block {
956                    establishes_new_context: true, // Anonymous wrappers are BFC roots
957                },
958            );
959            for inline_child_id in inline_run {
960                self.process_node(styled_dom, inline_child_id, Some(anon_idx), debug_messages)?;
961            }
962        }
963
964        Ok(())
965    }
966
967    /// CSS 2.2 Section 17.2.1 - Anonymous box generation for tables:
968    /// "Generate missing child wrappers. If a child C of a table-row parent P is not a
969    /// table-cell, then generate an anonymous table-cell box around C and all consecutive
970    /// siblings of C that are not table-cells."
971    ///
972    /// Handles children of a `display: table`, inserting anonymous `table-row`
973    /// wrappers for any direct `table-cell` children.
974    ///
975    /// Per CSS 2.2 Section 17.2.1, Stage 2 & 3:
976    /// - Stage 2: Wrap consecutive table-cell children in anonymous table-rows
977    /// - Stage 1 (implemented here): Skip whitespace-only text nodes
978    fn process_table_children(
979        &mut self,
980        styled_dom: &StyledDom,
981        parent_dom_id: NodeId,
982        parent_idx: usize,
983        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
984    ) -> Result<()> {
985        let parent_display = get_display_type(styled_dom, parent_dom_id);
986        let mut row_children = Vec::new();
987
988        for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
989            // CSS 2.2 Section 17.2.1, Stage 1: Skip whitespace-only text nodes
990            // "Remove all irrelevant boxes. These are boxes that do not contain table-related
991            // boxes and do not themselves have 'display' set to a table-related value."
992            if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
993                continue;
994            }
995
996            let child_display = get_display_type(styled_dom, child_id);
997
998            // CSS 2.2 Section 17.2.1, Stage 2:
999            // "Generate missing child wrappers"
1000            if child_display == LayoutDisplay::TableCell {
1001                // Accumulate consecutive table-cell children
1002                row_children.push(child_id);
1003            } else {
1004                // CSS 2.2 Section 17.2.1, Stage 2:
1005                // If we have accumulated cells, wrap them in an anonymous table-row
1006                if !row_children.is_empty() {
1007                    let anon_row_idx = self.create_anonymous_node(
1008                        parent_idx,
1009                        AnonymousBoxType::TableRow,
1010                        FormattingContext::TableRow,
1011                    );
1012
1013                    for cell_id in row_children.drain(..) {
1014                        self.process_node(styled_dom, cell_id, Some(anon_row_idx), debug_messages)?;
1015                    }
1016                }
1017
1018                // Process non-cell child (could be row, row-group, caption, etc.)
1019                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1020            }
1021        }
1022
1023        // CSS 2.2 Section 17.2.1, Stage 2:
1024        // Flush any remaining accumulated cells
1025        if !row_children.is_empty() {
1026            let anon_row_idx = self.create_anonymous_node(
1027                parent_idx,
1028                AnonymousBoxType::TableRow,
1029                FormattingContext::TableRow,
1030            );
1031
1032            for cell_id in row_children {
1033                self.process_node(styled_dom, cell_id, Some(anon_row_idx), debug_messages)?;
1034            }
1035        }
1036
1037        Ok(())
1038    }
1039
1040    /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
1041    /// Handles children of a `display: table-row-group`, `table-header-group`,
1042    /// or `table-footer-group`, inserting anonymous `table-row` wrappers as needed.
1043    ///
1044    /// The logic is identical to process_table_children per CSS 2.2 Section 17.2.1:
1045    /// "If a child C of a table-row-group parent P is not a table-row, then generate
1046    /// an anonymous table-row box around C and all consecutive siblings of C that are
1047    /// not table-rows."
1048    fn process_table_row_group_children(
1049        &mut self,
1050        styled_dom: &StyledDom,
1051        parent_dom_id: NodeId,
1052        parent_idx: usize,
1053        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1054    ) -> Result<()> {
1055        // CSS 2.2 Section 17.2.1: Row groups need the same anonymous box generation
1056        // as tables (wrapping consecutive non-row children in anonymous rows)
1057        self.process_table_children(styled_dom, parent_dom_id, parent_idx, debug_messages)
1058    }
1059
1060    /// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 2:
1061    /// "Generate missing child wrappers. If a child C of a table-row parent P is not a
1062    /// table-cell, then generate an anonymous table-cell box around C and all consecutive
1063    /// siblings of C that are not table-cells."
1064    ///
1065    /// Handles children of a `display: table-row`, inserting anonymous `table-cell` wrappers
1066    /// for any non-cell children.
1067    fn process_table_row_children(
1068        &mut self,
1069        styled_dom: &StyledDom,
1070        parent_dom_id: NodeId,
1071        parent_idx: usize,
1072        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1073    ) -> Result<()> {
1074        let parent_display = get_display_type(styled_dom, parent_dom_id);
1075
1076        for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1077            // CSS 2.2 Section 17.2.1, Stage 1: Skip whitespace-only text nodes
1078            if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
1079                continue;
1080            }
1081
1082            let child_display = get_display_type(styled_dom, child_id);
1083
1084            // CSS 2.2 Section 17.2.1, Stage 2:
1085            // "If a child C of a table-row parent P is not a table-cell, then generate
1086            // an anonymous table-cell box around C"
1087            if child_display == LayoutDisplay::TableCell {
1088                // Normal table cell - process directly
1089                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1090            } else {
1091                // CSS 2.2 Section 17.2.1, Stage 2:
1092                // Non-cell child must be wrapped in an anonymous table-cell
1093                let anon_cell_idx = self.create_anonymous_node(
1094                    parent_idx,
1095                    AnonymousBoxType::TableCell,
1096                    FormattingContext::Block {
1097                        establishes_new_context: true,
1098                    },
1099                );
1100
1101                self.process_node(styled_dom, child_id, Some(anon_cell_idx), debug_messages)?;
1102            }
1103        }
1104
1105        Ok(())
1106    }
1107    /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
1108    /// "In this process, inline-level boxes are wrapped in anonymous boxes as needed
1109    /// to satisfy the constraints of the table model."
1110    ///
1111    /// Helper to create an anonymous node in the tree.
1112    /// Anonymous boxes don't have a corresponding DOM node and are used to enforce
1113    /// the CSS box model structure (e.g., wrapping inline content in blocks,
1114    /// or creating missing table structural elements).
1115    pub fn create_anonymous_node(
1116        &mut self,
1117        parent: usize,
1118        anon_type: AnonymousBoxType,
1119        fc: FormattingContext,
1120    ) -> usize {
1121        let index = self.nodes.len();
1122
1123        // CSS 2.2 Section 17.2.1: Anonymous boxes inherit properties from their
1124        // enclosing non-anonymous box
1125        let parent_fc = self.nodes.get(parent).map(|n| n.formatting_context.clone());
1126
1127        self.nodes.push(LayoutNode {
1128            // Anonymous boxes have no DOM correspondence
1129            dom_node_id: None,
1130            pseudo_element: None,
1131            parent: Some(parent),
1132            formatting_context: fc,
1133            parent_formatting_context: parent_fc,
1134            // Anonymous boxes inherit from parent (default = all zeros)
1135            unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
1136            box_props: BoxProps::default(),
1137            taffy_cache: TaffyCache::new(),
1138            is_anonymous: true,
1139            anonymous_type: Some(anon_type),
1140            children: Vec::new(),
1141            dirty_flag: DirtyFlag::Layout,
1142            // Anonymous boxes don't have style/data
1143            node_data_hash: 0,
1144            subtree_hash: SubtreeHash(0),
1145            intrinsic_sizes: None,
1146            used_size: None,
1147            relative_position: None,
1148            baseline: None,
1149            inline_layout_result: None,
1150            escaped_top_margin: None,
1151            escaped_bottom_margin: None,
1152            scrollbar_info: None,
1153            overflow_content_size: None,
1154            ifc_id: None,
1155            ifc_membership: None,
1156            computed_style: ComputedLayoutStyle::default(),
1157        });
1158
1159        self.nodes[parent].children.push(index);
1160        index
1161    }
1162
1163    /// Creates a ::marker pseudo-element as the first child of a list-item.
1164    ///
1165    /// Per CSS Lists Module Level 3, Section 3.1:
1166    /// "For elements with display: list-item, user agents must generate a
1167    /// ::marker pseudo-element as the first child of the principal box."
1168    ///
1169    /// The ::marker references the same DOM node as its parent list-item,
1170    /// but is marked as a pseudo-element for proper counter resolution and styling.
1171    pub fn create_marker_pseudo_element(
1172        &mut self,
1173        styled_dom: &StyledDom,
1174        list_item_dom_id: NodeId,
1175        list_item_idx: usize,
1176    ) -> usize {
1177        let index = self.nodes.len();
1178
1179        // The marker references the same DOM node as the list-item
1180        // This is important for style resolution (the marker inherits from the list-item)
1181        let parent_fc = self
1182            .nodes
1183            .get(list_item_idx)
1184            .map(|n| n.formatting_context.clone());
1185        self.nodes.push(LayoutNode {
1186            dom_node_id: Some(list_item_dom_id),
1187            pseudo_element: Some(PseudoElement::Marker),
1188            parent: Some(list_item_idx),
1189            // Markers contain inline text
1190            formatting_context: FormattingContext::Inline,
1191            parent_formatting_context: parent_fc,
1192            // Will be resolved from ::marker styles (default for now)
1193            unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
1194            box_props: BoxProps::default(),
1195            taffy_cache: TaffyCache::new(),
1196            // Pseudo-elements are not anonymous boxes
1197            is_anonymous: false,
1198            anonymous_type: None,
1199            children: Vec::new(),
1200            dirty_flag: DirtyFlag::Layout,
1201            // Pseudo-elements don't have separate style in current impl
1202            node_data_hash: 0,
1203            subtree_hash: SubtreeHash(0),
1204            intrinsic_sizes: None,
1205            used_size: None,
1206            relative_position: None,
1207            baseline: None,
1208            inline_layout_result: None,
1209            escaped_top_margin: None,
1210            escaped_bottom_margin: None,
1211            scrollbar_info: None,
1212            overflow_content_size: None,
1213            ifc_id: None,
1214            ifc_membership: None,
1215            computed_style: ComputedLayoutStyle::default(),
1216        });
1217
1218        // Insert as FIRST child (per spec)
1219        self.nodes[list_item_idx].children.insert(0, index);
1220
1221        // Register with DOM mapping for counter resolution
1222        self.dom_to_layout
1223            .entry(list_item_dom_id)
1224            .or_default()
1225            .push(index);
1226
1227        index
1228    }
1229
1230    pub fn create_node_from_dom(
1231        &mut self,
1232        styled_dom: &StyledDom,
1233        dom_id: NodeId,
1234        parent: Option<usize>,
1235        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1236    ) -> Result<usize> {
1237        let index = self.nodes.len();
1238        let parent_fc =
1239            parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context.clone()));
1240        let collected = collect_box_props(styled_dom, dom_id, debug_messages, self.viewport_size);
1241        self.nodes.push(LayoutNode {
1242            dom_node_id: Some(dom_id),
1243            pseudo_element: None,
1244            parent,
1245            formatting_context: determine_formatting_context(styled_dom, dom_id),
1246            parent_formatting_context: parent_fc,
1247            unresolved_box_props: collected.unresolved,
1248            box_props: collected.resolved,
1249            taffy_cache: TaffyCache::new(),
1250            is_anonymous: false,
1251            anonymous_type: None,
1252            children: Vec::new(),
1253            dirty_flag: DirtyFlag::Layout,
1254            node_data_hash: hash_node_data(styled_dom, dom_id),
1255            subtree_hash: SubtreeHash(0),
1256            intrinsic_sizes: None,
1257            used_size: None,
1258            relative_position: None,
1259            baseline: None,
1260            inline_layout_result: None,
1261            escaped_top_margin: None,
1262            escaped_bottom_margin: None,
1263            scrollbar_info: None,
1264            overflow_content_size: None,
1265            ifc_id: None,
1266            ifc_membership: None,
1267            computed_style: compute_layout_style(styled_dom, dom_id),
1268        });
1269        if let Some(p) = parent {
1270            self.nodes[p].children.push(index);
1271        }
1272        self.dom_to_layout.entry(dom_id).or_default().push(index);
1273        Ok(index)
1274    }
1275
1276    pub fn clone_node_from_old(&mut self, old_node: &LayoutNode, parent: Option<usize>) -> usize {
1277        let index = self.nodes.len();
1278        let mut new_node = old_node.clone();
1279        new_node.parent = parent;
1280        new_node.parent_formatting_context =
1281            parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context.clone()));
1282        new_node.children = Vec::new();
1283        new_node.dirty_flag = DirtyFlag::None;
1284        self.nodes.push(new_node);
1285        if let Some(p) = parent {
1286            self.nodes[p].children.push(index);
1287        }
1288        if let Some(dom_id) = old_node.dom_node_id {
1289            self.dom_to_layout.entry(dom_id).or_default().push(index);
1290        }
1291        index
1292    }
1293
1294    pub fn build(self, root_idx: usize) -> LayoutTree {
1295        LayoutTree {
1296            nodes: self.nodes,
1297            root: root_idx,
1298            dom_to_layout: self.dom_to_layout,
1299        }
1300    }
1301}
1302
1303pub fn is_block_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
1304    matches!(
1305        get_display_type(styled_dom, node_id),
1306        LayoutDisplay::Block
1307            | LayoutDisplay::FlowRoot
1308            | LayoutDisplay::Flex
1309            | LayoutDisplay::Grid
1310            | LayoutDisplay::Table
1311            | LayoutDisplay::TableCaption
1312            | LayoutDisplay::TableRow
1313            | LayoutDisplay::TableRowGroup
1314            | LayoutDisplay::ListItem
1315    )
1316}
1317
1318/// Checks if a node is inline-level (including text nodes).
1319/// According to CSS spec, inline-level content includes:
1320///
1321/// - Elements with display: inline, inline-block, inline-table, inline-flex, inline-grid
1322/// - Text nodes
1323/// - Generated content
1324fn is_inline_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
1325    // Text nodes are always inline-level
1326    let node_data = &styled_dom.node_data.as_container()[node_id];
1327    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
1328        return true;
1329    }
1330
1331    // Check the display property
1332    matches!(
1333        get_display_type(styled_dom, node_id),
1334        LayoutDisplay::Inline
1335            | LayoutDisplay::InlineBlock
1336            | LayoutDisplay::InlineTable
1337            | LayoutDisplay::InlineFlex
1338            | LayoutDisplay::InlineGrid
1339    )
1340}
1341
1342/// Checks if a block container has only inline-level children.
1343/// According to CSS 2.2 Section 9.4.2: "An inline formatting context is established
1344/// by a block container box that contains no block-level boxes."
1345fn has_only_inline_children(styled_dom: &StyledDom, node_id: NodeId) -> bool {
1346    let hierarchy = styled_dom.node_hierarchy.as_container();
1347    let node_hier = match hierarchy.get(node_id) {
1348        Some(n) => n,
1349        None => {
1350            return false;
1351        }
1352    };
1353
1354    // Get the first child
1355    let mut current_child = node_hier.first_child_id(node_id);
1356
1357    // If there are no children, it's not an IFC (it's empty)
1358    if current_child.is_none() {
1359        return false;
1360    }
1361
1362    // Check all children
1363    while let Some(child_id) = current_child {
1364        let is_inline = is_inline_level(styled_dom, child_id);
1365
1366        if !is_inline {
1367            // Found a block-level child
1368            return false;
1369        }
1370
1371        // Move to next sibling
1372        if let Some(child_hier) = hierarchy.get(child_id) {
1373            current_child = child_hier.next_sibling_id();
1374        } else {
1375            break;
1376        }
1377    }
1378
1379    // All children are inline-level
1380    true
1381}
1382
1383/// Pre-computes all CSS properties needed during layout for a single node.
1384/// 
1385/// This is called once per node during layout tree construction, avoiding
1386/// repeated style lookups during the actual layout pass (O(n) vs O(n²)).
1387fn compute_layout_style(styled_dom: &StyledDom, dom_id: NodeId) -> ComputedLayoutStyle {
1388    let styled_node_state = styled_dom
1389        .styled_nodes
1390        .as_container()
1391        .get(dom_id)
1392        .map(|n| n.styled_node_state.clone())
1393        .unwrap_or_default();
1394
1395    // Get display property
1396    let display = match get_display_property(styled_dom, Some(dom_id)) {
1397        MultiValue::Exact(d) => d,
1398        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => LayoutDisplay::Block,
1399    };
1400
1401    // Get position property
1402    let position = get_position(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
1403
1404    // Get float property  
1405    let float = get_float(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
1406
1407    // Get overflow properties
1408    let overflow_x = get_overflow_x(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
1409    let overflow_y = get_overflow_y(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
1410
1411    // Get writing mode
1412    let writing_mode = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
1413
1414    // Get text-align
1415    let text_align = get_text_align(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
1416
1417    // Get explicit width/height (None = auto)
1418    let width = match get_css_width(styled_dom, dom_id, &styled_node_state) {
1419        MultiValue::Exact(w) => Some(w),
1420        _ => None,
1421    };
1422    let height = match get_css_height(styled_dom, dom_id, &styled_node_state) {
1423        MultiValue::Exact(h) => Some(h),
1424        _ => None,
1425    };
1426
1427    // Get min/max constraints
1428    let min_width = match get_css_min_width(styled_dom, dom_id, &styled_node_state) {
1429        MultiValue::Exact(v) => Some(v),
1430        _ => None,
1431    };
1432    let min_height = match get_css_min_height(styled_dom, dom_id, &styled_node_state) {
1433        MultiValue::Exact(v) => Some(v),
1434        _ => None,
1435    };
1436    let max_width = match get_css_max_width(styled_dom, dom_id, &styled_node_state) {
1437        MultiValue::Exact(v) => Some(v),
1438        _ => None,
1439    };
1440    let max_height = match get_css_max_height(styled_dom, dom_id, &styled_node_state) {
1441        MultiValue::Exact(v) => Some(v),
1442        _ => None,
1443    };
1444
1445    ComputedLayoutStyle {
1446        display,
1447        position,
1448        float,
1449        overflow_x,
1450        overflow_y,
1451        writing_mode,
1452        width,
1453        height,
1454        min_width,
1455        min_height,
1456        max_width,
1457        max_height,
1458        text_align,
1459    }
1460}
1461
1462fn hash_node_data(dom: &StyledDom, node_id: NodeId) -> u64 {
1463    let mut hasher = std::hash::DefaultHasher::new();
1464    // Use node_state flags and node_type as a reasonable surrogate for now.
1465    if let Some(styled_node) = dom.node_data.as_container().get(node_id) {
1466        styled_node.get_hash().hash(&mut hasher);
1467    }
1468    hasher.finish()
1469}
1470
1471/// Helper function to get element's computed font-size
1472fn get_element_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
1473    let node_state = styled_dom
1474        .styled_nodes
1475        .as_container()
1476        .get(dom_id)
1477        .map(|n| &n.styled_node_state)
1478        .cloned()
1479        .unwrap_or_default();
1480
1481    crate::solver3::getters::get_element_font_size(styled_dom, dom_id, &node_state)
1482}
1483
1484/// Helper function to get parent's computed font-size
1485fn get_parent_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
1486    styled_dom
1487        .node_hierarchy
1488        .as_container()
1489        .get(dom_id)
1490        .and_then(|node| node.parent_id())
1491        .map(|parent_id| get_element_font_size(styled_dom, parent_id))
1492        .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE)
1493}
1494
1495/// Helper function to get root element's font-size
1496fn get_root_font_size(styled_dom: &StyledDom) -> f32 {
1497    // Root is always NodeId(0) in Azul
1498    get_element_font_size(styled_dom, NodeId::new(0))
1499}
1500
1501/// Create a ResolutionContext for a given node
1502fn create_resolution_context(
1503    styled_dom: &StyledDom,
1504    dom_id: NodeId,
1505    containing_block_size: Option<azul_css::props::basic::PhysicalSize>,
1506    viewport_size: LogicalSize,
1507) -> azul_css::props::basic::ResolutionContext {
1508    let element_font_size = get_element_font_size(styled_dom, dom_id);
1509    let parent_font_size = get_parent_font_size(styled_dom, dom_id);
1510    let root_font_size = get_root_font_size(styled_dom);
1511
1512    ResolutionContext {
1513        element_font_size,
1514        parent_font_size,
1515        root_font_size,
1516        containing_block_size: containing_block_size.unwrap_or(PhysicalSize::new(0.0, 0.0)),
1517        element_size: None, // Not yet laid out
1518        viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
1519    }
1520}
1521
1522/// Result of collecting box properties from the styled DOM.
1523struct CollectedBoxProps {
1524    unresolved: crate::solver3::geometry::UnresolvedBoxProps,
1525    resolved: BoxProps,
1526}
1527
1528/// Collects box properties from the styled DOM and returns both unresolved and resolved forms.
1529///
1530/// The unresolved form stores the raw CSS values for later re-resolution when
1531/// the containing block size is known. The resolved form is an initial resolution
1532/// using viewport_size for viewport-relative units.
1533fn collect_box_props(
1534    styled_dom: &StyledDom,
1535    dom_id: NodeId,
1536    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1537    viewport_size: LogicalSize,
1538) -> CollectedBoxProps {
1539    use crate::solver3::geometry::{UnresolvedBoxProps, UnresolvedEdge, UnresolvedMargin};
1540    use crate::solver3::getters::*;
1541
1542    let node_data = &styled_dom.node_data.as_container()[dom_id];
1543
1544    // Get styled node state
1545    let node_state = styled_dom
1546        .styled_nodes
1547        .as_container()
1548        .get(dom_id)
1549        .map(|n| &n.styled_node_state)
1550        .cloned()
1551        .unwrap_or_default();
1552
1553    // Create resolution context for this element
1554    // Note: containing_block_size is None here because we don't have it yet
1555    // This is fine for initial resolution - will be re-resolved during layout
1556    let context = create_resolution_context(styled_dom, dom_id, None, viewport_size);
1557
1558    // Read margin values from styled_dom
1559    let margin_top_mv = get_css_margin_top(styled_dom, dom_id, &node_state);
1560    let margin_right_mv = get_css_margin_right(styled_dom, dom_id, &node_state);
1561    let margin_bottom_mv = get_css_margin_bottom(styled_dom, dom_id, &node_state);
1562    let margin_left_mv = get_css_margin_left(styled_dom, dom_id, &node_state);
1563
1564    // Convert MultiValue to UnresolvedMargin
1565    let to_unresolved_margin = |mv: &MultiValue<PixelValue>| -> UnresolvedMargin {
1566        match mv {
1567            MultiValue::Auto => UnresolvedMargin::Auto,
1568            MultiValue::Exact(pv) => UnresolvedMargin::Length(*pv),
1569            _ => UnresolvedMargin::Zero,
1570        }
1571    };
1572
1573    // Build unresolved margins
1574    let unresolved_margin = UnresolvedEdge {
1575        top: to_unresolved_margin(&margin_top_mv),
1576        right: to_unresolved_margin(&margin_right_mv),
1577        bottom: to_unresolved_margin(&margin_bottom_mv),
1578        left: to_unresolved_margin(&margin_left_mv),
1579    };
1580
1581    // Read padding values
1582    let padding_top_mv = get_css_padding_top(styled_dom, dom_id, &node_state);
1583    let padding_right_mv = get_css_padding_right(styled_dom, dom_id, &node_state);
1584    let padding_bottom_mv = get_css_padding_bottom(styled_dom, dom_id, &node_state);
1585    let padding_left_mv = get_css_padding_left(styled_dom, dom_id, &node_state);
1586
1587    // Convert MultiValue to PixelValue (default to 0px)
1588    let to_pixel_value = |mv: MultiValue<PixelValue>| -> PixelValue {
1589        match mv {
1590            MultiValue::Exact(pv) => pv,
1591            _ => PixelValue::const_px(0),
1592        }
1593    };
1594
1595    // Build unresolved padding
1596    let unresolved_padding = UnresolvedEdge {
1597        top: to_pixel_value(padding_top_mv),
1598        right: to_pixel_value(padding_right_mv),
1599        bottom: to_pixel_value(padding_bottom_mv),
1600        left: to_pixel_value(padding_left_mv),
1601    };
1602
1603    // Read border values
1604    let border_top_mv = get_css_border_top_width(styled_dom, dom_id, &node_state);
1605    let border_right_mv = get_css_border_right_width(styled_dom, dom_id, &node_state);
1606    let border_bottom_mv = get_css_border_bottom_width(styled_dom, dom_id, &node_state);
1607    let border_left_mv = get_css_border_left_width(styled_dom, dom_id, &node_state);
1608
1609    // Build unresolved border
1610    let unresolved_border = UnresolvedEdge {
1611        top: to_pixel_value(border_top_mv),
1612        right: to_pixel_value(border_right_mv),
1613        bottom: to_pixel_value(border_bottom_mv),
1614        left: to_pixel_value(border_left_mv),
1615    };
1616
1617    // Build the UnresolvedBoxProps
1618    let unresolved = UnresolvedBoxProps {
1619        margin: unresolved_margin,
1620        padding: unresolved_padding,
1621        border: unresolved_border,
1622    };
1623
1624    // Create initial resolution params (with viewport as containing block for now)
1625    let params = crate::solver3::geometry::ResolutionParams {
1626        containing_block: viewport_size,
1627        viewport_size,
1628        element_font_size: context.parent_font_size,
1629        root_font_size: context.root_font_size,
1630    };
1631
1632    // Resolve to get initial box_props
1633    let resolved = unresolved.resolve(&params);
1634
1635    // Debug ALL nodes with non-zero margins or vh units
1636    if let Some(msgs) = debug_messages.as_mut() {
1637        // Check if any margin uses vh
1638        let has_vh = match &unresolved_margin.top {
1639            UnresolvedMargin::Length(pv) => pv.metric == azul_css::props::basic::SizeMetric::Vh,
1640            _ => false,
1641        };
1642        if has_vh || resolved.margin.top > 0.0 || resolved.margin.left > 0.0 {
1643            msgs.push(LayoutDebugMessage::box_props(format!(
1644                "NodeId {:?} ({:?}): unresolved_margin_top={:?}, resolved_margin_top={:.2}, viewport_size={:?}",
1645                dom_id, node_data.node_type,
1646                unresolved_margin.top,
1647                resolved.margin.top,
1648                viewport_size
1649            )));
1650        }
1651    }
1652
1653    // Debug margin_auto detection
1654    if let Some(msgs) = debug_messages.as_mut() {
1655        msgs.push(LayoutDebugMessage::box_props(format!(
1656            "NodeId {:?} ({:?}): margin_auto: left={}, right={}, top={}, bottom={} | margin_left={:?}",
1657            dom_id, node_data.node_type,
1658            resolved.margin_auto.left, resolved.margin_auto.right,
1659            resolved.margin_auto.top, resolved.margin_auto.bottom,
1660            unresolved_margin.left
1661        )));
1662    }
1663
1664    // Debug for Body nodes
1665    if matches!(node_data.node_type, azul_core::dom::NodeType::Body) {
1666        if let Some(msgs) = debug_messages.as_mut() {
1667            msgs.push(LayoutDebugMessage::box_props(format!(
1668                "Body margin resolved: top={:.2}, right={:.2}, bottom={:.2}, left={:.2}",
1669                resolved.margin.top, resolved.margin.right,
1670                resolved.margin.bottom, resolved.margin.left
1671            )));
1672        }
1673    }
1674
1675    CollectedBoxProps { unresolved, resolved }
1676}
1677
1678/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 1:
1679/// "Remove all irrelevant boxes. These are boxes that do not contain table-related boxes
1680/// and do not themselves have 'display' set to a table-related value. In this context,
1681/// 'irrelevant boxes' means anonymous inline boxes that contain only white space."
1682///
1683/// Checks if a DOM node is whitespace-only text (for table anonymous box generation).
1684/// Returns true if the node is a text node containing only whitespace characters.
1685fn is_whitespace_only_text(styled_dom: &StyledDom, node_id: NodeId) -> bool {
1686    let binding = styled_dom.node_data.as_container();
1687    let node_data = binding.get(node_id);
1688    if let Some(data) = node_data {
1689        if let NodeType::Text(text) = data.get_node_type() {
1690            // Check if the text contains only whitespace characters
1691            // Per CSS 2.2 Section 17.2.1: whitespace-only anonymous boxes are irrelevant
1692            return text.chars().all(|c| c.is_whitespace());
1693        }
1694    }
1695
1696    false
1697}
1698
1699/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 1:
1700/// Determines if a node should be skipped in table structure generation.
1701/// Whitespace-only text nodes are "irrelevant" and should not generate boxes
1702/// when they appear between table-related elements.
1703///
1704/// Returns true if the node should be skipped (i.e., it's whitespace-only text
1705/// and the parent is a table structural element).
1706fn should_skip_for_table_structure(
1707    styled_dom: &StyledDom,
1708    node_id: NodeId,
1709    parent_display: LayoutDisplay,
1710) -> bool {
1711    // CSS 2.2 Section 17.2.1: Only skip whitespace text nodes when parent is
1712    // a table structural element (table, row group, row)
1713    matches!(
1714        parent_display,
1715        LayoutDisplay::Table
1716            | LayoutDisplay::TableRowGroup
1717            | LayoutDisplay::TableHeaderGroup
1718            | LayoutDisplay::TableFooterGroup
1719            | LayoutDisplay::TableRow
1720    ) && is_whitespace_only_text(styled_dom, node_id)
1721}
1722
1723/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 3:
1724/// "Generate missing parents. For each table-cell box C in a sequence of consecutive
1725/// table-cell boxes (that are not part of a table-row), an anonymous table-row box
1726/// is generated around C and its consecutive table-cell siblings.
1727///
1728/// For each proper table child C in a sequence of consecutive proper table children
1729/// that are misparented (i.e., their parent is not a table element), an anonymous
1730/// table box is generated around C and its consecutive siblings."
1731///
1732/// This function checks if a node needs a parent wrapper and returns the appropriate
1733/// anonymous box type, or None if no wrapper is needed.
1734fn needs_table_parent_wrapper(
1735    styled_dom: &StyledDom,
1736    node_id: NodeId,
1737    parent_display: LayoutDisplay,
1738) -> Option<AnonymousBoxType> {
1739    let child_display = get_display_type(styled_dom, node_id);
1740
1741    // CSS 2.2 Section 17.2.1, Stage 3:
1742    // If we have a table-cell but parent is not a table-row, need anonymous row
1743    if child_display == LayoutDisplay::TableCell {
1744        match parent_display {
1745            LayoutDisplay::TableRow
1746            | LayoutDisplay::TableRowGroup
1747            | LayoutDisplay::TableHeaderGroup
1748            | LayoutDisplay::TableFooterGroup => {
1749                // Parent can contain cells directly or via rows - no wrapper needed
1750                None
1751            }
1752            _ => Some(AnonymousBoxType::TableRow),
1753        }
1754    }
1755    // If we have a table-row but parent is not a table/row-group, need anonymous table
1756    else if matches!(child_display, LayoutDisplay::TableRow) {
1757        match parent_display {
1758            LayoutDisplay::Table
1759            | LayoutDisplay::TableRowGroup
1760            | LayoutDisplay::TableHeaderGroup
1761            | LayoutDisplay::TableFooterGroup => {
1762                None // Parent is correct
1763            }
1764            _ => Some(AnonymousBoxType::TableWrapper),
1765        }
1766    }
1767    // If we have a row-group but parent is not a table, need anonymous table
1768    else if matches!(
1769        child_display,
1770        LayoutDisplay::TableRowGroup
1771            | LayoutDisplay::TableHeaderGroup
1772            | LayoutDisplay::TableFooterGroup
1773    ) {
1774        match parent_display {
1775            LayoutDisplay::Table => None,
1776            _ => Some(AnonymousBoxType::TableWrapper),
1777        }
1778    } else {
1779        None
1780    }
1781}
1782
1783// Determines the display type of a node based on its tag and CSS properties.
1784// Delegates to getters::get_display_property which uses the compact cache fast path.
1785pub fn get_display_type(styled_dom: &StyledDom, node_id: NodeId) -> LayoutDisplay {
1786    use crate::solver3::getters::get_display_property;
1787    get_display_property(styled_dom, Some(node_id)).unwrap_or(LayoutDisplay::Inline)
1788}
1789
1790/// **Corrected:** Checks for all conditions that create a new Block Formatting Context.
1791/// A BFC contains floats and prevents margin collapse.
1792fn establishes_new_block_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> bool {
1793    let display = get_display_type(styled_dom, node_id);
1794    if matches!(
1795        display,
1796        LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::FlowRoot
1797    ) {
1798        return true;
1799    }
1800
1801    if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
1802        // `overflow` other than `visible`
1803
1804        let overflow_x = get_overflow_x(styled_dom, node_id, &styled_node.styled_node_state);
1805        if !overflow_x.is_visible_or_clip() {
1806            return true;
1807        }
1808
1809        let overflow_y = get_overflow_y(styled_dom, node_id, &styled_node.styled_node_state);
1810        if !overflow_y.is_visible_or_clip() {
1811            return true;
1812        }
1813
1814        // `position: absolute` or `position: fixed`
1815        let position = get_position(styled_dom, node_id, &styled_node.styled_node_state);
1816
1817        if position.is_absolute_or_fixed() {
1818            return true;
1819        }
1820
1821        // `float` is not `none`
1822        let float = get_float(styled_dom, node_id, &styled_node.styled_node_state);
1823        if !float.is_none() {
1824            return true;
1825        }
1826    }
1827
1828    // The root element (<html>) also establishes a BFC.
1829    if styled_dom.root.into_crate_internal() == Some(node_id) {
1830        return true;
1831    }
1832
1833    false
1834}
1835
1836/// The logic now correctly identifies all BFC roots.
1837fn determine_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> FormattingContext {
1838    // Special case: Text nodes should be treated as inline content.
1839    // They participate in their parent's inline formatting context.
1840    let node_data = &styled_dom.node_data.as_container()[node_id];
1841
1842    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
1843        // Text nodes are inline-level content within their parent's IFC
1844        return FormattingContext::Inline;
1845    }
1846
1847    let display_type = get_display_type(styled_dom, node_id);
1848
1849    match display_type {
1850        LayoutDisplay::Inline => FormattingContext::Inline,
1851
1852        // CSS 2.2 Section 9.4.2: "An inline formatting context is established by a
1853        // block container box that contains no block-level boxes."
1854        // Check if this block container has only inline-level children.
1855        LayoutDisplay::Block | LayoutDisplay::FlowRoot | LayoutDisplay::ListItem => {
1856            if has_only_inline_children(styled_dom, node_id) {
1857                // This block container should establish an IFC for its inline children
1858                FormattingContext::Inline
1859            } else {
1860                // Normal BFC
1861                FormattingContext::Block {
1862                    establishes_new_context: establishes_new_block_formatting_context(
1863                        styled_dom, node_id,
1864                    ),
1865                }
1866            }
1867        }
1868        LayoutDisplay::InlineBlock => FormattingContext::InlineBlock,
1869        LayoutDisplay::Table | LayoutDisplay::InlineTable => FormattingContext::Table,
1870        LayoutDisplay::TableRowGroup
1871        | LayoutDisplay::TableHeaderGroup
1872        | LayoutDisplay::TableFooterGroup => FormattingContext::TableRowGroup,
1873        LayoutDisplay::TableRow => FormattingContext::TableRow,
1874        LayoutDisplay::TableCell => FormattingContext::TableCell,
1875        LayoutDisplay::None => FormattingContext::None,
1876        LayoutDisplay::Flex | LayoutDisplay::InlineFlex => FormattingContext::Flex,
1877        LayoutDisplay::TableColumnGroup => FormattingContext::TableColumnGroup,
1878        LayoutDisplay::TableCaption => FormattingContext::TableCaption,
1879        LayoutDisplay::Grid | LayoutDisplay::InlineGrid => FormattingContext::Grid,
1880
1881        // These less common display types default to block behavior
1882        LayoutDisplay::TableColumn | LayoutDisplay::RunIn | LayoutDisplay::Marker => {
1883            FormattingContext::Block {
1884                establishes_new_context: true,
1885            }
1886        }
1887    }
1888}