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::{LayoutDisplay, LayoutFloat, LayoutOverflow, LayoutPosition},
96        property::{CssProperty, CssPropertyType},
97    },
98};
99use taffy::{Cache as TaffyCache, Layout, LayoutInput, LayoutOutput};
100
101#[cfg(feature = "text_layout")]
102use crate::text3;
103use crate::{
104    debug_log,
105    font::parsed::ParsedFont,
106    font_traits::{FontLoaderTrait, ParsedFontTrait, UnifiedLayout},
107    solver3::{
108        geometry::{BoxProps, IntrinsicSizes, PositionedRectangle},
109        getters::{get_float, get_overflow_x, get_overflow_y, get_position},
110        scrollbar::ScrollbarRequirements,
111        LayoutContext, Result,
112    },
113    text3::cache::AvailableSpace,
114};
115
116/// Represents the invalidation state of a layout node.
117///
118/// The states are ordered by severity, allowing for easy "upgrading" of the dirty state.
119/// A node marked for `Layout` does not also need to be marked for `Paint`.
120///
121/// Because this enum derives `PartialOrd` and `Ord`, you can directly compare variants:
122///
123/// - `DirtyFlag::Layout > DirtyFlag::Paint` is `true`
124/// - `DirtyFlag::Paint >= DirtyFlag::None` is `true`
125/// - `DirtyFlag::Paint < DirtyFlag::Layout` is `true`
126#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
127pub enum DirtyFlag {
128    /// The node's layout is valid and no repaint is needed. This is the "clean" state.
129    #[default]
130    None,
131    /// The node's geometry is valid, but its appearance (e.g., color) has changed.
132    /// Requires a display list update only.
133    Paint,
134    /// The node's geometry (size or position) is invalid.
135    /// Requires a full layout pass and a display list update.
136    Layout,
137}
138
139/// A hash that represents the content and style of a node PLUS all of its descendants.
140/// If two SubtreeHashes are equal, their entire subtrees are considered identical for layout
141/// purposes.
142#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
143pub struct SubtreeHash(pub u64);
144
145/// Cached inline layout result with the constraints used to compute it.
146///
147/// This structure solves a fundamental architectural problem: inline layouts
148/// (text wrapping, inline-block positioning) depend on the available width.
149/// Different layout phases may compute the layout with different widths:
150///
151/// 1. **Min-content measurement**: width = MinContent (effectively 0)
152/// 2. **Max-content measurement**: width = MaxContent (effectively infinite)
153/// 3. **Final layout**: width = Definite(actual_column_width)
154///
155/// Without tracking which constraints were used, a cached result from phase 1
156/// would incorrectly be reused in phase 3, causing text to wrap at the wrong
157/// positions (the root cause of table cell width bugs).
158///
159/// By storing the constraints alongside the result, we can:
160/// - Invalidate the cache when constraints change
161/// - Keep multiple cached results for different constraint types if needed
162/// - Ensure the final render always uses a layout computed with correct widths
163#[derive(Debug, Clone)]
164pub struct CachedInlineLayout {
165    /// The computed inline layout
166    pub layout: Arc<UnifiedLayout>,
167    /// The available width constraint used to compute this layout.
168    /// This is the key for cache validity checking.
169    pub available_width: AvailableSpace,
170    /// Whether this layout was computed with float exclusions.
171    /// Float-aware layouts should not be overwritten by non-float layouts.
172    pub has_floats: bool,
173    /// The full constraints used to compute this layout.
174    /// Used for quick relayout after text edits without rebuilding from CSS.
175    pub constraints: Option<UnifiedConstraints>,
176}
177
178impl CachedInlineLayout {
179    /// Creates a new cached inline layout.
180    pub fn new(
181        layout: Arc<UnifiedLayout>,
182        available_width: AvailableSpace,
183        has_floats: bool,
184    ) -> Self {
185        Self {
186            layout,
187            available_width,
188            has_floats,
189            constraints: None,
190        }
191    }
192
193    /// Creates a new cached inline layout with full constraints.
194    pub fn new_with_constraints(
195        layout: Arc<UnifiedLayout>,
196        available_width: AvailableSpace,
197        has_floats: bool,
198        constraints: UnifiedConstraints,
199    ) -> Self {
200        Self {
201            layout,
202            available_width,
203            has_floats,
204            constraints: Some(constraints),
205        }
206    }
207
208    /// Checks if this cached layout is valid for the given constraints.
209    ///
210    /// A cached layout is valid if:
211    /// 1. The available width matches (definite widths must be equal, or both are the same
212    ///    indefinite type)
213    /// 2. OR the new request doesn't have floats but the cached one does (keep float-aware layout)
214    ///
215    /// The second condition preserves float-aware layouts, which are more "correct" than
216    /// non-float layouts and shouldn't be overwritten.
217    pub fn is_valid_for(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
218        // If we have a float-aware layout and the new request doesn't have floats,
219        // keep the float-aware layout (it's more accurate)
220        if self.has_floats && !new_has_floats {
221            // But only if the width constraint type matches
222            return self.width_constraint_matches(new_width);
223        }
224
225        // Otherwise, require exact width match
226        self.width_constraint_matches(new_width)
227    }
228
229    /// Checks if the width constraint matches.
230    fn width_constraint_matches(&self, new_width: AvailableSpace) -> bool {
231        match (self.available_width, new_width) {
232            // Definite widths must match within a small epsilon
233            (AvailableSpace::Definite(old), AvailableSpace::Definite(new)) => {
234                (old - new).abs() < 0.1
235            }
236            // MinContent matches MinContent
237            (AvailableSpace::MinContent, AvailableSpace::MinContent) => true,
238            // MaxContent matches MaxContent
239            (AvailableSpace::MaxContent, AvailableSpace::MaxContent) => true,
240            // Different constraint types don't match
241            _ => false,
242        }
243    }
244
245    /// Determines if this cached layout should be replaced by a new layout.
246    ///
247    /// Returns true if the new layout should replace this one.
248    pub fn should_replace_with(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
249        // Always replace if we gain float information
250        if new_has_floats && !self.has_floats {
251            return true;
252        }
253
254        // Replace if width constraint changed
255        !self.width_constraint_matches(new_width)
256    }
257
258    /// Returns a reference to the inner UnifiedLayout.
259    ///
260    /// This is a convenience method for code that only needs the layout data
261    /// and doesn't care about the caching metadata.
262    #[inline]
263    pub fn get_layout(&self) -> &Arc<UnifiedLayout> {
264        &self.layout
265    }
266
267    /// Returns a clone of the inner Arc<UnifiedLayout>.
268    ///
269    /// This is useful for APIs that need to return an owned reference
270    /// to the layout without exposing the caching metadata.
271    #[inline]
272    pub fn clone_layout(&self) -> Arc<UnifiedLayout> {
273        self.layout.clone()
274    }
275}
276
277/// A layout tree node representing the CSS box model
278///
279/// Note: An absolute position is a final paint-time value and shouldn't be
280/// cached on the node itself, as it can change even if the node's
281/// layout is clean (e.g., if a sibling changes size). We will calculate
282/// it in a separate map.
283#[derive(Debug, Clone)]
284pub struct LayoutNode {
285    /// Reference back to the original DOM node (None for anonymous boxes)
286    pub dom_node_id: Option<NodeId>,
287    /// Pseudo-element type (::marker, ::before, ::after) if this node is a pseudo-element
288    pub pseudo_element: Option<PseudoElement>,
289    /// Whether this is an anonymous box generated by the layout engine
290    pub is_anonymous: bool,
291    /// Type of anonymous box (if applicable)
292    pub anonymous_type: Option<AnonymousBoxType>,
293    /// Children indices in the layout tree
294    pub children: Vec<usize>,
295    /// Parent index (None for root)
296    pub parent: Option<usize>,
297    /// Dirty flags to track what needs recalculation.
298    pub dirty_flag: DirtyFlag,
299    /// The resolved box model properties (margin, border, padding)
300    /// in logical pixels.
301    pub box_props: BoxProps,
302    /// Cache for Taffy layout computations for this node.
303    pub taffy_cache: TaffyCache, // NEW FIELD
304    /// A hash of this node's data (style, text content, etc.) used for
305    /// fast reconciliation.
306    pub node_data_hash: u64,
307    /// A hash of this node's data and all of its descendants. Used for
308    /// fast reconciliation.
309    pub subtree_hash: SubtreeHash,
310    /// The formatting context this node establishes or participates in.
311    pub formatting_context: FormattingContext,
312    /// Parent's formatting context (needed to determine if stretch applies)
313    pub parent_formatting_context: Option<FormattingContext>,
314    /// Cached intrinsic sizes (min-content, max-content, etc.)
315    pub intrinsic_sizes: Option<IntrinsicSizes>,
316    /// The size used during the last layout pass.
317    pub used_size: Option<LogicalSize>,
318    /// The position of this node *relative to its parent's content box*.
319    pub relative_position: Option<LogicalPosition>,
320    /// The baseline of this box, if applicable, measured from its content-box top edge.
321    pub baseline: Option<f32>,
322    /// Cached inline layout result with the constraints used to compute it.
323    ///
324    /// This field stores both the computed layout AND the constraints (available width,
325    /// float state) under which it was computed. This is essential for correctness:
326    /// - Table cells are measured multiple times with different widths
327    /// - Min-content/max-content intrinsic sizing uses special constraint values
328    /// - The final layout must use the actual available width, not a measurement width
329    ///
330    /// By tracking the constraints, we avoid the bug where a min-content measurement
331    /// (with width=0) would be incorrectly reused for final rendering.
332    pub inline_layout_result: Option<CachedInlineLayout>,
333    /// Escaped top margin (CSS 2.1 margin collapsing)
334    /// If this BFC's first child's top margin "escaped" the BFC, this contains
335    /// the collapsed margin that should be applied by the parent.
336    pub escaped_top_margin: Option<f32>,
337    /// Escaped bottom margin (CSS 2.1 margin collapsing)  
338    /// If this BFC's last child's bottom margin "escaped" the BFC, this contains
339    /// the collapsed margin that should be applied by the parent.
340    pub escaped_bottom_margin: Option<f32>,
341    /// Cached scrollbar information (calculated during layout)
342    /// Used to determine if scrollbars appeared/disappeared requiring reflow
343    pub scrollbar_info: Option<ScrollbarRequirements>,
344    /// The actual content size (children overflow size) for scrollable containers.
345    /// This is the size of all content that might need to be scrolled, which can
346    /// be larger than `used_size` when content overflows the container.
347    pub overflow_content_size: Option<LogicalSize>,
348    /// If this node is an IFC root, stores the IFC ID.
349    /// Used to identify which IFC this node's `inline_layout_result` belongs to.
350    pub ifc_id: Option<IfcId>,
351    /// If this node participates in an IFC (is inline content like text),
352    /// stores the reference back to the IFC root and the run index.
353    /// This allows text nodes to find their layout data in the parent's IFC.
354    pub ifc_membership: Option<IfcMembership>,
355}
356
357impl LayoutNode {
358    /// Calculates the actual content size of this node, including all children and text.
359    /// This is used to determine if scrollbars should appear for overflow: auto.
360    pub fn get_content_size(&self) -> LogicalSize {
361        // First, check if we have overflow_content_size from layout computation
362        if let Some(content_size) = self.overflow_content_size {
363            return content_size;
364        }
365
366        // Fall back to computing from used_size and text layout
367        let mut content_size = self.used_size.unwrap_or_default();
368
369        // If this node has text layout, calculate the bounds of all text items
370        if let Some(ref cached_layout) = self.inline_layout_result {
371            let text_layout = &cached_layout.layout;
372            // Find the maximum extent of all positioned items
373            let mut max_x: f32 = 0.0;
374            let mut max_y: f32 = 0.0;
375
376            for positioned_item in &text_layout.items {
377                let item_bounds = positioned_item.item.bounds();
378                let item_right = positioned_item.position.x + item_bounds.width;
379                let item_bottom = positioned_item.position.y + item_bounds.height;
380
381                max_x = max_x.max(item_right);
382                max_y = max_y.max(item_bottom);
383            }
384
385            // Use the maximum extent as content size if it's larger
386            content_size.width = content_size.width.max(max_x);
387            content_size.height = content_size.height.max(max_y);
388        }
389
390        // TODO: Also check children positions to get max content bounds
391        // For now, this handles the most common case (text overflowing)
392
393        content_size
394    }
395}
396
397/// CSS pseudo-elements that can be generated
398#[derive(Debug, Clone, Copy, PartialEq, Eq)]
399pub enum PseudoElement {
400    /// ::marker pseudo-element for list items
401    Marker,
402    /// ::before pseudo-element
403    Before,
404    /// ::after pseudo-element
405    After,
406}
407
408/// Types of anonymous boxes that can be generated
409#[derive(Debug, Clone, Copy, PartialEq)]
410pub enum AnonymousBoxType {
411    /// Anonymous block box wrapping inline content
412    InlineWrapper,
413    /// Anonymous box for a list item marker (bullet or number)
414    /// DEPRECATED: Use PseudoElement::Marker instead
415    ListItemMarker,
416    /// Anonymous table wrapper
417    TableWrapper,
418    /// Anonymous table row group (tbody)
419    TableRowGroup,
420    /// Anonymous table row
421    TableRow,
422    /// Anonymous table cell
423    TableCell,
424}
425
426/// The complete layout tree structure
427#[derive(Debug, Clone)]
428pub struct LayoutTree {
429    /// Arena-style storage for layout nodes
430    pub nodes: Vec<LayoutNode>,
431    /// Root node index
432    pub root: usize,
433    /// Mapping from DOM node IDs to layout node indices
434    pub dom_to_layout: BTreeMap<NodeId, Vec<usize>>,
435}
436
437impl LayoutTree {
438    pub fn get(&self, index: usize) -> Option<&LayoutNode> {
439        self.nodes.get(index)
440    }
441
442    pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNode> {
443        self.nodes.get_mut(index)
444    }
445
446    pub fn root_node(&self) -> &LayoutNode {
447        &self.nodes[self.root]
448    }
449
450    /// Marks a node and its ancestors as dirty with the given flag.
451    ///
452    /// The dirty state is "upgraded" if the new flag is more severe than the
453    /// existing one (e.g., upgrading from `Paint` to `Layout`). Propagation stops
454    /// if an ancestor is already marked with an equal or more severe flag.
455    pub fn mark_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
456        // A "None" flag is a no-op for marking dirty.
457        if flag == DirtyFlag::None {
458            return;
459        }
460
461        let mut current_index = Some(start_index);
462        while let Some(index) = current_index {
463            if let Some(node) = self.get_mut(index) {
464                // If the node's current flag is already as dirty or dirtier,
465                // then all ancestors are also sufficiently marked, so we can stop.
466                if node.dirty_flag >= flag {
467                    break;
468                }
469
470                // Upgrade the flag to the new, more severe state.
471                node.dirty_flag = flag;
472                current_index = node.parent;
473            } else {
474                break;
475            }
476        }
477    }
478
479    /// Marks a node and its entire subtree of descendants with the given dirty flag.
480    ///
481    /// This is used for inherited CSS properties. Each node in the subtree
482    /// will be upgraded to at least the new flag's severity.
483    pub fn mark_subtree_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
484        // A "None" flag is a no-op.
485        if flag == DirtyFlag::None {
486            return;
487        }
488
489        // Using a stack for an iterative traversal to avoid deep recursion
490        // on large subtrees.
491        let mut stack = vec![start_index];
492        while let Some(index) = stack.pop() {
493            if let Some(node) = self.get_mut(index) {
494                // Only update if the new flag is an upgrade.
495                if node.dirty_flag < flag {
496                    node.dirty_flag = flag;
497                }
498                // Add all children to be processed.
499                stack.extend_from_slice(&node.children);
500            }
501        }
502    }
503
504    /// Resets the dirty flags of all nodes in the tree to `None` after layout is complete.
505    pub fn clear_all_dirty_flags(&mut self) {
506        for node in &mut self.nodes {
507            node.dirty_flag = DirtyFlag::None;
508        }
509    }
510
511    /// Get inline layout for a node, navigating through IFC membership if needed.
512    ///
513    /// For text nodes that participate in an IFC (Inline Formatting Context),
514    /// the actual `inline_layout_result` is stored on the IFC root node (the block
515    /// container), not on the text node itself. This method handles both cases:
516    ///
517    /// 1. If the node has its own `inline_layout_result`, return it directly
518    /// 2. If the node has `ifc_membership`, navigate to the IFC root and return its layout
519    ///
520    /// This mirrors the W3C Selection model where:
521    /// - Selection.focusNode points to the TEXT node
522    /// - But the layout data is owned by the containing block
523    ///
524    /// # Arguments
525    /// * `layout_index` - The index of the layout node in the tree
526    ///
527    /// # Returns
528    /// The inline layout for the node's IFC, or `None` if no layout is available
529    pub fn get_inline_layout_for_node(&self, layout_index: usize) -> Option<&std::sync::Arc<UnifiedLayout>> {
530        let layout_node = self.nodes.get(layout_index)?;
531
532        // First, check if this node has its own inline_layout_result (it's an IFC root)
533        if let Some(cached) = &layout_node.inline_layout_result {
534            return Some(cached.get_layout());
535        }
536
537        // For text nodes, check if they have ifc_membership pointing to the IFC root
538        if let Some(ifc_membership) = &layout_node.ifc_membership {
539            let ifc_root_node = self.nodes.get(ifc_membership.ifc_root_layout_index)?;
540            if let Some(cached) = &ifc_root_node.inline_layout_result {
541                return Some(cached.get_layout());
542            }
543        }
544
545        None
546    }
547}
548
549/// Generate layout tree from styled DOM with proper anonymous box generation
550pub fn generate_layout_tree<T: ParsedFontTrait>(
551    ctx: &mut LayoutContext<'_, T>,
552) -> Result<LayoutTree> {
553    let mut builder = LayoutTreeBuilder::new();
554    let root_id = ctx
555        .styled_dom
556        .root
557        .into_crate_internal()
558        .unwrap_or(NodeId::ZERO);
559    let root_index =
560        builder.process_node(ctx.styled_dom, root_id, None, &mut ctx.debug_messages)?;
561    let layout_tree = builder.build(root_index);
562
563    debug_log!(
564        ctx,
565        "Generated layout tree with {} nodes (incl. anonymous)",
566        layout_tree.nodes.len()
567    );
568
569    Ok(layout_tree)
570}
571
572pub struct LayoutTreeBuilder {
573    nodes: Vec<LayoutNode>,
574    dom_to_layout: BTreeMap<NodeId, Vec<usize>>,
575}
576
577impl LayoutTreeBuilder {
578    pub fn new() -> Self {
579        Self {
580            nodes: Vec::new(),
581            dom_to_layout: BTreeMap::new(),
582        }
583    }
584
585    pub fn get(&self, index: usize) -> Option<&LayoutNode> {
586        self.nodes.get(index)
587    }
588
589    pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNode> {
590        self.nodes.get_mut(index)
591    }
592
593    /// Main entry point for recursively building the layout tree.
594    /// This function dispatches to specialized handlers based on the node's
595    /// `display` property to correctly generate anonymous boxes.
596    pub fn process_node(
597        &mut self,
598        styled_dom: &StyledDom,
599        dom_id: NodeId,
600        parent_idx: Option<usize>,
601        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
602    ) -> Result<usize> {
603        let node_data = &styled_dom.node_data.as_container()[dom_id];
604        let node_idx = self.create_node_from_dom(styled_dom, dom_id, parent_idx, debug_messages)?;
605        let display_type = get_display_type(styled_dom, dom_id);
606
607        // If this is a list-item, inject a ::marker pseudo-element as its first child
608        // Per CSS spec, the ::marker is generated as the first child of the list-item
609        if display_type == LayoutDisplay::ListItem {
610            self.create_marker_pseudo_element(styled_dom, dom_id, node_idx);
611        }
612
613        match display_type {
614            LayoutDisplay::Block
615            | LayoutDisplay::InlineBlock
616            | LayoutDisplay::FlowRoot
617            | LayoutDisplay::ListItem => {
618                self.process_block_children(styled_dom, dom_id, node_idx, debug_messages)?
619            }
620            LayoutDisplay::Table => {
621                self.process_table_children(styled_dom, dom_id, node_idx, debug_messages)?
622            }
623            LayoutDisplay::TableRowGroup => {
624                self.process_table_row_group_children(styled_dom, dom_id, node_idx, debug_messages)?
625            }
626            LayoutDisplay::TableRow => {
627                self.process_table_row_children(styled_dom, dom_id, node_idx, debug_messages)?
628            }
629            // Inline, TableCell, etc., have their children processed as part of their
630            // formatting context layout and don't require anonymous box generation at this stage.
631            _ => {
632                let children: Vec<NodeId> = dom_id
633                    .az_children(&styled_dom.node_hierarchy.as_container())
634                    .collect();
635
636                for child_dom_id in children {
637                    self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
638                }
639            }
640        }
641        Ok(node_idx)
642    }
643
644    /// Handles children of a block-level element, creating anonymous block
645    /// wrappers for consecutive runs of inline-level children if necessary.
646    fn process_block_children(
647        &mut self,
648        styled_dom: &StyledDom,
649        parent_dom_id: NodeId,
650        parent_idx: usize,
651        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
652    ) -> Result<()> {
653        let children: Vec<NodeId> = parent_dom_id
654            .az_children(&styled_dom.node_hierarchy.as_container())
655            .collect();
656
657        // Debug: log which children we found
658        if let Some(msgs) = debug_messages.as_mut() {
659            msgs.push(LayoutDebugMessage::info(format!(
660                "[process_block_children] DOM node {} has {} children: {:?}",
661                parent_dom_id.index(),
662                children.len(),
663                children.iter().map(|c| c.index()).collect::<Vec<_>>()
664            )));
665        }
666
667        let has_block_child = children.iter().any(|&id| is_block_level(styled_dom, id));
668
669        if let Some(msgs) = debug_messages.as_mut() {
670            msgs.push(LayoutDebugMessage::info(format!(
671                "[process_block_children] has_block_child={}, children display types: {:?}",
672                has_block_child,
673                children
674                    .iter()
675                    .map(|c| {
676                        let dt = get_display_type(styled_dom, *c);
677                        let is_block = is_block_level(styled_dom, *c);
678                        format!("{}:{:?}(block={})", c.index(), dt, is_block)
679                    })
680                    .collect::<Vec<_>>()
681            )));
682        }
683
684        if !has_block_child {
685            // All children are inline, no anonymous boxes needed.
686            if let Some(msgs) = debug_messages.as_mut() {
687                msgs.push(LayoutDebugMessage::info(format!(
688                    "[process_block_children] All inline, processing {} children directly",
689                    children.len()
690                )));
691            }
692            for child_id in children {
693                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
694            }
695            return Ok(());
696        }
697
698        // Mixed block and inline content requires anonymous wrappers.
699        let mut inline_run = Vec::new();
700
701        for child_id in children {
702            if is_block_level(styled_dom, child_id) {
703                // End the current inline run
704                if !inline_run.is_empty() {
705                    if let Some(msgs) = debug_messages.as_mut() {
706                        msgs.push(LayoutDebugMessage::info(format!(
707                            "[process_block_children] Creating anon wrapper for inline run: {:?}",
708                            inline_run
709                                .iter()
710                                .map(|c: &NodeId| c.index())
711                                .collect::<Vec<_>>()
712                        )));
713                    }
714                    let anon_idx = self.create_anonymous_node(
715                        parent_idx,
716                        AnonymousBoxType::InlineWrapper,
717                        FormattingContext::Block {
718                            // Anonymous wrappers are BFC roots
719                            establishes_new_context: true,
720                        },
721                    );
722                    for inline_child_id in inline_run.drain(..) {
723                        self.process_node(
724                            styled_dom,
725                            inline_child_id,
726                            Some(anon_idx),
727                            debug_messages,
728                        )?;
729                    }
730                }
731                // Process the block-level child directly
732                if let Some(msgs) = debug_messages.as_mut() {
733                    msgs.push(LayoutDebugMessage::info(format!(
734                        "[process_block_children] Processing block child DOM {}",
735                        child_id.index()
736                    )));
737                }
738                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
739            } else {
740                inline_run.push(child_id);
741            }
742        }
743        // Process any remaining inline children at the end
744        if !inline_run.is_empty() {
745            if let Some(msgs) = debug_messages.as_mut() {
746                msgs.push(LayoutDebugMessage::info(format!(
747                    "[process_block_children] Creating anon wrapper for remaining inline run: {:?}",
748                    inline_run.iter().map(|c| c.index()).collect::<Vec<_>>()
749                )));
750            }
751            let anon_idx = self.create_anonymous_node(
752                parent_idx,
753                AnonymousBoxType::InlineWrapper,
754                FormattingContext::Block {
755                    establishes_new_context: true, // Anonymous wrappers are BFC roots
756                },
757            );
758            for inline_child_id in inline_run {
759                self.process_node(styled_dom, inline_child_id, Some(anon_idx), debug_messages)?;
760            }
761        }
762
763        Ok(())
764    }
765
766    /// CSS 2.2 Section 17.2.1 - Anonymous box generation for tables:
767    /// "Generate missing child wrappers. If a child C of a table-row parent P is not a
768    /// table-cell, then generate an anonymous table-cell box around C and all consecutive
769    /// siblings of C that are not table-cells."
770    ///
771    /// Handles children of a `display: table`, inserting anonymous `table-row`
772    /// wrappers for any direct `table-cell` children.
773    ///
774    /// Per CSS 2.2 Section 17.2.1, Stage 2 & 3:
775    /// - Stage 2: Wrap consecutive table-cell children in anonymous table-rows
776    /// - Stage 1 (implemented here): Skip whitespace-only text nodes
777    fn process_table_children(
778        &mut self,
779        styled_dom: &StyledDom,
780        parent_dom_id: NodeId,
781        parent_idx: usize,
782        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
783    ) -> Result<()> {
784        let parent_display = get_display_type(styled_dom, parent_dom_id);
785        let mut row_children = Vec::new();
786
787        for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
788            // CSS 2.2 Section 17.2.1, Stage 1: Skip whitespace-only text nodes
789            // "Remove all irrelevant boxes. These are boxes that do not contain table-related
790            // boxes and do not themselves have 'display' set to a table-related value."
791            if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
792                continue;
793            }
794
795            let child_display = get_display_type(styled_dom, child_id);
796
797            // CSS 2.2 Section 17.2.1, Stage 2:
798            // "Generate missing child wrappers"
799            if child_display == LayoutDisplay::TableCell {
800                // Accumulate consecutive table-cell children
801                row_children.push(child_id);
802            } else {
803                // CSS 2.2 Section 17.2.1, Stage 2:
804                // If we have accumulated cells, wrap them in an anonymous table-row
805                if !row_children.is_empty() {
806                    let anon_row_idx = self.create_anonymous_node(
807                        parent_idx,
808                        AnonymousBoxType::TableRow,
809                        FormattingContext::TableRow,
810                    );
811
812                    for cell_id in row_children.drain(..) {
813                        self.process_node(styled_dom, cell_id, Some(anon_row_idx), debug_messages)?;
814                    }
815                }
816
817                // Process non-cell child (could be row, row-group, caption, etc.)
818                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
819            }
820        }
821
822        // CSS 2.2 Section 17.2.1, Stage 2:
823        // Flush any remaining accumulated cells
824        if !row_children.is_empty() {
825            let anon_row_idx = self.create_anonymous_node(
826                parent_idx,
827                AnonymousBoxType::TableRow,
828                FormattingContext::TableRow,
829            );
830
831            for cell_id in row_children {
832                self.process_node(styled_dom, cell_id, Some(anon_row_idx), debug_messages)?;
833            }
834        }
835
836        Ok(())
837    }
838
839    /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
840    /// Handles children of a `display: table-row-group`, `table-header-group`,
841    /// or `table-footer-group`, inserting anonymous `table-row` wrappers as needed.
842    ///
843    /// The logic is identical to process_table_children per CSS 2.2 Section 17.2.1:
844    /// "If a child C of a table-row-group parent P is not a table-row, then generate
845    /// an anonymous table-row box around C and all consecutive siblings of C that are
846    /// not table-rows."
847    fn process_table_row_group_children(
848        &mut self,
849        styled_dom: &StyledDom,
850        parent_dom_id: NodeId,
851        parent_idx: usize,
852        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
853    ) -> Result<()> {
854        // CSS 2.2 Section 17.2.1: Row groups need the same anonymous box generation
855        // as tables (wrapping consecutive non-row children in anonymous rows)
856        self.process_table_children(styled_dom, parent_dom_id, parent_idx, debug_messages)
857    }
858
859    /// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 2:
860    /// "Generate missing child wrappers. If a child C of a table-row parent P is not a
861    /// table-cell, then generate an anonymous table-cell box around C and all consecutive
862    /// siblings of C that are not table-cells."
863    ///
864    /// Handles children of a `display: table-row`, inserting anonymous `table-cell` wrappers
865    /// for any non-cell children.
866    fn process_table_row_children(
867        &mut self,
868        styled_dom: &StyledDom,
869        parent_dom_id: NodeId,
870        parent_idx: usize,
871        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
872    ) -> Result<()> {
873        let parent_display = get_display_type(styled_dom, parent_dom_id);
874
875        for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
876            // CSS 2.2 Section 17.2.1, Stage 1: Skip whitespace-only text nodes
877            if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
878                continue;
879            }
880
881            let child_display = get_display_type(styled_dom, child_id);
882
883            // CSS 2.2 Section 17.2.1, Stage 2:
884            // "If a child C of a table-row parent P is not a table-cell, then generate
885            // an anonymous table-cell box around C"
886            if child_display == LayoutDisplay::TableCell {
887                // Normal table cell - process directly
888                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
889            } else {
890                // CSS 2.2 Section 17.2.1, Stage 2:
891                // Non-cell child must be wrapped in an anonymous table-cell
892                let anon_cell_idx = self.create_anonymous_node(
893                    parent_idx,
894                    AnonymousBoxType::TableCell,
895                    FormattingContext::Block {
896                        establishes_new_context: true,
897                    },
898                );
899
900                self.process_node(styled_dom, child_id, Some(anon_cell_idx), debug_messages)?;
901            }
902        }
903
904        Ok(())
905    }
906    /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
907    /// "In this process, inline-level boxes are wrapped in anonymous boxes as needed
908    /// to satisfy the constraints of the table model."
909    ///
910    /// Helper to create an anonymous node in the tree.
911    /// Anonymous boxes don't have a corresponding DOM node and are used to enforce
912    /// the CSS box model structure (e.g., wrapping inline content in blocks,
913    /// or creating missing table structural elements).
914    pub fn create_anonymous_node(
915        &mut self,
916        parent: usize,
917        anon_type: AnonymousBoxType,
918        fc: FormattingContext,
919    ) -> usize {
920        let index = self.nodes.len();
921
922        // CSS 2.2 Section 17.2.1: Anonymous boxes inherit properties from their
923        // enclosing non-anonymous box
924        let parent_fc = self.nodes.get(parent).map(|n| n.formatting_context.clone());
925
926        self.nodes.push(LayoutNode {
927            // Anonymous boxes have no DOM correspondence
928            dom_node_id: None,
929            pseudo_element: None,
930            parent: Some(parent),
931            formatting_context: fc,
932            parent_formatting_context: parent_fc,
933            // Anonymous boxes inherit from parent
934            box_props: BoxProps::default(),
935            taffy_cache: TaffyCache::new(),
936            is_anonymous: true,
937            anonymous_type: Some(anon_type),
938            children: Vec::new(),
939            dirty_flag: DirtyFlag::Layout,
940            // Anonymous boxes don't have style/data
941            node_data_hash: 0,
942            subtree_hash: SubtreeHash(0),
943            intrinsic_sizes: None,
944            used_size: None,
945            relative_position: None,
946            baseline: None,
947            inline_layout_result: None,
948            escaped_top_margin: None,
949            escaped_bottom_margin: None,
950            scrollbar_info: None,
951            overflow_content_size: None,
952            ifc_id: None,
953            ifc_membership: None,
954        });
955
956        self.nodes[parent].children.push(index);
957        index
958    }
959
960    /// Creates a ::marker pseudo-element as the first child of a list-item.
961    ///
962    /// Per CSS Lists Module Level 3, Section 3.1:
963    /// "For elements with display: list-item, user agents must generate a
964    /// ::marker pseudo-element as the first child of the principal box."
965    ///
966    /// The ::marker references the same DOM node as its parent list-item,
967    /// but is marked as a pseudo-element for proper counter resolution and styling.
968    pub fn create_marker_pseudo_element(
969        &mut self,
970        styled_dom: &StyledDom,
971        list_item_dom_id: NodeId,
972        list_item_idx: usize,
973    ) -> usize {
974        let index = self.nodes.len();
975
976        // The marker references the same DOM node as the list-item
977        // This is important for style resolution (the marker inherits from the list-item)
978        let parent_fc = self
979            .nodes
980            .get(list_item_idx)
981            .map(|n| n.formatting_context.clone());
982        self.nodes.push(LayoutNode {
983            dom_node_id: Some(list_item_dom_id),
984            pseudo_element: Some(PseudoElement::Marker),
985            parent: Some(list_item_idx),
986            // Markers contain inline text
987            formatting_context: FormattingContext::Inline,
988            parent_formatting_context: parent_fc,
989            // Will be resolved from ::marker styles
990            box_props: BoxProps::default(),
991            taffy_cache: TaffyCache::new(),
992            // Pseudo-elements are not anonymous boxes
993            is_anonymous: false,
994            anonymous_type: None,
995            children: Vec::new(),
996            dirty_flag: DirtyFlag::Layout,
997            // Pseudo-elements don't have separate style in current impl
998            node_data_hash: 0,
999            subtree_hash: SubtreeHash(0),
1000            intrinsic_sizes: None,
1001            used_size: None,
1002            relative_position: None,
1003            baseline: None,
1004            inline_layout_result: None,
1005            escaped_top_margin: None,
1006            escaped_bottom_margin: None,
1007            scrollbar_info: None,
1008            overflow_content_size: None,
1009            ifc_id: None,
1010            ifc_membership: None,
1011        });
1012
1013        // Insert as FIRST child (per spec)
1014        self.nodes[list_item_idx].children.insert(0, index);
1015
1016        // Register with DOM mapping for counter resolution
1017        self.dom_to_layout
1018            .entry(list_item_dom_id)
1019            .or_default()
1020            .push(index);
1021
1022        index
1023    }
1024
1025    pub fn create_node_from_dom(
1026        &mut self,
1027        styled_dom: &StyledDom,
1028        dom_id: NodeId,
1029        parent: Option<usize>,
1030        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1031    ) -> Result<usize> {
1032        let index = self.nodes.len();
1033        let parent_fc =
1034            parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context.clone()));
1035        self.nodes.push(LayoutNode {
1036            dom_node_id: Some(dom_id),
1037            pseudo_element: None,
1038            parent,
1039            formatting_context: determine_formatting_context(styled_dom, dom_id),
1040            parent_formatting_context: parent_fc,
1041            box_props: resolve_box_props(styled_dom, dom_id, debug_messages),
1042            taffy_cache: TaffyCache::new(),
1043            is_anonymous: false,
1044            anonymous_type: None,
1045            children: Vec::new(),
1046            dirty_flag: DirtyFlag::Layout,
1047            node_data_hash: hash_node_data(styled_dom, dom_id),
1048            subtree_hash: SubtreeHash(0),
1049            intrinsic_sizes: None,
1050            used_size: None,
1051            relative_position: None,
1052            baseline: None,
1053            inline_layout_result: None,
1054            escaped_top_margin: None,
1055            escaped_bottom_margin: None,
1056            scrollbar_info: None,
1057            overflow_content_size: None,
1058            ifc_id: None,
1059            ifc_membership: None,
1060        });
1061        if let Some(p) = parent {
1062            self.nodes[p].children.push(index);
1063        }
1064        self.dom_to_layout.entry(dom_id).or_default().push(index);
1065        Ok(index)
1066    }
1067
1068    pub fn clone_node_from_old(&mut self, old_node: &LayoutNode, parent: Option<usize>) -> usize {
1069        let index = self.nodes.len();
1070        let mut new_node = old_node.clone();
1071        new_node.parent = parent;
1072        new_node.parent_formatting_context =
1073            parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context.clone()));
1074        new_node.children = Vec::new();
1075        new_node.dirty_flag = DirtyFlag::None;
1076        self.nodes.push(new_node);
1077        if let Some(p) = parent {
1078            self.nodes[p].children.push(index);
1079        }
1080        if let Some(dom_id) = old_node.dom_node_id {
1081            self.dom_to_layout.entry(dom_id).or_default().push(index);
1082        }
1083        index
1084    }
1085
1086    pub fn build(self, root_idx: usize) -> LayoutTree {
1087        LayoutTree {
1088            nodes: self.nodes,
1089            root: root_idx,
1090            dom_to_layout: self.dom_to_layout,
1091        }
1092    }
1093}
1094
1095pub fn is_block_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
1096    matches!(
1097        get_display_type(styled_dom, node_id),
1098        LayoutDisplay::Block
1099            | LayoutDisplay::FlowRoot
1100            | LayoutDisplay::Table
1101            | LayoutDisplay::TableRow
1102            | LayoutDisplay::TableRowGroup
1103            | LayoutDisplay::ListItem
1104    )
1105}
1106
1107/// Checks if a node is inline-level (including text nodes).
1108/// According to CSS spec, inline-level content includes:
1109///
1110/// - Elements with display: inline, inline-block, inline-table, inline-flex, inline-grid
1111/// - Text nodes
1112/// - Generated content
1113fn is_inline_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
1114    // Text nodes are always inline-level
1115    let node_data = &styled_dom.node_data.as_container()[node_id];
1116    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
1117        return true;
1118    }
1119
1120    // Check the display property
1121    matches!(
1122        get_display_type(styled_dom, node_id),
1123        LayoutDisplay::Inline
1124            | LayoutDisplay::InlineBlock
1125            | LayoutDisplay::InlineTable
1126            | LayoutDisplay::InlineFlex
1127            | LayoutDisplay::InlineGrid
1128    )
1129}
1130
1131/// Checks if a block container has only inline-level children.
1132/// According to CSS 2.2 Section 9.4.2: "An inline formatting context is established
1133/// by a block container box that contains no block-level boxes."
1134fn has_only_inline_children(styled_dom: &StyledDom, node_id: NodeId) -> bool {
1135    let hierarchy = styled_dom.node_hierarchy.as_container();
1136    let node_hier = match hierarchy.get(node_id) {
1137        Some(n) => n,
1138        None => {
1139            return false;
1140        }
1141    };
1142
1143    // Get the first child
1144    let mut current_child = node_hier.first_child_id(node_id);
1145
1146    // If there are no children, it's not an IFC (it's empty)
1147    if current_child.is_none() {
1148        return false;
1149    }
1150
1151    // Check all children
1152    while let Some(child_id) = current_child {
1153        let is_inline = is_inline_level(styled_dom, child_id);
1154
1155        if !is_inline {
1156            // Found a block-level child
1157            return false;
1158        }
1159
1160        // Move to next sibling
1161        if let Some(child_hier) = hierarchy.get(child_id) {
1162            current_child = child_hier.next_sibling_id();
1163        } else {
1164            break;
1165        }
1166    }
1167
1168    // All children are inline-level
1169    true
1170}
1171
1172fn hash_node_data(dom: &StyledDom, node_id: NodeId) -> u64 {
1173    let mut hasher = std::hash::DefaultHasher::new();
1174    // Use node_state flags and node_type as a reasonable surrogate for now.
1175    if let Some(styled_node) = dom.node_data.as_container().get(node_id) {
1176        styled_node.get_hash().hash(&mut hasher);
1177    }
1178    hasher.finish()
1179}
1180
1181/// Helper function to get element's computed font-size
1182fn get_element_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
1183    use crate::solver3::getters::*;
1184
1185    let node_data = &styled_dom.node_data.as_container()[dom_id];
1186    let node_state = styled_dom
1187        .styled_nodes
1188        .as_container()
1189        .get(dom_id)
1190        .map(|n| &n.styled_node_state)
1191        .cloned()
1192        .unwrap_or_default();
1193
1194    let cache = &styled_dom.css_property_cache.ptr;
1195
1196    // Try to get from dependency chain first (proper resolution)
1197    if let Some(node_chains) = cache.dependency_chains.get(&dom_id) {
1198        if let Some(chain) = node_chains.get(&CssPropertyType::FontSize) {
1199            if let Some(cached) = chain.cached_pixels {
1200                return cached;
1201            }
1202        }
1203    }
1204
1205    // Fallback: get from property cache
1206    cache
1207        .get_font_size(node_data, &dom_id, &node_state)
1208        .and_then(|v| v.get_property().cloned())
1209        .map(|v| {
1210            // Fallback using hardcoded 16px base
1211            v.inner.to_pixels_internal(0.0, DEFAULT_FONT_SIZE)
1212        })
1213        .unwrap_or(DEFAULT_FONT_SIZE)
1214}
1215
1216/// Helper function to get parent's computed font-size
1217fn get_parent_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
1218    styled_dom
1219        .node_hierarchy
1220        .as_container()
1221        .get(dom_id)
1222        .and_then(|node| node.parent_id())
1223        .map(|parent_id| get_element_font_size(styled_dom, parent_id))
1224        .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE)
1225}
1226
1227/// Helper function to get root element's font-size
1228fn get_root_font_size(styled_dom: &StyledDom) -> f32 {
1229    // Root is always NodeId(0) in Azul
1230    get_element_font_size(styled_dom, NodeId::new(0))
1231}
1232
1233/// Create a ResolutionContext for a given node
1234fn create_resolution_context(
1235    styled_dom: &StyledDom,
1236    dom_id: NodeId,
1237    containing_block_size: Option<azul_css::props::basic::PhysicalSize>,
1238) -> azul_css::props::basic::ResolutionContext {
1239    let element_font_size = get_element_font_size(styled_dom, dom_id);
1240    let parent_font_size = get_parent_font_size(styled_dom, dom_id);
1241    let root_font_size = get_root_font_size(styled_dom);
1242
1243    ResolutionContext {
1244        element_font_size,
1245        parent_font_size,
1246        root_font_size,
1247        containing_block_size: containing_block_size.unwrap_or(PhysicalSize::new(0.0, 0.0)),
1248        element_size: None, // Not yet laid out
1249        viewport_size: PhysicalSize::new(0.0, 0.0),
1250    }
1251}
1252
1253fn resolve_box_props(
1254    styled_dom: &StyledDom,
1255    dom_id: NodeId,
1256    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1257) -> BoxProps {
1258    use crate::solver3::getters::*;
1259
1260    let node_data = &styled_dom.node_data.as_container()[dom_id];
1261
1262    // Get styled node state
1263    let node_state = styled_dom
1264        .styled_nodes
1265        .as_container()
1266        .get(dom_id)
1267        .map(|n| &n.styled_node_state)
1268        .cloned()
1269        .unwrap_or_default();
1270
1271    // Create resolution context for this element
1272    // Note: containing_block_size is None here because we don't have it yet
1273    // This is fine - margins/padding use containing block width, but we'll handle that later
1274    let context = create_resolution_context(styled_dom, dom_id, None);
1275
1276    // Helper to extract and resolve pixel value from MultiValue<PixelValue>
1277    let resolve_value = |mv: MultiValue<PixelValue>, prop_context: PropertyContext| -> f32 {
1278        match mv {
1279            MultiValue::Exact(pv) => pv.resolve_with_context(&context, prop_context),
1280            _ => 0.0,
1281        }
1282    };
1283
1284    // Read margin, padding, border from styled_dom
1285    let margin_top_mv = get_css_margin_top(styled_dom, dom_id, &node_state);
1286    let margin_right_mv = get_css_margin_right(styled_dom, dom_id, &node_state);
1287    let margin_bottom_mv = get_css_margin_bottom(styled_dom, dom_id, &node_state);
1288    let margin_left_mv = get_css_margin_left(styled_dom, dom_id, &node_state);
1289
1290    let margin = crate::solver3::geometry::EdgeSizes {
1291        top: resolve_value(margin_top_mv, PropertyContext::Margin),
1292        right: resolve_value(margin_right_mv, PropertyContext::Margin),
1293        bottom: resolve_value(margin_bottom_mv, PropertyContext::Margin),
1294        left: resolve_value(margin_left_mv, PropertyContext::Margin),
1295    };
1296
1297    // Debug for Body nodes
1298    if matches!(node_data.node_type, azul_core::dom::NodeType::Body) {
1299        if let Some(msgs) = debug_messages.as_mut() {
1300            msgs.push(LayoutDebugMessage::box_props(format!(
1301                "Body margin resolved: top={:.2}, right={:.2}, bottom={:.2}, left={:.2}",
1302                margin.top, margin.right, margin.bottom, margin.left
1303            )));
1304        }
1305    }
1306
1307    let padding = crate::solver3::geometry::EdgeSizes {
1308        top: resolve_value(
1309            get_css_padding_top(styled_dom, dom_id, &node_state),
1310            PropertyContext::Padding,
1311        ),
1312        right: resolve_value(
1313            get_css_padding_right(styled_dom, dom_id, &node_state),
1314            PropertyContext::Padding,
1315        ),
1316        bottom: resolve_value(
1317            get_css_padding_bottom(styled_dom, dom_id, &node_state),
1318            PropertyContext::Padding,
1319        ),
1320        left: resolve_value(
1321            get_css_padding_left(styled_dom, dom_id, &node_state),
1322            PropertyContext::Padding,
1323        ),
1324    };
1325
1326    let border = crate::solver3::geometry::EdgeSizes {
1327        top: resolve_value(
1328            get_css_border_top_width(styled_dom, dom_id, &node_state),
1329            PropertyContext::Other,
1330        ),
1331        right: resolve_value(
1332            get_css_border_right_width(styled_dom, dom_id, &node_state),
1333            PropertyContext::Other,
1334        ),
1335        bottom: resolve_value(
1336            get_css_border_bottom_width(styled_dom, dom_id, &node_state),
1337            PropertyContext::Other,
1338        ),
1339        left: resolve_value(
1340            get_css_border_left_width(styled_dom, dom_id, &node_state),
1341            PropertyContext::Other,
1342        ),
1343    };
1344
1345    BoxProps {
1346        margin,
1347        padding,
1348        border,
1349    }
1350}
1351
1352/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 1:
1353/// "Remove all irrelevant boxes. These are boxes that do not contain table-related boxes
1354/// and do not themselves have 'display' set to a table-related value. In this context,
1355/// 'irrelevant boxes' means anonymous inline boxes that contain only white space."
1356///
1357/// Checks if a DOM node is whitespace-only text (for table anonymous box generation).
1358/// Returns true if the node is a text node containing only whitespace characters.
1359fn is_whitespace_only_text(styled_dom: &StyledDom, node_id: NodeId) -> bool {
1360    let binding = styled_dom.node_data.as_container();
1361    let node_data = binding.get(node_id);
1362    if let Some(data) = node_data {
1363        if let NodeType::Text(text) = data.get_node_type() {
1364            // Check if the text contains only whitespace characters
1365            // Per CSS 2.2 Section 17.2.1: whitespace-only anonymous boxes are irrelevant
1366            return text.chars().all(|c| c.is_whitespace());
1367        }
1368    }
1369
1370    false
1371}
1372
1373/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 1:
1374/// Determines if a node should be skipped in table structure generation.
1375/// Whitespace-only text nodes are "irrelevant" and should not generate boxes
1376/// when they appear between table-related elements.
1377///
1378/// Returns true if the node should be skipped (i.e., it's whitespace-only text
1379/// and the parent is a table structural element).
1380fn should_skip_for_table_structure(
1381    styled_dom: &StyledDom,
1382    node_id: NodeId,
1383    parent_display: LayoutDisplay,
1384) -> bool {
1385    // CSS 2.2 Section 17.2.1: Only skip whitespace text nodes when parent is
1386    // a table structural element (table, row group, row)
1387    matches!(
1388        parent_display,
1389        LayoutDisplay::Table
1390            | LayoutDisplay::TableRowGroup
1391            | LayoutDisplay::TableHeaderGroup
1392            | LayoutDisplay::TableFooterGroup
1393            | LayoutDisplay::TableRow
1394    ) && is_whitespace_only_text(styled_dom, node_id)
1395}
1396
1397/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 3:
1398/// "Generate missing parents. For each table-cell box C in a sequence of consecutive
1399/// table-cell boxes (that are not part of a table-row), an anonymous table-row box
1400/// is generated around C and its consecutive table-cell siblings.
1401///
1402/// For each proper table child C in a sequence of consecutive proper table children
1403/// that are misparented (i.e., their parent is not a table element), an anonymous
1404/// table box is generated around C and its consecutive siblings."
1405///
1406/// This function checks if a node needs a parent wrapper and returns the appropriate
1407/// anonymous box type, or None if no wrapper is needed.
1408fn needs_table_parent_wrapper(
1409    styled_dom: &StyledDom,
1410    node_id: NodeId,
1411    parent_display: LayoutDisplay,
1412) -> Option<AnonymousBoxType> {
1413    let child_display = get_display_type(styled_dom, node_id);
1414
1415    // CSS 2.2 Section 17.2.1, Stage 3:
1416    // If we have a table-cell but parent is not a table-row, need anonymous row
1417    if child_display == LayoutDisplay::TableCell {
1418        match parent_display {
1419            LayoutDisplay::TableRow
1420            | LayoutDisplay::TableRowGroup
1421            | LayoutDisplay::TableHeaderGroup
1422            | LayoutDisplay::TableFooterGroup => {
1423                // Parent can contain cells directly or via rows - no wrapper needed
1424                None
1425            }
1426            _ => Some(AnonymousBoxType::TableRow),
1427        }
1428    }
1429    // If we have a table-row but parent is not a table/row-group, need anonymous table
1430    else if matches!(child_display, LayoutDisplay::TableRow) {
1431        match parent_display {
1432            LayoutDisplay::Table
1433            | LayoutDisplay::TableRowGroup
1434            | LayoutDisplay::TableHeaderGroup
1435            | LayoutDisplay::TableFooterGroup => {
1436                None // Parent is correct
1437            }
1438            _ => Some(AnonymousBoxType::TableWrapper),
1439        }
1440    }
1441    // If we have a row-group but parent is not a table, need anonymous table
1442    else if matches!(
1443        child_display,
1444        LayoutDisplay::TableRowGroup
1445            | LayoutDisplay::TableHeaderGroup
1446            | LayoutDisplay::TableFooterGroup
1447    ) {
1448        match parent_display {
1449            LayoutDisplay::Table => None,
1450            _ => Some(AnonymousBoxType::TableWrapper),
1451        }
1452    } else {
1453        None
1454    }
1455}
1456
1457// Determines the display type of a node based on its tag and CSS properties.
1458pub fn get_display_type(styled_dom: &StyledDom, node_id: NodeId) -> LayoutDisplay {
1459    if let Some(_styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
1460        let node_data = &styled_dom.node_data.as_container()[node_id];
1461        let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
1462
1463        // 1. Check author CSS first
1464        if let Some(d) = styled_dom
1465            .css_property_cache
1466            .ptr
1467            .get_display(node_data, &node_id, node_state)
1468            .and_then(|v| v.get_property().copied())
1469        {
1470            return d;
1471        }
1472
1473        // 2. Check User Agent CSS (always returns a value for display)
1474        let node_type = &styled_dom.node_data.as_container()[node_id].node_type;
1475        if let Some(ua_prop) =
1476            azul_core::ua_css::get_ua_property(node_type, CssPropertyType::Display)
1477        {
1478            if let CssProperty::Display(azul_css::css::CssPropertyValue::Exact(d)) = ua_prop {
1479                return *d;
1480            }
1481        }
1482    }
1483
1484    // 3. Final fallback (should never be reached since UA CSS always provides display)
1485    // Inline is the safest default per CSS spec
1486    LayoutDisplay::Inline
1487}
1488
1489/// **Corrected:** Checks for all conditions that create a new Block Formatting Context.
1490/// A BFC contains floats and prevents margin collapse.
1491fn establishes_new_block_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> bool {
1492    let display = get_display_type(styled_dom, node_id);
1493    if matches!(
1494        display,
1495        LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::FlowRoot
1496    ) {
1497        return true;
1498    }
1499
1500    if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
1501        // `overflow` other than `visible`
1502
1503        let overflow_x = get_overflow_x(styled_dom, node_id, &styled_node.styled_node_state);
1504        if !overflow_x.is_visible_or_clip() {
1505            return true;
1506        }
1507
1508        let overflow_y = get_overflow_y(styled_dom, node_id, &styled_node.styled_node_state);
1509        if !overflow_y.is_visible_or_clip() {
1510            return true;
1511        }
1512
1513        // `position: absolute` or `position: fixed`
1514        let position = get_position(styled_dom, node_id, &styled_node.styled_node_state);
1515
1516        if position.is_absolute_or_fixed() {
1517            return true;
1518        }
1519
1520        // `float` is not `none`
1521        let float = get_float(styled_dom, node_id, &styled_node.styled_node_state);
1522        if !float.is_none() {
1523            return true;
1524        }
1525    }
1526
1527    // The root element (<html>) also establishes a BFC.
1528    if styled_dom.root.into_crate_internal() == Some(node_id) {
1529        return true;
1530    }
1531
1532    false
1533}
1534
1535/// The logic now correctly identifies all BFC roots.
1536fn determine_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> FormattingContext {
1537    // Special case: Text nodes should be treated as inline content.
1538    // They participate in their parent's inline formatting context.
1539    let node_data = &styled_dom.node_data.as_container()[node_id];
1540
1541    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
1542        // Text nodes are inline-level content within their parent's IFC
1543        return FormattingContext::Inline;
1544    }
1545
1546    let display_type = get_display_type(styled_dom, node_id);
1547
1548    match display_type {
1549        LayoutDisplay::Inline => FormattingContext::Inline,
1550
1551        // CSS 2.2 Section 9.4.2: "An inline formatting context is established by a
1552        // block container box that contains no block-level boxes."
1553        // Check if this block container has only inline-level children.
1554        LayoutDisplay::Block | LayoutDisplay::FlowRoot | LayoutDisplay::ListItem => {
1555            if has_only_inline_children(styled_dom, node_id) {
1556                // This block container should establish an IFC for its inline children
1557                FormattingContext::Inline
1558            } else {
1559                // Normal BFC
1560                FormattingContext::Block {
1561                    establishes_new_context: establishes_new_block_formatting_context(
1562                        styled_dom, node_id,
1563                    ),
1564                }
1565            }
1566        }
1567        LayoutDisplay::InlineBlock => FormattingContext::InlineBlock,
1568        LayoutDisplay::Table | LayoutDisplay::InlineTable => FormattingContext::Table,
1569        LayoutDisplay::TableRowGroup
1570        | LayoutDisplay::TableHeaderGroup
1571        | LayoutDisplay::TableFooterGroup => FormattingContext::TableRowGroup,
1572        LayoutDisplay::TableRow => FormattingContext::TableRow,
1573        LayoutDisplay::TableCell => FormattingContext::TableCell,
1574        LayoutDisplay::None => FormattingContext::None,
1575        LayoutDisplay::Flex | LayoutDisplay::InlineFlex => FormattingContext::Flex,
1576        LayoutDisplay::TableColumnGroup => FormattingContext::TableColumnGroup,
1577        LayoutDisplay::TableCaption => FormattingContext::TableCaption,
1578        LayoutDisplay::Grid | LayoutDisplay::InlineGrid => FormattingContext::Grid,
1579
1580        // These less common display types default to block behavior
1581        LayoutDisplay::TableColumn | LayoutDisplay::RunIn | LayoutDisplay::Marker => {
1582            FormattingContext::Block {
1583                establishes_new_context: true,
1584            }
1585        }
1586    }
1587}