Skip to main content

azul_layout/solver3/
layout_tree.rs

1//! Layout tree construction from a styled DOM, including anonymous box generation
2use std::{
3    collections::{BTreeMap, HashMap},
4    hash::{Hash, Hasher},
5    sync::{
6        atomic::{AtomicU32, Ordering},
7        Arc,
8    },
9};
10
11use azul_core::diff::NodeDataFingerprint;
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, NodeData, 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, StyleWhiteSpace},
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_direction_property as get_direction,
117            get_display_property, get_float, get_overflow_x,
118            get_overflow_y, get_position, get_text_align,
119            get_text_orientation_property as get_text_orientation,
120            get_white_space_property, get_writing_mode, MultiValue,
121        },
122        scrollbar::ScrollbarRequirements,
123        LayoutContext, Result,
124    },
125    text3::cache::AvailableSpace,
126};
127
128/// Represents the invalidation state of a layout node.
129///
130/// The states are ordered by severity, allowing for easy "upgrading" of the dirty state.
131/// A node marked for `Layout` does not also need to be marked for `Paint`.
132///
133/// Because this enum derives `PartialOrd` and `Ord`, you can directly compare variants:
134///
135/// - `DirtyFlag::Layout > DirtyFlag::Paint` is `true`
136/// - `DirtyFlag::Paint >= DirtyFlag::None` is `true`
137/// - `DirtyFlag::Paint < DirtyFlag::Layout` is `true`
138#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
139pub enum DirtyFlag {
140    /// The node's layout is valid and no repaint is needed. This is the "clean" state.
141    #[default]
142    None,
143    /// The node's geometry is valid, but its appearance (e.g., color) has changed.
144    /// Requires a display list update only.
145    Paint,
146    /// The node's geometry (size or position) is invalid.
147    /// Requires a full layout pass and a display list update.
148    Layout,
149}
150
151/// A hash that represents the content and style of a node PLUS all of its descendants.
152/// If two SubtreeHashes are equal, their entire subtrees are considered identical for layout
153/// purposes.
154#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
155pub struct SubtreeHash(pub u64);
156
157/// Per-item metrics cached from the last IFC layout.
158///
159/// These metrics enable incremental IFC relayout (Phase 2 optimization):
160/// when a single inline item changes, we can check whether its advance width
161/// changed and potentially skip full line-breaking for unaffected lines.
162///
163/// Index in `CachedInlineLayout::item_metrics` matches the item order in
164/// `UnifiedLayout::items`.
165#[derive(Debug, Clone)]
166pub struct InlineItemMetrics {
167    /// The DOM NodeId of the source node for this item (for dirty checking).
168    /// `None` for generated content (list markers, hyphens, etc.)
169    pub source_node_id: Option<NodeId>,
170    /// Advance width of this item (glyph run width, inline-block width, etc.)
171    pub advance_width: f32,
172    /// Advance height contribution from this item to its line box.
173    pub line_height_contribution: f32,
174    /// Whether this item can participate in line breaking.
175    /// `false` for items inside `white-space: nowrap` or `white-space: pre`.
176    pub can_break: bool,
177    /// Which line this item was placed on (0-indexed).
178    pub line_index: u32,
179    /// X offset within its line.
180    pub x_offset: f32,
181}
182
183/// Cached inline layout result with the constraints used to compute it.
184///
185/// This structure solves a fundamental architectural problem: inline layouts
186/// (text wrapping, inline-block positioning) depend on the available width.
187/// Different layout phases may compute the layout with different widths:
188///
189/// 1. **Min-content measurement**: width = MinContent (effectively 0)
190/// 2. **Max-content measurement**: width = MaxContent (effectively infinite)
191/// 3. **Final layout**: width = Definite(actual_column_width)
192///
193/// Without tracking which constraints were used, a cached result from phase 1
194/// would incorrectly be reused in phase 3, causing text to wrap at the wrong
195/// positions (the root cause of table cell width bugs).
196///
197/// By storing the constraints alongside the result, we can:
198/// - Invalidate the cache when constraints change
199/// - Keep multiple cached results for different constraint types if needed
200/// - Ensure the final render always uses a layout computed with correct widths
201#[derive(Debug, Clone)]
202pub struct CachedInlineLayout {
203    /// The computed inline layout
204    pub layout: Arc<UnifiedLayout>,
205    /// The available width constraint used to compute this layout.
206    /// This is the key for cache validity checking.
207    /// +spec:writing-modes:1dcba2 - "available width" (CSS2.1) = auto size in inline axis
208    pub available_width: AvailableSpace,
209    /// Whether this layout was computed with float exclusions.
210    /// Float-aware layouts should not be overwritten by non-float layouts.
211    pub has_floats: bool,
212    /// The full constraints used to compute this layout.
213    /// Used for quick relayout after text edits without rebuilding from CSS.
214    pub constraints: Option<UnifiedConstraints>,
215    /// Per-item metrics for incremental IFC relayout (Phase 2).
216    ///
217    /// Each entry corresponds to one `PositionedItem` in `layout.items`.
218    /// These metrics enable the IFC relayout decision tree:
219    /// - Check if a dirty node's advance_width changed → skip repositioning if not
220    /// - Use `can_break` + `line_index` for the nowrap fast path
221    /// - Use `x_offset` for shifting subsequent items without full line-breaking
222    pub item_metrics: Vec<InlineItemMetrics>,
223    /// Cached line break boundaries for incremental relayout.
224    /// Enables checking if a width change fits on the same line without
225    /// re-running the full line-breaking algorithm.
226    pub line_breaks: Option<crate::text3::cache::CachedLineBreaks>,
227}
228
229impl CachedInlineLayout {
230    /// Creates a new cached inline layout.
231    pub fn new(
232        layout: Arc<UnifiedLayout>,
233        available_width: AvailableSpace,
234        has_floats: bool,
235    ) -> Self {
236        let item_metrics = Self::extract_item_metrics(&layout);
237        Self {
238            layout,
239            available_width,
240            has_floats,
241            constraints: None,
242            item_metrics,
243            line_breaks: None,
244        }
245    }
246
247    /// Creates a new cached inline layout with full constraints.
248    pub fn new_with_constraints(
249        layout: Arc<UnifiedLayout>,
250        available_width: AvailableSpace,
251        has_floats: bool,
252        constraints: UnifiedConstraints,
253    ) -> Self {
254        let item_metrics = Self::extract_item_metrics(&layout);
255        let available_width_px = match available_width {
256            AvailableSpace::Definite(w) => w,
257            _ => f32::MAX,
258        };
259        let line_breaks = Some(crate::text3::cache::extract_line_breaks(
260            &layout.items, available_width_px,
261        ));
262        Self {
263            layout,
264            available_width,
265            has_floats,
266            constraints: Some(constraints),
267            item_metrics,
268            line_breaks,
269        }
270    }
271
272    /// Extracts per-item metrics from a computed `UnifiedLayout`.
273    ///
274    /// This is called automatically by the constructors. The metrics
275    /// enable incremental IFC relayout in Phase 2c/2d by providing
276    /// cached advance widths, line assignments, and break information
277    /// for each positioned item.
278    fn extract_item_metrics(layout: &UnifiedLayout) -> Vec<InlineItemMetrics> {
279        use crate::text3::cache::{ShapedItem, get_item_vertical_metrics_approx};
280
281        layout.items.iter().map(|positioned_item| {
282            let bounds = positioned_item.item.bounds();
283            let (ascent, descent) = get_item_vertical_metrics_approx(&positioned_item.item);
284
285            let source_node_id = match &positioned_item.item {
286                ShapedItem::Cluster(c) => c.source_node_id,
287                // Objects (inline-blocks, images) and other generated items
288                // don't expose source_node_id directly on ShapedItem.
289                // Phase 2c will refine this via the ContentIndex mapping.
290                ShapedItem::Object { .. }
291                | ShapedItem::CombinedBlock { .. }
292                | ShapedItem::Tab { .. }
293                | ShapedItem::Break { .. } => None,
294            };
295
296            // For Phase 2a, default can_break = true for all items.
297            // Phase 2c will refine this by checking the white-space property
298            // on the IFC root's style or the item's own style context.
299            // (Note: text3::StyleProperties doesn't carry white-space;
300            //  that's resolved at the IFC/BFC boundary level.)
301            let can_break = !matches!(&positioned_item.item, ShapedItem::Break { .. });
302
303            InlineItemMetrics {
304                source_node_id,
305                advance_width: bounds.width,
306                line_height_contribution: ascent + descent,
307                can_break,
308                line_index: positioned_item.line_index as u32,
309                x_offset: positioned_item.position.x,
310            }
311        }).collect()
312    }
313
314    /// Checks if this cached layout is valid for the given constraints.
315    ///
316    /// A cached layout is valid if:
317    /// 1. The available width matches (definite widths must be equal, or both are the same
318    ///    indefinite type)
319    /// 2. OR the new request doesn't have floats but the cached one does (keep float-aware layout)
320    ///
321    /// The second condition preserves float-aware layouts, which are more "correct" than
322    /// non-float layouts and shouldn't be overwritten.
323    pub fn is_valid_for(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
324        // If we have a float-aware layout and the new request doesn't have floats,
325        // keep the float-aware layout (it's more accurate)
326        if self.has_floats && !new_has_floats {
327            // But only if the width constraint type matches
328            return self.width_constraint_matches(new_width);
329        }
330
331        // Otherwise, require exact width match
332        self.width_constraint_matches(new_width)
333    }
334
335    /// Tolerance for comparing definite layout widths (in logical pixels).
336    /// Sub-pixel differences below this threshold are treated as identical
337    /// to avoid unnecessary relayout from floating-point rounding.
338    const LAYOUT_WIDTH_EPSILON: f32 = 0.1;
339
340    /// Checks if the width constraint matches.
341    fn width_constraint_matches(&self, new_width: AvailableSpace) -> bool {
342        match (self.available_width, new_width) {
343            // Definite widths must match within a small epsilon
344            (AvailableSpace::Definite(old), AvailableSpace::Definite(new)) => {
345                (old - new).abs() < Self::LAYOUT_WIDTH_EPSILON
346            }
347            // MinContent matches MinContent
348            (AvailableSpace::MinContent, AvailableSpace::MinContent) => true,
349            // MaxContent matches MaxContent
350            (AvailableSpace::MaxContent, AvailableSpace::MaxContent) => true,
351            // Different constraint types don't match
352            _ => false,
353        }
354    }
355
356    /// Determines if this cached layout should be replaced by a new layout.
357    ///
358    /// Returns true if the new layout should replace this one.
359    pub fn should_replace_with(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
360        // Always replace if we gain float information
361        if new_has_floats && !self.has_floats {
362            return true;
363        }
364
365        // Replace if width constraint changed
366        !self.width_constraint_matches(new_width)
367    }
368
369    /// Returns a reference to the inner UnifiedLayout.
370    ///
371    /// This is a convenience method for code that only needs the layout data
372    /// and doesn't care about the caching metadata.
373    #[inline]
374    pub fn get_layout(&self) -> &Arc<UnifiedLayout> {
375        &self.layout
376    }
377
378    /// Returns a clone of the inner Arc<UnifiedLayout>.
379    ///
380    /// This is useful for APIs that need to return an owned reference
381    /// to the layout without exposing the caching metadata.
382    #[inline]
383    pub fn clone_layout(&self) -> Arc<UnifiedLayout> {
384        self.layout.clone()
385    }
386}
387
388/// A layout tree node representing the CSS box model.
389///
390/// ## Memory Layout Optimization (`#[repr(C)]`)
391///
392/// Fields are ordered by access frequency (hottest first) to maximize CPU
393/// cache line utilization during tree traversal. With `#[repr(C)]`, the
394/// compiler preserves this ordering. The 6 hottest fields (~140 bytes)
395/// occupy the first 2-3 cache lines (64 bytes each), which are loaded
396/// first by the hardware prefetcher.
397///
398/// | Tier   | Fields                                  | ~Bytes | Accesses |
399/// |--------|-----------------------------------------|--------|----------|
400/// | HOT    | box_props, dom_node_id, children,       |  ~140  |  410+    |
401/// |        | used_size, formatting_context, parent    |        |          |
402/// | WARM   | intrinsic_sizes..computed_style          |  ~220  |  ~80     |
403/// | COLD   | dirty_flag..is_anonymous                 |  ~190  |  ~20     |
404///
405/// Note: An absolute position is a final paint-time value and shouldn't be
406/// cached on the node itself, as it can change even if the node's
407/// layout is clean (e.g., if a sibling changes size). We will calculate
408/// it in a separate map.
409#[derive(Debug, Clone)]
410#[repr(C)]
411pub struct LayoutNode {
412    // ── HOT tier: accessed on every node in every layout pass ────────────
413    // These fields should fit in the first 2-3 cache lines (~128-192 bytes).
414
415    /// The resolved box model properties (margin, border, padding)
416    /// in logical pixels. Cached after first resolution.
417    /// (148 accesses — hottest field)
418    pub box_props: BoxProps,
419    /// Reference back to the original DOM node (None for anonymous boxes)
420    /// (111 accesses)
421    pub dom_node_id: Option<NodeId>,
422    /// Children indices in the layout tree
423    /// (53 accesses)
424    pub children: Vec<usize>,
425    /// The size used during the last layout pass.
426    /// (43 accesses)
427    pub used_size: Option<LogicalSize>,
428    /// The formatting context this node establishes or participates in.
429    /// (30 accesses)
430    pub formatting_context: FormattingContext,
431    /// Parent index (None for root)
432    /// (25 accesses)
433    pub parent: Option<usize>,
434
435    // ── WARM tier: frequently accessed but not on every node ─────────────
436
437    /// Cached intrinsic sizes (min-content, max-content, etc.)
438    /// (16 accesses — sizing pass only)
439    pub intrinsic_sizes: Option<IntrinsicSizes>,
440    // +spec:display-property:af3a89 - alignment baseline for inline-level boxes
441    /// The baseline of this box, if applicable, measured from its content-box top edge.
442    /// (14 accesses — IFC/table alignment)
443    pub baseline: Option<f32>,
444    /// Cached inline layout result with the constraints used to compute it.
445    ///
446    /// This field stores both the computed layout AND the constraints (available width,
447    /// float state) under which it was computed. This is essential for correctness:
448    /// 
449    /// - Table cells are measured multiple times with different widths
450    /// - Min-content/max-content intrinsic sizing uses special constraint values
451    /// - The final layout must use the actual available width, not a measurement width
452    ///
453    /// By tracking the constraints, we avoid the bug where a min-content measurement
454    /// (with width=0) would be incorrectly reused for final rendering.
455    /// (13 accesses — IFC roots / table cells)
456    pub inline_layout_result: Option<CachedInlineLayout>,
457    /// Cached scrollbar information (calculated during layout)
458    /// Used to determine if scrollbars appeared/disappeared requiring reflow
459    /// (12 accesses — scrollable containers only)
460    pub scrollbar_info: Option<ScrollbarRequirements>,
461    /// The position of this node *relative to its parent's content box*.
462    /// (9 accesses — positioning pass)
463    pub relative_position: Option<LogicalPosition>,
464    /// The actual content size (children overflow size) for scrollable containers.
465    /// This is the size of all content that might need to be scrolled, which can
466    /// be larger than `used_size` when content overflows the container.
467    /// (7 accesses — scrollable containers)
468    pub overflow_content_size: Option<LogicalSize>,
469    /// Cache for Taffy layout computations for this node.
470    /// (6 accesses — Taffy bridge)
471    pub taffy_cache: TaffyCache,
472    /// Pre-computed CSS properties needed during layout.
473    /// Computed once during layout tree build to avoid repeated style lookups.
474    /// (5 accesses — cache.rs only)
475    pub computed_style: ComputedLayoutStyle,
476    /// Pseudo-element type (::marker, ::before, ::after) if this node is a pseudo-element
477    /// (5 accesses — pseudo-elements only)
478    pub pseudo_element: Option<PseudoElement>,
479    /// Escaped top margin (CSS 2.1 margin collapsing)
480    /// If this BFC's first child's top margin "escaped" the BFC, this contains
481    /// the collapsed margin that should be applied by the parent.
482    /// (4 accesses — BFC margin collapsing)
483    pub escaped_top_margin: Option<f32>,
484    /// Escaped bottom margin (CSS 2.1 margin collapsing)  
485    /// If this BFC's last child's bottom margin "escaped" the BFC, this contains
486    /// the collapsed margin that should be applied by the parent.
487    /// (4 accesses)
488    pub escaped_bottom_margin: Option<f32>,
489    /// Parent's formatting context (needed to determine if stretch applies)
490    /// (4 accesses — flex/grid children)
491    pub parent_formatting_context: Option<FormattingContext>,
492    /// If this node participates in an IFC (is inline content like text),
493    /// stores the reference back to the IFC root and the run index.
494    /// This allows text nodes to find their layout data in the parent's IFC.
495    /// (3 accesses — text nodes only)
496    pub ifc_membership: Option<IfcMembership>,
497    /// The layout tree index of this node's containing block.
498    /// - For abs-pos elements: nearest positioned (non-static) ancestor
499    /// - For fixed elements: root / None (viewport)
500    /// - For normal-flow: parent (None = implicit)
501    /// Used for clip exemption: abs-pos elements whose containing block
502    /// is above an overflow clipper should not be clipped.
503    pub containing_block_index: Option<usize>,
504
505    // ── COLD tier: construction / reconciliation / debugging only ────────
506
507    /// Type of anonymous box (if applicable)
508    /// (2 accesses)
509    pub anonymous_type: Option<AnonymousBoxType>,
510    /// Multi-field fingerprint of this node's data (style, text, etc.)
511    /// for granular change detection during reconciliation.
512    /// (2 accesses — reconciliation only)
513    pub node_data_fingerprint: NodeDataFingerprint,
514    /// A hash of this node's data and all of its descendants. Used for
515    /// fast reconciliation.
516    /// (9 accesses — all in cache.rs reconciliation)
517    pub subtree_hash: SubtreeHash,
518    /// Dirty flags to track what needs recalculation.
519    /// (7 accesses — reconciliation setup)
520    pub dirty_flag: DirtyFlag,
521    /// Unresolved box model properties (raw CSS values).
522    /// These are resolved lazily during layout when containing block is known.
523    /// (1 access — initial resolution only)
524    pub unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps,
525    /// If this node is an IFC root, stores the IFC ID.
526    /// Used to identify which IFC this node's `inline_layout_result` belongs to.
527    /// (1 access — IFC creation only)
528    pub ifc_id: Option<IfcId>,
529}
530
531/// Pre-computed CSS properties needed during layout.
532/// 
533/// This struct stores resolved CSS values that are frequently accessed during
534/// layout calculations. By computing these once during layout tree construction,
535/// we avoid O(n * m) style lookups where n = nodes and m = layout passes.
536///
537/// All values are resolved to their final form (no 'inherit', 'initial', etc.)
538#[derive(Debug, Clone, Default)]
539pub struct ComputedLayoutStyle {
540    /// CSS `display` property
541    pub display: LayoutDisplay,
542    /// CSS `position` property
543    pub position: LayoutPosition,
544    /// CSS `float` property
545    pub float: LayoutFloat,
546    /// CSS `overflow-x` property
547    pub overflow_x: LayoutOverflow,
548    /// CSS `overflow-y` property
549    pub overflow_y: LayoutOverflow,
550    /// CSS `writing-mode` property
551    pub writing_mode: azul_css::props::layout::LayoutWritingMode,
552    /// CSS `direction` property (ltr/rtl)
553    pub direction: azul_css::props::style::StyleDirection,
554    /// CSS `text-orientation` property (for vertical writing modes)
555    pub text_orientation: azul_css::props::style::effects::StyleTextOrientation,
556    /// CSS `width` property (None = auto)
557    pub width: Option<azul_css::props::layout::LayoutWidth>,
558    /// CSS `height` property (None = auto)
559    pub height: Option<azul_css::props::layout::LayoutHeight>,
560    /// CSS `min-width` property
561    pub min_width: Option<azul_css::props::layout::LayoutMinWidth>,
562    /// CSS `min-height` property
563    pub min_height: Option<azul_css::props::layout::LayoutMinHeight>,
564    /// CSS `max-width` property
565    pub max_width: Option<azul_css::props::layout::LayoutMaxWidth>,
566    /// CSS `max-height` property
567    pub max_height: Option<azul_css::props::layout::LayoutMaxHeight>,
568    /// CSS `text-align` property
569    pub text_align: azul_css::props::style::StyleTextAlign,
570}
571
572// Note: LayoutNode methods that cross hot/warm/cold boundaries have been
573// moved to LayoutTree methods (resolve_box_props, get_content_size).
574
575/// CSS pseudo-elements that can be generated
576#[derive(Debug, Clone, Copy, PartialEq, Eq)]
577pub enum PseudoElement {
578    /// ::marker pseudo-element for list items
579    Marker,
580    /// ::before pseudo-element
581    Before,
582    /// ::after pseudo-element
583    After,
584}
585
586// +spec:display-property:b7f4bf - anonymous inline/block boxes are both called "anonymous boxes"
587/// Types of anonymous boxes that can be generated
588// +spec:display-property:ae4f16 - anonymous boxes are treated as descendants alongside pseudo-elements
589#[derive(Debug, Clone, Copy, PartialEq)]
590pub enum AnonymousBoxType {
591    /// Anonymous block box wrapping inline content
592    InlineWrapper,
593    /// Anonymous box for a list item marker (bullet or number)
594    /// DEPRECATED: Use PseudoElement::Marker instead
595    ListItemMarker,
596    /// Anonymous table wrapper
597    TableWrapper,
598    /// Anonymous table row group (tbody)
599    TableRowGroup,
600    /// Anonymous table row
601    TableRow,
602    /// Anonymous table cell
603    TableCell,
604}
605
606// =============================================================================
607// SoA (struct-of-arrays) layout node split for cache performance
608// =============================================================================
609
610/// Hot layout node fields — accessed on every node in every layout pass.
611///
612/// Stored in a separate `Vec` for cache locality. At ~100 bytes per node,
613/// 1000 nodes fit in ~100 KB (L2 cache), vs ~550 KB with the monolithic struct.
614#[derive(Debug, Clone)]
615pub struct LayoutNodeHot {
616    /// The resolved box model properties (margin, border, padding)
617    /// Stored in packed i16×10 encoding to reduce cache footprint.
618    /// Use `box_props.unpack()` to get f32 `ResolvedBoxProps` for computation.
619    pub box_props: crate::solver3::geometry::PackedBoxProps,
620    /// Reference back to the original DOM node (None for anonymous boxes)
621    pub dom_node_id: Option<NodeId>,
622    /// The size used during the last layout pass.
623    pub used_size: Option<LogicalSize>,
624    /// The formatting context this node establishes or participates in.
625    pub formatting_context: FormattingContext,
626    /// Parent index (None for root)
627    pub parent: Option<usize>,
628}
629
630/// Warm layout node fields — accessed frequently but not on every node.
631///
632/// Stored in a separate `Vec`. These fields are accessed during specific
633/// layout phases (sizing, IFC, table alignment) but not during the main
634/// constraint-solving loop.
635#[derive(Debug, Clone, Default)]
636pub struct LayoutNodeWarm {
637    /// Cached intrinsic sizes (min-content, max-content, etc.)
638    pub intrinsic_sizes: Option<IntrinsicSizes>,
639    /// The baseline of this box, measured from its content-box top edge.
640    pub baseline: Option<f32>,
641    /// Cached inline layout result with the constraints used to compute it.
642    pub inline_layout_result: Option<CachedInlineLayout>,
643    /// Cached scrollbar information
644    pub scrollbar_info: Option<ScrollbarRequirements>,
645    /// The position relative to parent's content box.
646    pub relative_position: Option<LogicalPosition>,
647    /// The actual content size for scrollable containers.
648    pub overflow_content_size: Option<LogicalSize>,
649    /// Cache for Taffy layout computations.
650    pub taffy_cache: TaffyCache,
651    /// Pre-computed CSS properties needed during layout.
652    pub computed_style: ComputedLayoutStyle,
653    /// Pseudo-element type if this node is a pseudo-element
654    pub pseudo_element: Option<PseudoElement>,
655    /// Escaped top margin (CSS 2.1 margin collapsing)
656    pub escaped_top_margin: Option<f32>,
657    /// Escaped bottom margin (CSS 2.1 margin collapsing)
658    pub escaped_bottom_margin: Option<f32>,
659    /// Parent's formatting context
660    pub parent_formatting_context: Option<FormattingContext>,
661    /// IFC membership for text nodes
662    pub ifc_membership: Option<IfcMembership>,
663    /// Containing block index for clip exemption
664    pub containing_block_index: Option<usize>,
665}
666
667/// Cold layout node fields — construction / reconciliation / debugging only.
668///
669/// Stored in a separate `Vec`. These fields are rarely accessed during layout;
670/// mostly used during tree construction, reconciliation, and dirty tracking.
671#[derive(Debug, Clone)]
672pub struct LayoutNodeCold {
673    /// Type of anonymous box (if applicable)
674    pub anonymous_type: Option<AnonymousBoxType>,
675    /// Multi-field fingerprint for granular change detection.
676    pub node_data_fingerprint: NodeDataFingerprint,
677    /// Hash of this node's data + all descendants.
678    pub subtree_hash: SubtreeHash,
679    /// Dirty flags for recalculation tracking.
680    pub dirty_flag: DirtyFlag,
681    /// Unresolved box model properties (raw CSS values).
682    pub unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps,
683    /// IFC ID if this node is an IFC root.
684    pub ifc_id: Option<IfcId>,
685}
686
687impl Default for LayoutNodeCold {
688    fn default() -> Self {
689        Self {
690            anonymous_type: None,
691            node_data_fingerprint: NodeDataFingerprint::default(),
692            subtree_hash: SubtreeHash::default(),
693            dirty_flag: DirtyFlag::default(),
694            unresolved_box_props: Default::default(),
695            ifc_id: None,
696        }
697    }
698}
699
700impl LayoutNode {
701    /// Split this full layout node into hot/warm/cold components.
702    /// Used during `LayoutTreeBuilder::build()` to create the SoA layout.
703    pub fn split(self) -> (LayoutNodeHot, LayoutNodeWarm, LayoutNodeCold) {
704        (
705            LayoutNodeHot {
706                box_props: crate::solver3::geometry::PackedBoxProps::pack(&self.box_props),
707                dom_node_id: self.dom_node_id,
708                used_size: self.used_size,
709                formatting_context: self.formatting_context,
710                parent: self.parent,
711            },
712            LayoutNodeWarm {
713                intrinsic_sizes: self.intrinsic_sizes,
714                baseline: self.baseline,
715                inline_layout_result: self.inline_layout_result,
716                scrollbar_info: self.scrollbar_info,
717                relative_position: self.relative_position,
718                overflow_content_size: self.overflow_content_size,
719                taffy_cache: self.taffy_cache,
720                computed_style: self.computed_style,
721                pseudo_element: self.pseudo_element,
722                escaped_top_margin: self.escaped_top_margin,
723                escaped_bottom_margin: self.escaped_bottom_margin,
724                parent_formatting_context: self.parent_formatting_context,
725                ifc_membership: self.ifc_membership,
726                containing_block_index: self.containing_block_index,
727            },
728            LayoutNodeCold {
729                anonymous_type: self.anonymous_type,
730                node_data_fingerprint: self.node_data_fingerprint,
731                subtree_hash: self.subtree_hash,
732                dirty_flag: self.dirty_flag,
733                unresolved_box_props: self.unresolved_box_props,
734                ifc_id: self.ifc_id,
735            },
736        )
737    }
738}
739
740/// The complete layout tree structure.
741///
742/// Uses a struct-of-arrays (SoA) layout for cache performance:
743/// - `nodes` (hot): accessed on every node in every layout pass
744/// - `warm`: accessed during specific layout phases
745/// - `cold`: construction / reconciliation only
746#[derive(Debug, Clone)]
747pub struct LayoutTree {
748    /// Hot layout data — box props, parent, used_size, formatting context
749    pub nodes: Vec<LayoutNodeHot>,
750    /// Warm layout data — intrinsic sizes, baseline, inline layout, etc.
751    pub warm: Vec<LayoutNodeWarm>,
752    /// Cold layout data — dirty flags, fingerprints, reconciliation data
753    pub cold: Vec<LayoutNodeCold>,
754    /// Root node index
755    pub root: usize,
756    /// Mapping from DOM node IDs to layout node indices
757    pub dom_to_layout: HashMap<NodeId, Vec<usize>>,
758    /// Flat arena holding all children indices contiguously.
759    pub children_arena: Vec<usize>,
760    /// Per-node (start, len) into `children_arena`. Indexed by node index.
761    pub children_offsets: Vec<(u32, u32)>,
762    /// Per-node bit: this node or any descendant establishes a shrink-to-fit
763    /// (STF) context whose sizing algorithm reads children's intrinsic sizes
764    /// (flex/grid/table/inline-block containers, floats, or abspos elements).
765    ///
766    /// If `subtree_needs_intrinsic[i]` is false AND no ancestor of `i` is STF
767    /// either, the intrinsic sizing pass can skip the entire subtree — nothing
768    /// will ever read those values. This is the static-DOM optimization from
769    /// §58 Win #3 (the "safely re-enabled Fix C").
770    ///
771    /// Computed once at tree build time in `generate_layout_tree`. An empty
772    /// vec means "assume every subtree needs intrinsics" (safe fallback for
773    /// code paths that construct `LayoutTree` without going through the
774    /// builder — currently none, but preserves the invariant for tests).
775    pub subtree_needs_intrinsic: Vec<bool>,
776}
777
778/// Approximate per-field heap-byte breakdown of a [`LayoutTree`].
779#[derive(Debug, Clone, Default)]
780pub struct LayoutTreeMemoryReport {
781    pub node_count: usize,
782    pub hot_bytes: usize,
783    pub warm_bytes: usize,
784    pub warm_inline_layout_bytes: usize,
785    pub warm_taffy_cache_bytes: usize,
786    pub cold_bytes: usize,
787    pub dom_to_layout_bytes: usize,
788    pub children_arena_bytes: usize,
789    pub children_offsets_bytes: usize,
790}
791
792impl LayoutTreeMemoryReport {
793    pub fn total_bytes(&self) -> usize {
794        self.hot_bytes
795            + self.warm_bytes
796            + self.warm_inline_layout_bytes
797            + self.warm_taffy_cache_bytes
798            + self.cold_bytes
799            + self.dom_to_layout_bytes
800            + self.children_arena_bytes
801            + self.children_offsets_bytes
802    }
803}
804
805impl LayoutTree {
806    /// Approximate heap bytes retained by this LayoutTree.
807    pub fn memory_report(&self) -> LayoutTreeMemoryReport {
808        let mut report = LayoutTreeMemoryReport {
809            node_count: self.nodes.len(),
810            hot_bytes: self.nodes.capacity() * core::mem::size_of::<LayoutNodeHot>(),
811            warm_bytes: self.warm.capacity() * core::mem::size_of::<LayoutNodeWarm>(),
812            cold_bytes: self.cold.capacity() * core::mem::size_of::<LayoutNodeCold>(),
813            children_arena_bytes: self.children_arena.capacity() * core::mem::size_of::<usize>(),
814            children_offsets_bytes: self.children_offsets.capacity() * core::mem::size_of::<(u32, u32)>(),
815            dom_to_layout_bytes: 0,
816            warm_inline_layout_bytes: 0,
817            warm_taffy_cache_bytes: 0,
818        };
819        // HashMap<NodeId, Vec<usize>> — approximate: (key + Vec-header) per entry
820        // plus heap for each inner Vec.
821        let entries = self.dom_to_layout.len();
822        report.dom_to_layout_bytes = entries * (core::mem::size_of::<NodeId>() + core::mem::size_of::<Vec<usize>>());
823        for v in self.dom_to_layout.values() {
824            report.dom_to_layout_bytes += v.capacity() * core::mem::size_of::<usize>();
825        }
826        // Inline layout data lives behind Arc — count Arc heap-shares once
827        // per node that has a cached layout. Counted conservatively.
828        for w in &self.warm {
829            if let Some(cached) = &w.inline_layout_result {
830                // Arc<UnifiedLayout> — count the UnifiedLayout header + its items.
831                report.warm_inline_layout_bytes += core::mem::size_of::<crate::text3::cache::UnifiedLayout>();
832                report.warm_inline_layout_bytes += cached.layout.items.capacity()
833                    * core::mem::size_of::<crate::text3::cache::PositionedItem>();
834                report.warm_inline_layout_bytes += cached.item_metrics.capacity()
835                    * core::mem::size_of::<InlineItemMetrics>();
836                // Glyph bytes inside ShapedItem::Cluster — unbounded but bounded
837                // per entry. Approximate by counting clusters × 32 bytes/glyph.
838                for item in cached.layout.items.iter() {
839                    if let crate::text3::cache::ShapedItem::Cluster(c) = &item.item {
840                        report.warm_inline_layout_bytes += c.glyphs.capacity()
841                            * core::mem::size_of::<crate::text3::cache::ShapedGlyph>();
842                        report.warm_inline_layout_bytes += c.text.capacity();
843                    }
844                }
845            }
846            // Taffy cache — each slot is an Option, ~50 B empty
847            report.warm_taffy_cache_bytes += core::mem::size_of::<TaffyCache>();
848        }
849        report
850    }
851
852    /// Returns the children of node `index` as a contiguous slice from the arena.
853    #[inline]
854    pub fn children(&self, index: usize) -> &[usize] {
855        if let Some(&(start, len)) = self.children_offsets.get(index) {
856            &self.children_arena[(start as usize)..((start as usize) + (len as usize))]
857        } else {
858            &[]
859        }
860    }
861
862    /// Get hot layout data for a node (box_props, dom_node_id, used_size, etc.)
863    #[inline]
864    pub fn get(&self, index: usize) -> Option<&LayoutNodeHot> {
865        self.nodes.get(index)
866    }
867
868    /// Get mutable hot layout data for a node.
869    #[inline]
870    pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNodeHot> {
871        self.nodes.get_mut(index)
872    }
873
874    /// Get warm layout data for a node (intrinsic_sizes, baseline, inline_layout, etc.)
875    #[inline]
876    pub fn warm(&self, index: usize) -> Option<&LayoutNodeWarm> {
877        self.warm.get(index)
878    }
879
880    /// Get mutable warm layout data for a node.
881    #[inline]
882    pub fn warm_mut(&mut self, index: usize) -> Option<&mut LayoutNodeWarm> {
883        self.warm.get_mut(index)
884    }
885
886    /// Get cold layout data for a node (dirty_flag, subtree_hash, fingerprint, etc.)
887    #[inline]
888    pub fn cold(&self, index: usize) -> Option<&LayoutNodeCold> {
889        self.cold.get(index)
890    }
891
892    /// Get mutable cold layout data for a node.
893    #[inline]
894    pub fn cold_mut(&mut self, index: usize) -> Option<&mut LayoutNodeCold> {
895        self.cold.get_mut(index)
896    }
897
898    pub fn root_node(&self) -> &LayoutNodeHot {
899        &self.nodes[self.root]
900    }
901
902    /// Reconstruct a full `LayoutNode` from the split hot/warm/cold arrays.
903    ///
904    /// Used when passing node data to `LayoutTreeBuilder::clone_node_from_old()`.
905    pub fn get_full_node(&self, index: usize) -> Option<LayoutNode> {
906        let hot = self.nodes.get(index)?;
907        let warm = self.warm.get(index).cloned().unwrap_or_default();
908        let cold = self.cold.get(index).cloned().unwrap_or_default();
909        let children = self.children(index).to_vec();
910        Some(LayoutNode {
911            box_props: hot.box_props.unpack(),
912            dom_node_id: hot.dom_node_id,
913            children,
914            used_size: hot.used_size,
915            formatting_context: hot.formatting_context.clone(),
916            parent: hot.parent,
917            intrinsic_sizes: warm.intrinsic_sizes,
918            baseline: warm.baseline,
919            inline_layout_result: warm.inline_layout_result,
920            scrollbar_info: warm.scrollbar_info,
921            relative_position: warm.relative_position,
922            overflow_content_size: warm.overflow_content_size,
923            taffy_cache: warm.taffy_cache,
924            computed_style: warm.computed_style,
925            pseudo_element: warm.pseudo_element,
926            escaped_top_margin: warm.escaped_top_margin,
927            escaped_bottom_margin: warm.escaped_bottom_margin,
928            parent_formatting_context: warm.parent_formatting_context,
929            ifc_membership: warm.ifc_membership,
930            containing_block_index: warm.containing_block_index,
931            anonymous_type: cold.anonymous_type,
932            node_data_fingerprint: cold.node_data_fingerprint,
933            subtree_hash: cold.subtree_hash,
934            dirty_flag: cold.dirty_flag,
935            unresolved_box_props: cold.unresolved_box_props,
936            ifc_id: cold.ifc_id,
937        })
938    }
939
940    /// Re-resolve box properties for a node with the actual containing block size.
941    pub fn resolve_box_props(
942        &mut self,
943        node_index: usize,
944        containing_block: LogicalSize,
945        viewport_size: LogicalSize,
946        element_font_size: f32,
947        root_font_size: f32,
948    ) {
949        let params = crate::solver3::geometry::ResolutionParams {
950            containing_block,
951            viewport_size,
952            element_font_size,
953            root_font_size,
954        };
955        if let (Some(hot), Some(cold)) = (self.nodes.get_mut(node_index), self.cold.get(node_index)) {
956            hot.box_props = crate::solver3::geometry::PackedBoxProps::pack(&cold.unresolved_box_props.resolve(&params));
957        }
958    }
959
960    /// Marks a node and its ancestors as dirty with the given flag.
961    pub fn mark_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
962        if flag == DirtyFlag::None {
963            return;
964        }
965
966        let mut current_index = Some(start_index);
967        while let Some(index) = current_index {
968            let cold = match self.cold.get_mut(index) {
969                Some(c) => c,
970                None => break,
971            };
972            if cold.dirty_flag >= flag {
973                break;
974            }
975            cold.dirty_flag = flag;
976            current_index = self.nodes.get(index).and_then(|n| n.parent);
977        }
978    }
979
980    /// Marks a node and its entire subtree of descendants with the given dirty flag.
981    pub fn mark_subtree_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
982        if flag == DirtyFlag::None {
983            return;
984        }
985
986        let mut stack = vec![start_index];
987        while let Some(index) = stack.pop() {
988            let children = self.children(index).to_vec();
989            if let Some(cold) = self.cold.get_mut(index) {
990                if cold.dirty_flag < flag {
991                    cold.dirty_flag = flag;
992                }
993                stack.extend_from_slice(&children);
994            }
995        }
996    }
997
998    /// Resets the dirty flags of all nodes in the tree to `None` after layout is complete.
999    pub fn clear_all_dirty_flags(&mut self) {
1000        for cold in &mut self.cold {
1001            cold.dirty_flag = DirtyFlag::None;
1002        }
1003    }
1004
1005    /// Get inline layout for a node, navigating through IFC membership if needed.
1006    pub fn get_inline_layout_for_node(&self, layout_index: usize) -> Option<&std::sync::Arc<UnifiedLayout>> {
1007        let warm = self.warm.get(layout_index)?;
1008
1009        // First, check if this node has its own inline_layout_result (it's an IFC root)
1010        if let Some(cached) = &warm.inline_layout_result {
1011            return Some(cached.get_layout());
1012        }
1013
1014        // For text nodes, check if they have ifc_membership pointing to the IFC root
1015        if let Some(ifc_membership) = &warm.ifc_membership {
1016            let ifc_root_warm = self.warm.get(ifc_membership.ifc_root_layout_index)?;
1017            if let Some(cached) = &ifc_root_warm.inline_layout_result {
1018                return Some(cached.get_layout());
1019            }
1020        }
1021
1022        None
1023    }
1024
1025    /// Get the content size of a node (for scrollbar calculations).
1026    pub fn get_content_size(&self, index: usize) -> LogicalSize {
1027        let warm = match self.warm.get(index) {
1028            Some(w) => w,
1029            None => return LogicalSize::default(),
1030        };
1031
1032        if let Some(content_size) = warm.overflow_content_size {
1033            return content_size;
1034        }
1035
1036        let hot = match self.nodes.get(index) {
1037            Some(h) => h,
1038            None => return LogicalSize::default(),
1039        };
1040
1041        let mut content_size = hot.used_size.unwrap_or_default();
1042
1043        if let Some(ref cached_layout) = warm.inline_layout_result {
1044            let text_layout = &cached_layout.layout;
1045            let mut max_x: f32 = 0.0;
1046            let mut max_y: f32 = 0.0;
1047            for positioned_item in &text_layout.items {
1048                let item_bounds = positioned_item.item.bounds();
1049                max_x = max_x.max(positioned_item.position.x + item_bounds.width);
1050                max_y = max_y.max(positioned_item.position.y + item_bounds.height);
1051            }
1052            content_size.width = content_size.width.max(max_x);
1053            content_size.height = content_size.height.max(max_y);
1054        }
1055
1056        content_size
1057    }
1058}
1059
1060/// Generate layout tree from styled DOM with proper anonymous box generation
1061pub fn generate_layout_tree<T: ParsedFontTrait>(
1062    ctx: &mut LayoutContext<'_, T>,
1063) -> Result<LayoutTree> {
1064    let mut builder = LayoutTreeBuilder::new(ctx.viewport_size);
1065    let root_id = ctx
1066        .styled_dom
1067        .root
1068        .into_crate_internal()
1069        .unwrap_or(NodeId::ZERO);
1070    let root_index =
1071        builder.process_node(ctx.styled_dom, root_id, None, &mut ctx.debug_messages)?;
1072    let mut layout_tree = builder.build(root_index);
1073
1074    // Pre-compute the STF (shrink-to-fit) subtree bitmap. This is static-DOM
1075    // information: whether a subtree establishes any shrink-to-fit context
1076    // depends only on the DOM structure + formatting context, both of which
1077    // are frozen from here until the next layout-tree rebuild. The intrinsic
1078    // sizing pass reads this to skip subtrees whose intrinsics are never
1079    // consumed (§58 Win #3).
1080    layout_tree.subtree_needs_intrinsic = compute_subtree_needs_intrinsic(ctx.styled_dom, &layout_tree);
1081
1082    debug_log!(
1083        ctx,
1084        "Generated layout tree with {} nodes (incl. anonymous)",
1085        layout_tree.nodes.len()
1086    );
1087
1088    Ok(layout_tree)
1089}
1090
1091/// Returns true if `(dom_node_id, fc)` establishes a formatting context whose
1092/// sizing algorithm reads children's intrinsic sizes. Covers:
1093/// - flex containers (flex item sizing uses child min/max-content),
1094/// - grid containers (grid-track sizing likewise),
1095/// - tables and table cells,
1096/// - inline-block (its own width may be shrink-to-fit),
1097/// - floats and abspos elements (their `auto` width resolves to shrink-to-fit).
1098///
1099/// A `FormattingContext::Block` with a definite CSS width is NOT shrink-to-fit —
1100/// its inner layout gets the width top-down, so descendant intrinsics don't
1101/// feed back up. That's the path Fix C short-circuits.
1102pub(crate) fn is_shrink_to_fit_context(
1103    styled_dom: &StyledDom,
1104    dom_node_id: Option<NodeId>,
1105    fc: &FormattingContext,
1106) -> bool {
1107    use crate::solver3::getters::{get_float, MultiValue};
1108    use crate::solver3::positioning::get_position_type;
1109    use azul_css::props::layout::{LayoutFloat, LayoutPosition};
1110
1111    match fc {
1112        FormattingContext::Flex
1113        | FormattingContext::Grid
1114        | FormattingContext::Table
1115        | FormattingContext::InlineBlock => return true,
1116        _ => {}
1117    }
1118    let Some(dom_id) = dom_node_id else { return false; };
1119    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
1120    let float_val = match get_float(styled_dom, dom_id, node_state) {
1121        MultiValue::Exact(v) => v,
1122        _ => LayoutFloat::None,
1123    };
1124    if float_val != LayoutFloat::None {
1125        return true;
1126    }
1127    let pos = get_position_type(styled_dom, Some(dom_id));
1128    if pos == LayoutPosition::Absolute || pos == LayoutPosition::Fixed {
1129        // Abspos only becomes shrink-to-fit when width is `auto`.
1130        // Being conservative: treat as STF whenever abspos so we still
1131        // compute intrinsics for the auto-width case. Misses no work.
1132        return true;
1133    }
1134    false
1135}
1136
1137/// Per-node bitmap of "this node or any descendant establishes a shrink-to-fit
1138/// context." Post-order walk: `out[i] = self_stf(i) || any(out[child_of_i])`.
1139/// Layout tree nodes are built top-down (pre-order), so iterating from the end
1140/// visits children before parents.
1141fn compute_subtree_needs_intrinsic(
1142    styled_dom: &StyledDom,
1143    tree: &LayoutTree,
1144) -> Vec<bool> {
1145    let n = tree.nodes.len();
1146    let mut out = vec![false; n];
1147    for idx in (0..n).rev() {
1148        let hot = &tree.nodes[idx];
1149        let self_stf = is_shrink_to_fit_context(styled_dom, hot.dom_node_id, &hot.formatting_context);
1150        let mut any = self_stf;
1151        if !any {
1152            for &child in tree.children(idx) {
1153                if out.get(child).copied().unwrap_or(false) {
1154                    any = true;
1155                    break;
1156                }
1157            }
1158        }
1159        out[idx] = any;
1160    }
1161    out
1162}
1163
1164/// Incrementally builds a [`LayoutTree`] from a [`StyledDom`].
1165///
1166/// Usage: create via [`LayoutTreeBuilder::new`], call [`process_node`](Self::process_node)
1167/// on the root DOM node, then call [`build`](Self::build) to produce the final
1168/// SoA-split `LayoutTree`. During `process_node`, anonymous boxes are generated
1169/// as required by CSS 2.2 §9.2.1.1 (inline wrappers) and §17.2.1 (table fixup).
1170pub struct LayoutTreeBuilder {
1171    nodes: Vec<LayoutNode>,
1172    dom_to_layout: HashMap<NodeId, Vec<usize>>,
1173    viewport_size: LogicalSize,
1174}
1175
1176impl LayoutTreeBuilder {
1177    pub fn new(viewport_size: LogicalSize) -> Self {
1178        Self {
1179            nodes: Vec::new(),
1180            dom_to_layout: HashMap::new(),
1181            viewport_size,
1182        }
1183    }
1184
1185    pub fn get(&self, index: usize) -> Option<&LayoutNode> {
1186        self.nodes.get(index)
1187    }
1188
1189    pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNode> {
1190        self.nodes.get_mut(index)
1191    }
1192
1193    // +spec:display-property:2188b7 - builds box tree: each element's principal box is child of nearest ancestor's principal box, with anonymous boxes for tables/inline wrapping
1194    /// Main entry point for recursively building the layout tree.
1195    /// This function dispatches to specialized handlers based on the node's
1196    /// `display` property to correctly generate anonymous boxes.
1197    pub fn process_node(
1198        &mut self,
1199        styled_dom: &StyledDom,
1200        dom_id: NodeId,
1201        parent_idx: Option<usize>,
1202        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1203    ) -> Result<usize> {
1204        let node_data = &styled_dom.node_data.as_container()[dom_id];
1205        let node_idx = self.create_node_from_dom(styled_dom, dom_id, parent_idx, debug_messages);
1206        let raw_display = get_display_type(styled_dom, dom_id);
1207
1208        // +spec:display-property:042f56 - replaced elements with layout-internal display use inline
1209        // CSS Display 3 §2.4: "When the display property of a replaced element computes to
1210        // one of the layout-internal values, it is handled as having a used value of inline."
1211        let raw_display = if raw_display.is_layout_internal() && is_replaced_element(node_data) {
1212            LayoutDisplay::Inline
1213        } else {
1214            raw_display
1215        };
1216
1217        // +spec:display-property:0b40af - display/position/float interaction per CSS 2.2 §9.7
1218        // +spec:display-property:ba53ba - float!=none or position!=static causes display to blockify
1219        // +spec:positioning:69468c - absolute/fixed blockifies the box, float computes to none
1220        // +spec:table-layout:cfc60a - CSS 2.2 §9.7: display/position/float interaction
1221        // Blockification rules (CSS Display 3 §2.7 / §2.8):
1222        // 1. Root element → blockify
1223        // 2. position:absolute or position:fixed → float computes to 'none', blockify
1224        // 3. float is not 'none' → blockify
1225        // 4. Flex/Grid children → blockify
1226        let node_position = self.nodes.get(node_idx).map(|n| n.computed_style.position).unwrap_or_default();
1227        let node_float = self.nodes.get(node_idx).map(|n| n.computed_style.float).unwrap_or_default();
1228        let is_absolute_or_fixed = matches!(node_position, LayoutPosition::Absolute | LayoutPosition::Fixed);
1229        let is_floated = node_float != LayoutFloat::None;
1230        let is_root = parent_idx.is_none();
1231
1232        // Per CSS 2.2 §9.7: if position is absolute or fixed, float computes to 'none'
1233        if is_absolute_or_fixed && is_floated {
1234            if let Some(node) = self.nodes.get_mut(node_idx) {
1235                node.computed_style.float = LayoutFloat::None;
1236            }
1237        }
1238
1239        let is_flex_grid_child = parent_idx
1240            .and_then(|p| self.nodes.get(p).map(|n| matches!(n.formatting_context, FormattingContext::Flex | FormattingContext::Grid)))
1241            .unwrap_or(false);
1242
1243        let display_type = crate::solver3::getters::get_computed_display(
1244            raw_display, is_absolute_or_fixed, is_floated, is_root, is_flex_grid_child,
1245        );
1246
1247        // If blockification changed the display type, update the node's formatting context
1248        if display_type != raw_display {
1249            if let Some(node) = self.nodes.get_mut(node_idx) {
1250                node.computed_style.display = display_type;
1251                node.formatting_context = determine_formatting_context_for_display(
1252                    styled_dom, dom_id, display_type,
1253                );
1254            }
1255        }
1256
1257        // Compute containing block index for abs-pos clip exemption
1258        if is_absolute_or_fixed {
1259            let cb_index = if matches!(node_position, LayoutPosition::Fixed) {
1260                // Fixed elements: containing block is the root (viewport)
1261                None
1262            } else {
1263                // Absolute elements: containing block is nearest positioned ancestor
1264                let mut ancestor = parent_idx;
1265                loop {
1266                    match ancestor {
1267                        Some(idx) => {
1268                            let pos = self.nodes.get(idx)
1269                                .map(|n| n.computed_style.position)
1270                                .unwrap_or_default();
1271                            if pos.is_positioned() {
1272                                break Some(idx);
1273                            }
1274                            ancestor = self.nodes.get(idx).and_then(|n| n.parent);
1275                        }
1276                        None => break None, // root
1277                    }
1278                }
1279            };
1280            if let Some(node) = self.nodes.get_mut(node_idx) {
1281                node.containing_block_index = cb_index;
1282            }
1283        }
1284
1285        if parent_idx.is_none() {
1286            if let Some(node) = self.nodes.get_mut(node_idx) {
1287                if let FormattingContext::Block { ref mut establishes_new_context } = node.formatting_context {
1288                    *establishes_new_context = true;
1289                }
1290            }
1291        }
1292
1293        // +spec:display-property:1f4039 - list-item generates ::marker pseudo-element + principal box
1294        // +spec:display-property:2bb592 - list-item generates ::marker pseudo-element with list-style content
1295        // +spec:display-property:3b507e - list-item generates ::marker pseudo-element
1296        // +spec:display-property:a48f00 - additional boxes (marker, table wrapper) placed w.r.t. principal box
1297        // +spec:display-property:998063 - list-item generates principal block box + marker box
1298        // If this is a list-item, inject a ::marker pseudo-element as its first child
1299        // +spec:display-property:a42905 - list-item generates ::marker pseudo-element with list-style content, principal box outer=block inner=flow
1300        if display_type == LayoutDisplay::ListItem {
1301            self.create_marker_pseudo_element(styled_dom, dom_id, node_idx);
1302        }
1303
1304        // +spec:display-contents:376f2e - display:contents removes principal box, children render normally
1305        // +spec:display-contents:3c7066 - display:contents strips element from formatting tree, hoists children
1306        // +spec:display-contents:3f4884 - replaced elements / form controls not specially handled yet (spec note: use display:none instead)
1307        // +spec:display-contents:4f9129 - semantic container role preserved: children promoted but DOM structure unchanged
1308        // +spec:display-contents:7558e8 - display:contents is rendering-time only; DOM relationships unaffected
1309        // +spec:display-contents:a079e3 - display:contents generates no box; children promoted to nearest non-contents ancestor (writing-mode parent lookup skips these)
1310        // +spec:display-contents:e202d5 - display:contents removes principal box, children render as normal
1311        // +spec:display-contents:6bbdf4 - display:contents preserves semantic container role (visibility context)
1312        // +spec:display-property:d7a8de - display:none/contents elements generate no box; anonymous box generation ignores them
1313        // +spec:display-property:dc2132 - display:none and display:contents control box generation
1314        // display:contents - element generates no box; promote children to parent
1315        // +spec:display-contents:61992e - element itself generates no boxes, children promoted to parent
1316        // +spec:display-contents:af8feb - treated as if replaced in element tree by its contents
1317        // +spec:display-contents:353e71 - display:contents box generation behavior
1318        // +spec:display-contents:b0a76b - display:contents generates no box; children promoted to parent
1319        // +spec:display-property:e370af - display:contents generates no box; children promoted to parent
1320        //
1321        // +spec:display-contents:852a59 - display:contents computes to display:none for replaced elements
1322        // +spec:display-contents:4a524e - display:contents computes to display:none on replaced elements
1323        // +spec:replaced-elements:af1e68 - display:contents on replaced elements has no effect (element renders normally)
1324        // Per CSS Display 3 §2.5 / Appendix B: replaced elements (img, canvas, embed, object,
1325        // audio, iframe, video, input, textarea, select, br, wbr, meter, progress)
1326        // and similar cannot be "un-boxed" — display:contents becomes display:none.
1327        if display_type == LayoutDisplay::Contents && is_replaced_element(node_data) {
1328            // Treat as display:none — remove node from parent and skip children
1329            if let Some(parent) = parent_idx {
1330                if let Some(p) = self.nodes.get_mut(parent) {
1331                    p.children.retain(|&c| c != node_idx);
1332                }
1333            }
1334            if let Some(node) = self.nodes.get_mut(node_idx) {
1335                node.computed_style.display = LayoutDisplay::None;
1336                node.formatting_context = FormattingContext::None;
1337            }
1338            return Ok(node_idx);
1339        }
1340
1341        if display_type == LayoutDisplay::Contents {
1342            // Remove the node we just created — it shouldn't generate a box
1343            if let Some(parent) = parent_idx {
1344                if let Some(p) = self.nodes.get_mut(parent) {
1345                    p.children.retain(|&c| c != node_idx);
1346                }
1347            }
1348            // Process children as if they belong to the parent (or root if no parent)
1349            let effective_parent = parent_idx.unwrap_or(node_idx);
1350            for child_dom_id in dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1351                self.process_node(styled_dom, child_dom_id, Some(effective_parent), debug_messages)?;
1352            }
1353            return Ok(node_idx);
1354        }
1355
1356        match display_type {
1357            LayoutDisplay::Block
1358            | LayoutDisplay::InlineBlock
1359            | LayoutDisplay::FlowRoot
1360            | LayoutDisplay::ListItem => {
1361                self.process_block_children(styled_dom, dom_id, node_idx, debug_messages)?
1362            }
1363            // +spec:table-layout:d52e09 - display:table/inline-table cause element to behave like a table element
1364            // +spec:table-layout:360da0 - table display values cause table formatting behavior
1365            LayoutDisplay::Table | LayoutDisplay::InlineTable => {
1366                self.process_table_children(styled_dom, dom_id, node_idx, debug_messages)?
1367            }
1368            LayoutDisplay::TableRowGroup
1369            | LayoutDisplay::TableHeaderGroup
1370            | LayoutDisplay::TableFooterGroup => {
1371                self.process_table_row_group_children(styled_dom, dom_id, node_idx, debug_messages)?
1372            }
1373            LayoutDisplay::TableRow => {
1374                self.process_table_row_children(styled_dom, dom_id, node_idx, debug_messages)?
1375            }
1376            LayoutDisplay::TableColumn => {
1377                // +spec:table-layout:77974f - Stage 1: all children of table-column treated as display:none
1378                // +spec:table-layout:c8dc69 - Stage 1: remove irrelevant boxes from table-column
1379                // CSS 2.2 §17.2.1: "All child boxes of a 'table-column' parent are
1380                // treated as if they had 'display: none'." - skip all children.
1381            }
1382            LayoutDisplay::TableColumnGroup => {
1383                // CSS 2.2 §17.2.1: "If a child C of a 'table-column-group' parent is not
1384                // a 'table-column' box, then it is treated as if it had 'display: none'."
1385                for child_dom_id in dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1386                    let child_display = get_display_type(styled_dom, child_dom_id);
1387                    if child_display == LayoutDisplay::TableColumn {
1388                        self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
1389                    }
1390                    // Non-table-column children are suppressed (treated as display:none)
1391                }
1392            }
1393            // Inline, TableCell, etc., have their children processed as part of their
1394            // formatting context layout and don't require anonymous box generation at this stage.
1395            // of table-internal display values is handled via blockify_flex_item_if_table_internal
1396            _ => {
1397                // +spec:display-contents:34008d - display:none elements generate no boxes; excluded from formatting structure
1398                // +spec:display-property:1f38b2 - display:none creates no box at all, filter from layout tree
1399                // +spec:display-property:eb53f7 - display:none suppresses box generation; visibility:hidden boxes still affect layout
1400                // Filter out display: none children - they don't participate in layout
1401                // +spec:display-property:d1600a - display:none suppresses box generation; visibility:hidden boxes still affect layout
1402                // ALSO filter out whitespace-only text nodes for Flex/Grid/etc containers
1403                // to prevent them from becoming unwanted anonymous items.
1404                let children: Vec<NodeId> = dom_id
1405                    .az_children(&styled_dom.node_hierarchy.as_container())
1406                    // +spec:display-property:9f02c6 - display:none elements generate no boxes
1407                    .filter(|&child_id| {
1408                        // +spec:display-property:3b507e - display:none excludes subtree from box tree
1409                        if get_display_type(styled_dom, child_id) == LayoutDisplay::None {
1410                            return false;
1411                        }
1412                        // Check for whitespace-only text
1413                        let node_data = &styled_dom.node_data.as_container()[child_id];
1414                        if let NodeType::Text(text) = node_data.get_node_type() {
1415                            // Skip if text is empty or just whitespace
1416                            return !text.as_str().trim().is_empty();
1417                        }
1418                        true
1419                    })
1420                    .collect();
1421
1422                let is_flex_or_grid = matches!(
1423                    display_type,
1424                    LayoutDisplay::Flex | LayoutDisplay::InlineFlex
1425                    | LayoutDisplay::Grid | LayoutDisplay::InlineGrid
1426                );
1427
1428                for child_dom_id in children {
1429                    // +spec:display-property:934c84 - table wrapper box generation: display:table/inline-table generates a principal block container (table wrapper box) that establishes BFC and contains the table box + caption boxes
1430                    // +spec:width-calculation:59d456 - table wrapper box is block-level, establishes BFC (CSS 2.2 §17.4)
1431                    // the table wrapper box becomes the flex item; align-self applies to the
1432                    // wrapper, flex longhands apply to the inner table box, caption contents
1433                    // contribute to wrapper min/max-content sizes
1434                    let child_display = get_display_type(styled_dom, child_dom_id);
1435                    if is_flex_or_grid && child_display.creates_table_context() {
1436                        let wrapper_idx = self.create_anonymous_node(
1437                            node_idx,
1438                            AnonymousBoxType::TableWrapper,
1439                            FormattingContext::Block { establishes_new_context: true },
1440                        );
1441                        self.process_node(styled_dom, child_dom_id, Some(wrapper_idx), debug_messages)?;
1442                    } else {
1443                        let child_idx = self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
1444                        // table-internal flex items are blockified, preventing anonymous table
1445                        // box generation (e.g. two display:table-cell flex items become two
1446                        // separate display:block flex items)
1447                        if is_flex_or_grid {
1448                            blockify_flex_item_if_table_internal(&mut self.nodes, child_idx);
1449                        }
1450                    }
1451                }
1452            }
1453        }
1454        Ok(node_idx)
1455    }
1456
1457    // +spec:display-property:5572e7 - Anonymous block boxes: wrap inline runs when block container has mixed block/inline children
1458    // +spec:display-property:090043 - Anonymous block box properties inherited from enclosing non-anonymous box; non-inherited props get initial values
1459    // +spec:display-property:7b9f7a - Block-level vs inline-level classification and anonymous block box creation
1460    // +spec:display-property:078fe5 - Anonymous block boxes wrapping inline content in mixed block/inline contexts
1461    // +spec:display-property:8d8ef3 - block container anonymous box generation: wraps inline runs in anonymous block boxes to ensure block containers contain only block-level or only inline-level boxes
1462    // +spec:display-property:1fe2be - inline box construction with anonymous text interspersed with inline elements
1463    // +spec:display-property:be80e3 - Anonymous inline boxes: text in block containers treated as anonymous inlines, whitespace-only runs collapsed
1464    /// Handles children of a block-level element, creating anonymous block
1465    /// wrappers for consecutive runs of inline-level children if necessary.
1466    // +spec:display-property:b73c50 - blockify inline content by wrapping in anonymous block containers
1467    fn process_block_children(
1468        &mut self,
1469        styled_dom: &StyledDom,
1470        parent_dom_id: NodeId,
1471        parent_idx: usize,
1472        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1473    ) -> Result<()> {
1474        // Filter out display: none children - they don't participate in layout
1475        let children: Vec<NodeId> = parent_dom_id
1476            .az_children(&styled_dom.node_hierarchy.as_container())
1477            .filter(|&child_id| get_display_type(styled_dom, child_id) != LayoutDisplay::None)
1478            .collect();
1479
1480        // Debug: log which children we found
1481        if let Some(msgs) = debug_messages.as_mut() {
1482            msgs.push(LayoutDebugMessage::info(format!(
1483                "[process_block_children] DOM node {} has {} children: {:?}",
1484                parent_dom_id.index(),
1485                children.len(),
1486                children.iter().map(|c| c.index()).collect::<Vec<_>>()
1487            )));
1488        }
1489
1490        let has_block_child = children.iter().any(|&id| is_block_level(styled_dom, id));
1491
1492        if let Some(msgs) = debug_messages.as_mut() {
1493            msgs.push(LayoutDebugMessage::info(format!(
1494                "[process_block_children] has_block_child={}, children display types: {:?}",
1495                has_block_child,
1496                children
1497                    .iter()
1498                    .map(|c| {
1499                        let dt = get_display_type(styled_dom, *c);
1500                        let is_block = is_block_level(styled_dom, *c);
1501                        format!("{}:{:?}(block={})", c.index(), dt, is_block)
1502                    })
1503                    .collect::<Vec<_>>()
1504            )));
1505        }
1506
1507        if !has_block_child {
1508            // All children are inline, no anonymous boxes needed.
1509            if let Some(msgs) = debug_messages.as_mut() {
1510                msgs.push(LayoutDebugMessage::info(format!(
1511                    "[process_block_children] All inline, processing {} children directly",
1512                    children.len()
1513                )));
1514            }
1515            for child_id in children {
1516                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1517            }
1518            return Ok(());
1519        }
1520
1521        // Mixed block and inline content requires anonymous wrappers.
1522        let mut inline_run = Vec::new();
1523
1524        for child_id in children {
1525            if is_block_level(styled_dom, child_id) {
1526                // +spec:display-contents:02a534 - contiguous text sequences with no text don't generate boxes
1527                // End the current inline run — but skip if all nodes are whitespace-only text.
1528                // +spec:display-property:7d1570 - whitespace-only text that would be collapsed does not generate anonymous inline boxes
1529                // +spec:white-space-processing:b32f69 - whitespace-only inline runs between blocks don't generate anonymous inline boxes
1530                // CSS 2.1 §9.2.2.1: "White space content that would subsequently be collapsed
1531                // away according to the 'white-space' property does not generate any anonymous
1532                // inline boxes."
1533                if !inline_run.is_empty() {
1534                    let all_whitespace = inline_run
1535                        .iter()
1536                        .all(|id| is_whitespace_only_text(styled_dom, *id));
1537                    if all_whitespace {
1538                        if let Some(msgs) = debug_messages.as_mut() {
1539                            msgs.push(LayoutDebugMessage::info(format!(
1540                                "[process_block_children] Skipping whitespace-only inline run between blocks: {:?}",
1541                                inline_run.iter().map(|c: &NodeId| c.index()).collect::<Vec<_>>()
1542                            )));
1543                        }
1544                        inline_run.clear();
1545                    } else {
1546                        if let Some(msgs) = debug_messages.as_mut() {
1547                            msgs.push(LayoutDebugMessage::info(format!(
1548                                "[process_block_children] Creating anon wrapper for inline run: {:?}",
1549                                inline_run
1550                                    .iter()
1551                                    .map(|c: &NodeId| c.index())
1552                                    .collect::<Vec<_>>()
1553                            )));
1554                        }
1555                        let anon_idx = self.create_anonymous_node(
1556                            parent_idx,
1557                            AnonymousBoxType::InlineWrapper,
1558                            FormattingContext::Block {
1559                                // Anonymous wrappers are BFC roots
1560                                establishes_new_context: true,
1561                            },
1562                        );
1563                        for inline_child_id in inline_run.drain(..) {
1564                            self.process_node(
1565                                styled_dom,
1566                                inline_child_id,
1567                                Some(anon_idx),
1568                                debug_messages,
1569                            )?;
1570                        }
1571                    }
1572                }
1573                // Process the block-level child directly
1574                if let Some(msgs) = debug_messages.as_mut() {
1575                    msgs.push(LayoutDebugMessage::info(format!(
1576                        "[process_block_children] Processing block child DOM {}",
1577                        child_id.index()
1578                    )));
1579                }
1580                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1581            } else {
1582                inline_run.push(child_id);
1583            }
1584        }
1585        // Process any remaining inline children at the end — skip if all whitespace
1586        if !inline_run.is_empty() {
1587            let all_whitespace = inline_run
1588                .iter()
1589                .all(|id| is_whitespace_only_text(styled_dom, *id));
1590            if all_whitespace {
1591                if let Some(msgs) = debug_messages.as_mut() {
1592                    msgs.push(LayoutDebugMessage::info(format!(
1593                        "[process_block_children] Skipping trailing whitespace-only inline run: {:?}",
1594                        inline_run.iter().map(|c| c.index()).collect::<Vec<_>>()
1595                    )));
1596                }
1597            } else {
1598                if let Some(msgs) = debug_messages.as_mut() {
1599                    msgs.push(LayoutDebugMessage::info(format!(
1600                        "[process_block_children] Creating anon wrapper for remaining inline run: {:?}",
1601                        inline_run.iter().map(|c| c.index()).collect::<Vec<_>>()
1602                    )));
1603                }
1604                let anon_idx = self.create_anonymous_node(
1605                    parent_idx,
1606                    AnonymousBoxType::InlineWrapper,
1607                    FormattingContext::Block {
1608                        establishes_new_context: true, // Anonymous wrappers are BFC roots
1609                    },
1610                );
1611                for inline_child_id in inline_run {
1612                    self.process_node(
1613                        styled_dom,
1614                        inline_child_id,
1615                        Some(anon_idx),
1616                        debug_messages,
1617                    )?;
1618                }
1619            }
1620        }
1621
1622        Ok(())
1623    }
1624
1625    // +spec:table-layout:6bb84e - Anonymous table object generation (stages 1-3: remove irrelevant boxes, generate missing child wrappers, generate missing parents)
1626    // +spec:table-layout:77974f - Stage 2: generate missing child wrappers for table/inline-table
1627    // +spec:table-layout:c8dc69 - Stage 2: wrap non-proper children in anonymous table-row
1628    /// CSS 2.2 Section 17.2.1 - Anonymous box generation for tables:
1629    /// "If a child C of a 'table' or 'inline-table' box is not a proper table child,
1630    /// then generate an anonymous 'table-row' box around C and all consecutive
1631    /// siblings of C that are not proper table children."
1632    ///
1633    // +spec:display-property:6f8f13 - anonymous table object generation (§17.2.1): suppress table-column/table-column-group children, wrap non-proper children in anonymous rows/cells
1634    /// Proper table children are: table-row-group, table-header-group,
1635    /// table-footer-group, table-row, table-column-group, table-column, table-caption.
1636    fn process_table_children(
1637        &mut self,
1638        styled_dom: &StyledDom,
1639        parent_dom_id: NodeId,
1640        parent_idx: usize,
1641        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1642    ) -> Result<()> {
1643        let parent_display = get_display_type(styled_dom, parent_dom_id);
1644        let mut non_proper_children = Vec::new();
1645
1646        for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1647            // CSS 2.2 Section 17.2.1, Stage 1: Skip whitespace-only text nodes
1648            if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
1649                continue;
1650            }
1651
1652            let child_display = get_display_type(styled_dom, child_id);
1653
1654            if is_proper_table_child(child_display) {
1655                // Flush any accumulated non-proper children into an anonymous table-row
1656                if !non_proper_children.is_empty() {
1657                    let anon_row_idx = self.create_anonymous_node(
1658                        parent_idx,
1659                        AnonymousBoxType::TableRow,
1660                        FormattingContext::TableRow,
1661                    );
1662
1663                    for np_id in non_proper_children.drain(..) {
1664                        self.process_node(styled_dom, np_id, Some(anon_row_idx), debug_messages)?;
1665                    }
1666                }
1667
1668                // Process proper table child directly (row, row-group, caption, etc.)
1669                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1670            } else {
1671                // Non-proper table child: accumulate for wrapping
1672                non_proper_children.push(child_id);
1673            }
1674        }
1675
1676        // Flush any remaining accumulated non-proper children
1677        if !non_proper_children.is_empty() {
1678            let anon_row_idx = self.create_anonymous_node(
1679                parent_idx,
1680                AnonymousBoxType::TableRow,
1681                FormattingContext::TableRow,
1682            );
1683
1684            for np_id in non_proper_children {
1685                self.process_node(styled_dom, np_id, Some(anon_row_idx), debug_messages)?;
1686            }
1687        }
1688
1689        Ok(())
1690    }
1691
1692    /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
1693    /// "If a child C of a row group box is not a 'table-row' box, then generate
1694    /// an anonymous 'table-row' box around C and all consecutive siblings of C
1695    /// that are not 'table-row' boxes."
1696    fn process_table_row_group_children(
1697        &mut self,
1698        styled_dom: &StyledDom,
1699        parent_dom_id: NodeId,
1700        parent_idx: usize,
1701        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1702    ) -> Result<()> {
1703        let parent_display = get_display_type(styled_dom, parent_dom_id);
1704        let mut non_row_children = Vec::new();
1705
1706        for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1707            if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
1708                continue;
1709            }
1710
1711            let child_display = get_display_type(styled_dom, child_id);
1712
1713            if child_display == LayoutDisplay::TableRow {
1714                // Flush accumulated non-row children into anonymous row
1715                if !non_row_children.is_empty() {
1716                    let anon_row_idx = self.create_anonymous_node(
1717                        parent_idx,
1718                        AnonymousBoxType::TableRow,
1719                        FormattingContext::TableRow,
1720                    );
1721                    for nr_id in non_row_children.drain(..) {
1722                        self.process_node(styled_dom, nr_id, Some(anon_row_idx), debug_messages)?;
1723                    }
1724                }
1725                // Process table-row child directly
1726                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1727            } else {
1728                non_row_children.push(child_id);
1729            }
1730        }
1731
1732        // Flush remaining
1733        if !non_row_children.is_empty() {
1734            let anon_row_idx = self.create_anonymous_node(
1735                parent_idx,
1736                AnonymousBoxType::TableRow,
1737                FormattingContext::TableRow,
1738            );
1739            for nr_id in non_row_children {
1740                self.process_node(styled_dom, nr_id, Some(anon_row_idx), debug_messages)?;
1741            }
1742        }
1743
1744        Ok(())
1745    }
1746
1747    /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
1748    /// "If a child C of a 'table-row' box is not a 'table-cell', then generate an
1749    /// anonymous 'table-cell' box around C and all consecutive siblings of C that
1750    /// are not 'table-cell' boxes."
1751    fn process_table_row_children(
1752        &mut self,
1753        styled_dom: &StyledDom,
1754        parent_dom_id: NodeId,
1755        parent_idx: usize,
1756        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1757    ) -> Result<()> {
1758        let parent_display = get_display_type(styled_dom, parent_dom_id);
1759        let mut non_cell_children = Vec::new();
1760
1761        for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1762            if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
1763                continue;
1764            }
1765
1766            let child_display = get_display_type(styled_dom, child_id);
1767
1768            if child_display == LayoutDisplay::TableCell {
1769                // Flush accumulated non-cell children into one anonymous table-cell
1770                if !non_cell_children.is_empty() {
1771                    let anon_cell_idx = self.create_anonymous_node(
1772                        parent_idx,
1773                        AnonymousBoxType::TableCell,
1774                        FormattingContext::Block {
1775                            establishes_new_context: true,
1776                        },
1777                    );
1778                    for nc_id in non_cell_children.drain(..) {
1779                        self.process_node(styled_dom, nc_id, Some(anon_cell_idx), debug_messages)?;
1780                    }
1781                }
1782                // Process table-cell child directly
1783                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1784            } else {
1785                // Accumulate consecutive non-cell children
1786                non_cell_children.push(child_id);
1787            }
1788        }
1789
1790        // Flush remaining non-cell children
1791        if !non_cell_children.is_empty() {
1792            let anon_cell_idx = self.create_anonymous_node(
1793                parent_idx,
1794                AnonymousBoxType::TableCell,
1795                FormattingContext::Block {
1796                    establishes_new_context: true,
1797                },
1798            );
1799            for nc_id in non_cell_children {
1800                self.process_node(styled_dom, nc_id, Some(anon_cell_idx), debug_messages)?;
1801            }
1802        }
1803
1804        Ok(())
1805    }
1806    // +spec:display-property:52f497 - anonymous inline boxes inherit inheritable properties from block parent; non-inherited properties use initial values (dom_node_id: None + BoxProps::default())
1807    /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
1808    /// "In this process, inline-level boxes are wrapped in anonymous boxes as needed
1809    /// to satisfy the constraints of the table model."
1810    ///
1811    // +spec:display-property:ee83bf - Anonymous box generation: boxes not associated with elements, inheriting through box tree parentage
1812    /// Helper to create an anonymous node in the tree.
1813    /// Anonymous boxes don't have a corresponding DOM node and are used to enforce
1814    /// the CSS box model structure (e.g., wrapping inline content in blocks,
1815    /// or creating missing table structural elements).
1816    // +spec:display-property:6ff51a - anonymous block boxes have no styles (box_props default), so parent element properties still apply to its content
1817    pub fn create_anonymous_node(
1818        &mut self,
1819        parent: usize,
1820        anon_type: AnonymousBoxType,
1821        fc: FormattingContext,
1822    ) -> usize {
1823        let index = self.nodes.len();
1824
1825        // +spec:display-property:e67146 - Anonymous boxes inherit from enclosing non-anonymous box; non-inherited props use initial values
1826        let parent_fc = self.nodes.get(parent).map(|n| n.formatting_context.clone());
1827
1828        self.nodes.push(LayoutNode {
1829            // ── HOT ──
1830            box_props: BoxProps::default(),
1831            dom_node_id: None,
1832            children: Vec::new(),
1833            used_size: None,
1834            formatting_context: fc,
1835            parent: Some(parent),
1836            // ── WARM ──
1837            intrinsic_sizes: None,
1838            baseline: None,
1839            inline_layout_result: None,
1840            scrollbar_info: None,
1841            relative_position: None,
1842            overflow_content_size: None,
1843            taffy_cache: TaffyCache::new(),
1844            computed_style: ComputedLayoutStyle::default(),
1845            pseudo_element: None,
1846            escaped_top_margin: None,
1847            escaped_bottom_margin: None,
1848            parent_formatting_context: parent_fc,
1849            ifc_membership: None,
1850            containing_block_index: None,
1851            // ── COLD ──
1852            anonymous_type: Some(anon_type),
1853            node_data_fingerprint: NodeDataFingerprint::default(),
1854            subtree_hash: SubtreeHash(0),
1855            dirty_flag: DirtyFlag::Layout,
1856            unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
1857            ifc_id: None,
1858        });
1859
1860        self.nodes[parent].children.push(index);
1861        index
1862    }
1863
1864    /// Creates a ::marker pseudo-element as the first child of a list-item.
1865    ///
1866    /// Per CSS Lists Module Level 3, Section 3.1:
1867    /// "For elements with display: list-item, user agents must generate a
1868    /// ::marker pseudo-element as the first child of the principal box."
1869    ///
1870    /// The ::marker references the same DOM node as its parent list-item,
1871    /// but is marked as a pseudo-element for proper counter resolution and styling.
1872    pub fn create_marker_pseudo_element(
1873        &mut self,
1874        styled_dom: &StyledDom,
1875        list_item_dom_id: NodeId,
1876        list_item_idx: usize,
1877    ) -> usize {
1878        let index = self.nodes.len();
1879
1880        // The marker references the same DOM node as the list-item
1881        // This is important for style resolution (the marker inherits from the list-item)
1882        let parent_fc = self
1883            .nodes
1884            .get(list_item_idx)
1885            .map(|n| n.formatting_context.clone());
1886        self.nodes.push(LayoutNode {
1887            // ── HOT ──
1888            box_props: BoxProps::default(),
1889            dom_node_id: Some(list_item_dom_id),
1890            children: Vec::new(),
1891            used_size: None,
1892            formatting_context: FormattingContext::Inline,
1893            parent: Some(list_item_idx),
1894            // ── WARM ──
1895            intrinsic_sizes: None,
1896            baseline: None,
1897            inline_layout_result: None,
1898            scrollbar_info: None,
1899            relative_position: None,
1900            overflow_content_size: None,
1901            taffy_cache: TaffyCache::new(),
1902            computed_style: ComputedLayoutStyle::default(),
1903            pseudo_element: Some(PseudoElement::Marker),
1904            escaped_top_margin: None,
1905            escaped_bottom_margin: None,
1906            parent_formatting_context: parent_fc,
1907            ifc_membership: None,
1908            containing_block_index: None,
1909            // ── COLD ──
1910            anonymous_type: None,
1911            node_data_fingerprint: NodeDataFingerprint::default(),
1912            subtree_hash: SubtreeHash(0),
1913            dirty_flag: DirtyFlag::Layout,
1914            unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
1915            ifc_id: None,
1916        });
1917
1918        // Insert as FIRST child (per spec)
1919        self.nodes[list_item_idx].children.insert(0, index);
1920
1921        // Register with DOM mapping for counter resolution
1922        self.dom_to_layout
1923            .entry(list_item_dom_id)
1924            .or_default()
1925            .push(index);
1926
1927        index
1928    }
1929
1930    // M12.7: returns `usize`, NOT `Result<usize>` — this fn has no error path
1931    // (always `Ok(index)`). The `Result` forced callers to use `?`, whose lifted
1932    // discriminant decode mis-reads the Ok as Err (the rc=5 root cause: reconcile
1933    // reaches this fn but returns Err before its own Ok). Dropping the Result
1934    // removes that mis-lifting `?`.
1935    pub fn create_node_from_dom(
1936        &mut self,
1937        styled_dom: &StyledDom,
1938        dom_id: NodeId,
1939        parent: Option<usize>,
1940        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1941    ) -> usize {
1942        let index = self.nodes.len();
1943        // M12.7 diag: 0x400B4 = create_node_from_dom's pre-push index (= nodes.len()
1944        // as IT sees it). If this is 0 but build() sees 0 nodes, the push is lost
1945        // between here and build (builder &mut threading); if garbage, len mis-reads.
1946        unsafe { core::ptr::write_volatile(0x400B4 as *mut u32, 0xCE00_0000u32 | (index as u32 & 0xffff)); }
1947        let parent_fc =
1948            parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context.clone()));
1949        // M12.7 diag: 0x400CC = parent.and_then done (Option<usize> discriminant). If
1950        // this is reached but step A is NOT, collect_box_props diverges; if this is
1951        // NOT reached, the parent Option discriminant mis-lifts (None→Some garbage).
1952        unsafe { core::ptr::write_volatile(0x400CC as *mut u32, 0xCD00_0001u32 | ((parent_fc.is_some() as u32) << 8)); }
1953        let collected = collect_box_props(styled_dom, dom_id, debug_messages, self.viewport_size);
1954        // M12.7 diag: 0x400C0 = collect_box_props returned (step A).
1955        unsafe { core::ptr::write_volatile(0x400C0 as *mut u32, 0xCA00_0001u32); }
1956        self.nodes.push(LayoutNode {
1957            // ── HOT ──
1958            box_props: collected.resolved,
1959            dom_node_id: Some(dom_id),
1960            children: Vec::new(),
1961            used_size: None,
1962            formatting_context: determine_formatting_context(styled_dom, dom_id),
1963            parent,
1964            // ── WARM ──
1965            intrinsic_sizes: None,
1966            baseline: None,
1967            inline_layout_result: None,
1968            scrollbar_info: None,
1969            relative_position: None,
1970            overflow_content_size: None,
1971            taffy_cache: TaffyCache::new(),
1972            // +spec:overflow:8f9f7e - viewport overflow propagation: visible→auto, clip→hidden
1973            computed_style: {
1974                let mut style = compute_layout_style(styled_dom, dom_id);
1975                if parent.is_none() {
1976                    // CSS Overflow 3 §3.3: If visible is applied to the viewport,
1977                    // it must be interpreted as auto. If clip is applied to the
1978                    // viewport, it must be interpreted as hidden.
1979                    use azul_css::props::layout::LayoutOverflow;
1980                    if style.overflow_x == LayoutOverflow::Visible {
1981                        style.overflow_x = LayoutOverflow::Auto;
1982                    } else if style.overflow_x == LayoutOverflow::Clip {
1983                        style.overflow_x = LayoutOverflow::Hidden;
1984                    }
1985                    if style.overflow_y == LayoutOverflow::Visible {
1986                        style.overflow_y = LayoutOverflow::Auto;
1987                    } else if style.overflow_y == LayoutOverflow::Clip {
1988                        style.overflow_y = LayoutOverflow::Hidden;
1989                    }
1990                }
1991                style
1992            },
1993            pseudo_element: None,
1994            escaped_top_margin: None,
1995            escaped_bottom_margin: None,
1996            parent_formatting_context: parent_fc,
1997            ifc_membership: None,
1998            containing_block_index: None,
1999            // ── COLD ──
2000            anonymous_type: None,
2001            node_data_fingerprint: NodeDataFingerprint::compute(
2002                &styled_dom.node_data.as_container()[dom_id],
2003                styled_dom.styled_nodes.as_container().get(dom_id).map(|n| &n.styled_node_state),
2004            ),
2005            subtree_hash: SubtreeHash(0),
2006            dirty_flag: DirtyFlag::Layout,
2007            unresolved_box_props: collected.unresolved,
2008            ifc_id: None,
2009        });
2010        // M12.7 diag: 0x400C4 = LayoutNode literal + self.nodes.push done (step B).
2011        unsafe { core::ptr::write_volatile(0x400C4 as *mut u32, 0xCB00_0001u32 | ((self.nodes.len() as u32 & 0xff) << 8)); }
2012        if let Some(p) = parent {
2013            self.nodes[p].children.push(index);
2014        }
2015        self.dom_to_layout.entry(dom_id).or_default().push(index);
2016        // M12.7 diag: 0x400B8 = nodes.len() AFTER the push (should be index+1).
2017        unsafe { core::ptr::write_volatile(0x400B8 as *mut u32, 0xCF00_0000u32 | (self.nodes.len() as u32 & 0xffff)); }
2018        index
2019    }
2020
2021    pub fn clone_node_from_old(&mut self, old_node: &LayoutNode, parent: Option<usize>) -> usize {
2022        let index = self.nodes.len();
2023        let mut new_node = old_node.clone();
2024        new_node.parent = parent;
2025        new_node.parent_formatting_context =
2026            parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context.clone()));
2027        new_node.children = Vec::new();
2028        new_node.dirty_flag = DirtyFlag::None;
2029        self.nodes.push(new_node);
2030        if let Some(p) = parent {
2031            self.nodes[p].children.push(index);
2032        }
2033        if let Some(dom_id) = old_node.dom_node_id {
2034            self.dom_to_layout.entry(dom_id).or_default().push(index);
2035        }
2036        index
2037    }
2038
2039    pub fn build(self, root_idx: usize) -> LayoutTree {
2040        let nodes = self.nodes;
2041        let node_count = nodes.len();
2042
2043        // Flatten per-node children Vecs into a single contiguous arena.
2044        let total_children: usize = nodes.iter().map(|n| n.children.len()).sum();
2045        let mut arena = Vec::with_capacity(total_children);
2046        let mut offsets = Vec::with_capacity(node_count);
2047
2048        // Split monolithic LayoutNodes into hot/warm/cold SoA arrays
2049        let mut hot_nodes = Vec::with_capacity(node_count);
2050        let mut warm_nodes = Vec::with_capacity(node_count);
2051        let mut cold_nodes = Vec::with_capacity(node_count);
2052
2053        for node in nodes {
2054            // Flatten children into arena first
2055            let start = arena.len() as u32;
2056            let len = node.children.len() as u32;
2057            arena.extend_from_slice(&node.children);
2058            offsets.push((start, len));
2059
2060            // Split into hot/warm/cold
2061            let (hot, warm, cold) = node.split();
2062            hot_nodes.push(hot);
2063            warm_nodes.push(warm);
2064            cold_nodes.push(cold);
2065        }
2066
2067        // M12.7 diag: 0x400B0 = 0xBD00_<len><root> — plain field reads (NOT a
2068        // discriminant). If len>0 but calculate_intrinsic_recursive's
2069        // `tree.get(root).ok_or(InvalidTree)?` still errors, that `?`/null-check
2070        // mis-discriminates Some→None. If len==0, build's input was empty.
2071        unsafe {
2072            core::ptr::write_volatile(
2073                0x400B0 as *mut u32,
2074                0xBD00_0000u32 | (((hot_nodes.len() as u32) & 0xff) << 8) | (root_idx as u32 & 0xff),
2075            );
2076        }
2077
2078        LayoutTree {
2079            nodes: hot_nodes,
2080            warm: warm_nodes,
2081            cold: cold_nodes,
2082            root: root_idx,
2083            dom_to_layout: self.dom_to_layout,
2084            children_arena: arena,
2085            children_offsets: offsets,
2086            // Populated by `generate_layout_tree` after the tree is built,
2087            // since the computation needs styled_dom for float/position lookup.
2088            subtree_needs_intrinsic: Vec::new(),
2089        }
2090    }
2091}
2092
2093// +spec:display-property:697082 - outer display type determines principal box's role in flow layout (block vs inline)
2094// +spec:display-property:0d251b - Block-level elements: display 'block', 'list-item', 'table' generate block-level boxes
2095// +spec:display-property:9464be - block-level vs block container distinction: not all block-level boxes are block containers (e.g. replaced elements, flex containers)
2096pub fn is_block_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2097    matches!(
2098        get_display_type(styled_dom, node_id),
2099        LayoutDisplay::Block
2100            | LayoutDisplay::FlowRoot
2101            | LayoutDisplay::Flex
2102            | LayoutDisplay::Grid
2103            | LayoutDisplay::Table
2104            | LayoutDisplay::TableCaption
2105            | LayoutDisplay::TableRow
2106            | LayoutDisplay::TableRowGroup
2107            | LayoutDisplay::TableHeaderGroup
2108            | LayoutDisplay::TableFooterGroup
2109            | LayoutDisplay::TableCell
2110            | LayoutDisplay::ListItem
2111    )
2112}
2113
2114// +spec:display-property:23f111 - Inline-level elements: inline, inline-block, inline-table, inline-flex, inline-grid
2115/// Checks if a node is inline-level (including text nodes).
2116/// According to CSS spec, inline-level content includes:
2117///
2118/// - Elements with display: inline, inline-block, inline-table, inline-flex, inline-grid
2119/// - Text nodes
2120/// - Generated content
2121fn is_inline_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2122    // Text nodes are always inline-level
2123    let node_data = &styled_dom.node_data.as_container()[node_id];
2124    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
2125        return true;
2126    }
2127
2128    // Check the display property
2129    matches!(
2130        get_display_type(styled_dom, node_id),
2131        LayoutDisplay::Inline
2132            | LayoutDisplay::InlineBlock
2133            | LayoutDisplay::InlineTable
2134            | LayoutDisplay::InlineFlex
2135            | LayoutDisplay::InlineGrid
2136    )
2137}
2138
2139// +spec:display-property:c2520b - Block containers with only inline-level children establish IFC; mixed content gets anonymous block wrappers
2140/// Checks if a block container has only inline-level children.
2141/// According to CSS 2.2 Section 9.4.2: "An inline formatting context is established
2142/// by a block container box that contains no block-level boxes."
2143// +spec:display-property:75d642 - block container with only inline-level content establishes IFC
2144// +spec:display-property:c188d6 - IFC: all inline content within a containing block flows together as continuous text
2145fn has_only_inline_children(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2146    let hierarchy = styled_dom.node_hierarchy.as_container();
2147    let node_hier = match hierarchy.get(node_id) {
2148        Some(n) => n,
2149        None => {
2150            return false;
2151        }
2152    };
2153
2154    // Get the first child
2155    let mut current_child = node_hier.first_child_id(node_id);
2156
2157    // If there are no children, it's not an IFC (it's empty)
2158    if current_child.is_none() {
2159        return false;
2160    }
2161
2162    // Check all children
2163    while let Some(child_id) = current_child {
2164        let is_inline = is_inline_level(styled_dom, child_id);
2165
2166        if !is_inline {
2167            // Found a block-level child
2168            return false;
2169        }
2170
2171        // Move to next sibling
2172        if let Some(child_hier) = hierarchy.get(child_id) {
2173            current_child = child_hier.next_sibling_id();
2174        } else {
2175            break;
2176        }
2177    }
2178
2179    // All children are inline-level
2180    true
2181}
2182
2183/// Pre-computes all CSS properties needed during layout for a single node.
2184/// 
2185/// This is called once per node during layout tree construction, avoiding
2186/// repeated style lookups during the actual layout pass (O(n) vs O(n²)).
2187fn compute_layout_style(styled_dom: &StyledDom, dom_id: NodeId) -> ComputedLayoutStyle {
2188    let styled_node_state = styled_dom
2189        .styled_nodes
2190        .as_container()
2191        .get(dom_id)
2192        .map(|n| n.styled_node_state.clone())
2193        .unwrap_or_default();
2194
2195    // Get display property
2196    let display = match get_display_property(styled_dom, Some(dom_id)) {
2197        MultiValue::Exact(d) => d,
2198        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => LayoutDisplay::Block,
2199    };
2200
2201    // Get position property
2202    let position = get_position(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2203
2204    // Get float property  
2205    let float = get_float(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2206
2207    // Get overflow properties
2208    // +spec:overflow:48890c - overflow:hidden treated as overflow:clip on replaced elements
2209    let is_replaced = matches!(
2210        styled_dom.node_data.as_container()[dom_id].get_node_type(),
2211        NodeType::Image(_) | NodeType::VirtualView
2212    );
2213    let overflow_x = {
2214        let v = get_overflow_x(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2215        if is_replaced && v == LayoutOverflow::Hidden { LayoutOverflow::Clip } else { v }
2216    };
2217    let overflow_y = {
2218        let v = get_overflow_y(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2219        if is_replaced && v == LayoutOverflow::Hidden { LayoutOverflow::Clip } else { v }
2220    };
2221
2222    // Get writing mode, direction, and text-orientation
2223    // +spec:writing-modes:2af307 - Propagate used writing-mode from <body> to <html> root
2224    let writing_mode = {
2225        let own_wm = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2226        let nd = &styled_dom.node_data.as_container()[dom_id];
2227        if matches!(nd.node_type, NodeType::Html) {
2228            // If root <html>, propagate writing-mode from first <body> child
2229            styled_dom
2230                .node_hierarchy
2231                .as_container()
2232                .get(dom_id)
2233                .and_then(|node| node.first_child_id(dom_id))
2234                .and_then(|child_id| {
2235                    let child_data = &styled_dom.node_data.as_container()[child_id];
2236                    if matches!(child_data.node_type, NodeType::Body) {
2237                        let child_state = &styled_dom
2238                            .styled_nodes
2239                            .as_container()[child_id]
2240                            .styled_node_state;
2241                        Some(get_writing_mode(styled_dom, child_id, child_state)
2242                            .unwrap_or_default())
2243                    } else {
2244                        None
2245                    }
2246                })
2247                .unwrap_or(own_wm)
2248        } else {
2249            own_wm
2250        }
2251    };
2252    let direction = get_direction(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2253    let text_orientation = get_text_orientation(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2254
2255    // Get text-align
2256    let text_align = get_text_align(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2257
2258    // Get explicit width/height (None = auto)
2259    let width = match get_css_width(styled_dom, dom_id, &styled_node_state) {
2260        MultiValue::Exact(w) => Some(w),
2261        _ => None,
2262    };
2263    let height = match get_css_height(styled_dom, dom_id, &styled_node_state) {
2264        MultiValue::Exact(h) => Some(h),
2265        _ => None,
2266    };
2267
2268    // Get min/max constraints
2269    let min_width = match get_css_min_width(styled_dom, dom_id, &styled_node_state) {
2270        MultiValue::Exact(v) => Some(v),
2271        _ => None,
2272    };
2273    let min_height = match get_css_min_height(styled_dom, dom_id, &styled_node_state) {
2274        MultiValue::Exact(v) => Some(v),
2275        _ => None,
2276    };
2277    let max_width = match get_css_max_width(styled_dom, dom_id, &styled_node_state) {
2278        MultiValue::Exact(v) => Some(v),
2279        _ => None,
2280    };
2281    let max_height = match get_css_max_height(styled_dom, dom_id, &styled_node_state) {
2282        MultiValue::Exact(v) => Some(v),
2283        _ => None,
2284    };
2285
2286    ComputedLayoutStyle {
2287        display,
2288        position,
2289        float,
2290        overflow_x,
2291        overflow_y,
2292        writing_mode,
2293        direction,
2294        text_orientation,
2295        width,
2296        height,
2297        min_width,
2298        min_height,
2299        max_width,
2300        max_height,
2301        text_align,
2302    }
2303}
2304
2305// hash_node_data() removed — replaced by NodeDataFingerprint::compute()
2306
2307/// Helper function to get element's computed font-size
2308fn get_element_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
2309    unsafe { core::ptr::write_volatile(0x400E0 as *mut u32, 0xC3_000001u32); } // 2-arg wrapper entered
2310    let node_state = styled_dom
2311        .styled_nodes
2312        .as_container()
2313        .get(dom_id)
2314        .map(|n| &n.styled_node_state)
2315        .cloned()
2316        .unwrap_or_default();
2317    unsafe { core::ptr::write_volatile(0x400E0 as *mut u32, 0xC3_000002u32); } // after node_state (clone); next = 3-arg call
2318
2319    crate::solver3::getters::get_element_font_size(styled_dom, dom_id, &node_state)
2320}
2321
2322/// Helper function to get parent's computed font-size
2323fn get_parent_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
2324    styled_dom
2325        .node_hierarchy
2326        .as_container()
2327        .get(dom_id)
2328        .and_then(|node| node.parent_id())
2329        .map(|parent_id| get_element_font_size(styled_dom, parent_id))
2330        .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE)
2331}
2332
2333/// Helper function to get root element's font-size
2334fn get_root_font_size(styled_dom: &StyledDom) -> f32 {
2335    // Root is always NodeId(0) in Azul
2336    get_element_font_size(styled_dom, NodeId::new(0))
2337}
2338
2339/// Create a ResolutionContext for a given node
2340fn create_resolution_context(
2341    styled_dom: &StyledDom,
2342    dom_id: NodeId,
2343    containing_block_size: Option<azul_css::props::basic::PhysicalSize>,
2344    viewport_size: LogicalSize,
2345) -> azul_css::props::basic::ResolutionContext {
2346    unsafe { core::ptr::write_volatile(0x400D8 as *mut u32, 0xC1_000001u32); } // create_resolution_context entered
2347    let element_font_size = get_element_font_size(styled_dom, dom_id);
2348    unsafe { core::ptr::write_volatile(0x400D8 as *mut u32, 0xC1_000002u32); } // after get_element_font_size
2349    let parent_font_size = get_parent_font_size(styled_dom, dom_id);
2350    unsafe { core::ptr::write_volatile(0x400D8 as *mut u32, 0xC1_000003u32); } // after get_parent_font_size
2351    let root_font_size = get_root_font_size(styled_dom);
2352    unsafe { core::ptr::write_volatile(0x400D8 as *mut u32, 0xC1_000004u32); } // after get_root_font_size
2353
2354    ResolutionContext {
2355        element_font_size,
2356        parent_font_size,
2357        root_font_size,
2358        // +spec:box-model:ec6466 - percentage margins/padding resolve to 0 when containing block is unknown (intrinsic sizing), breaking cyclic dependencies per css-sizing-3 §5.2.1
2359        containing_block_size: containing_block_size.unwrap_or(PhysicalSize::new(0.0, 0.0)),
2360        element_size: None, // Not yet laid out
2361        viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
2362    }
2363}
2364
2365/// Result of collecting box properties from the styled DOM.
2366struct CollectedBoxProps {
2367    unresolved: crate::solver3::geometry::UnresolvedBoxProps,
2368    resolved: BoxProps,
2369}
2370
2371/// Collects box properties from the styled DOM and returns both unresolved and resolved forms.
2372///
2373/// The unresolved form stores the raw CSS values for later re-resolution when
2374/// the containing block size is known. The resolved form is an initial resolution
2375/// using viewport_size for viewport-relative units.
2376fn collect_box_props(
2377    styled_dom: &StyledDom,
2378    dom_id: NodeId,
2379    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
2380    viewport_size: LogicalSize,
2381) -> CollectedBoxProps {
2382    use crate::solver3::geometry::{UnresolvedBoxProps, UnresolvedEdge, UnresolvedMargin};
2383    use crate::solver3::getters::*;
2384    // M12.7 diag: collect_box_props sub-step markers (0xC0_0N). The last one set
2385    // before create_node step A is the diverging call.
2386    unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000001u32); } // entered
2387
2388    let node_data = &styled_dom.node_data.as_container()[dom_id];
2389
2390    // Get styled node state
2391    let node_state = styled_dom
2392        .styled_nodes
2393        .as_container()
2394        .get(dom_id)
2395        .map(|n| &n.styled_node_state)
2396        .cloned()
2397        .unwrap_or_default();
2398    unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000002u32); } // after node_state (clone)
2399
2400    // Create resolution context for this element
2401    // Note: containing_block_size is None here because we don't have it yet
2402    // This is fine for initial resolution - will be re-resolved during layout
2403    let context = create_resolution_context(styled_dom, dom_id, None, viewport_size);
2404    unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000003u32); } // after create_resolution_context
2405
2406    // Read margin values from styled_dom
2407    let margin_top_mv = get_css_margin_top(styled_dom, dom_id, &node_state);
2408    unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000004u32); } // after get_css_margin_top
2409    let margin_right_mv = get_css_margin_right(styled_dom, dom_id, &node_state);
2410    let margin_bottom_mv = get_css_margin_bottom(styled_dom, dom_id, &node_state);
2411    let margin_left_mv = get_css_margin_left(styled_dom, dom_id, &node_state);
2412
2413    // Convert MultiValue to UnresolvedMargin
2414    let to_unresolved_margin = |mv: &MultiValue<PixelValue>| -> UnresolvedMargin {
2415        match mv {
2416            MultiValue::Auto => UnresolvedMargin::Auto,
2417            MultiValue::Exact(pv) => UnresolvedMargin::Length(*pv),
2418            _ => UnresolvedMargin::Zero,
2419        }
2420    };
2421
2422    // Build unresolved margins
2423    let unresolved_margin = UnresolvedEdge {
2424        top: to_unresolved_margin(&margin_top_mv),
2425        right: to_unresolved_margin(&margin_right_mv),
2426        bottom: to_unresolved_margin(&margin_bottom_mv),
2427        left: to_unresolved_margin(&margin_left_mv),
2428    };
2429    unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000005u32); } // after margin block
2430
2431    // Read padding values
2432    let padding_top_mv = get_css_padding_top(styled_dom, dom_id, &node_state);
2433    let padding_right_mv = get_css_padding_right(styled_dom, dom_id, &node_state);
2434    let padding_bottom_mv = get_css_padding_bottom(styled_dom, dom_id, &node_state);
2435    let padding_left_mv = get_css_padding_left(styled_dom, dom_id, &node_state);
2436
2437    // Convert MultiValue to PixelValue (default to 0px)
2438    let to_pixel_value = |mv: MultiValue<PixelValue>| -> PixelValue {
2439        match mv {
2440            MultiValue::Exact(pv) => pv,
2441            _ => PixelValue::const_px(0),
2442        }
2443    };
2444
2445    // Build unresolved padding
2446    let unresolved_padding = UnresolvedEdge {
2447        top: to_pixel_value(padding_top_mv),
2448        right: to_pixel_value(padding_right_mv),
2449        bottom: to_pixel_value(padding_bottom_mv),
2450        left: to_pixel_value(padding_left_mv),
2451    };
2452    unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000056u32); } // after padding getters+values, before get_display_type
2453
2454    // +spec:table-layout:038f9d - padding does not apply to table-row-group, table-header-group, table-footer-group, table-row, table-column-group, table-column
2455    // Non-cell internal table elements (rows, row groups, columns, column groups) do not have padding.
2456    // M12.7 diag: capture get_display_type's return BEFORE the match. If 0x400D0 reads
2457    // 0xC0_57<dt> the CALL returned (dt = LayoutDisplay discriminant) and the MATCH below
2458    // diverges; if it stays 0x56, get_display_type (the enum extraction) itself diverges.
2459    // M12.7 NOTE: get_display_type RETURNS a valid dt here (captured =2), but the code
2460    // immediately after diverges — and replacing the `match` below with a branchless
2461    // bitmask test did NOT help (so it's NOT the multi-way-branch codegen). So the
2462    // get_display_type CALL corrupts the caller frame / control flow (same class as
2463    // create_node's return 0→48704), specific to ENUM-returning getters (pixel getters
2464    // like get_css_margin_* lift fine). Remill-level. The match is kept (original).
2465    let unresolved_padding = match get_display_type(styled_dom, dom_id) {
2466        LayoutDisplay::TableRow
2467        | LayoutDisplay::TableRowGroup
2468        | LayoutDisplay::TableHeaderGroup
2469        | LayoutDisplay::TableFooterGroup
2470        | LayoutDisplay::TableColumn
2471        | LayoutDisplay::TableColumnGroup => UnresolvedEdge {
2472            top: PixelValue::const_px(0),
2473            right: PixelValue::const_px(0),
2474            bottom: PixelValue::const_px(0),
2475            left: PixelValue::const_px(0),
2476        },
2477        _ => unresolved_padding,
2478    };
2479    unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000006u32); } // after padding block
2480
2481    // Read border values
2482    let border_top_mv = get_css_border_top_width(styled_dom, dom_id, &node_state);
2483    let border_right_mv = get_css_border_right_width(styled_dom, dom_id, &node_state);
2484    let border_bottom_mv = get_css_border_bottom_width(styled_dom, dom_id, &node_state);
2485    let border_left_mv = get_css_border_left_width(styled_dom, dom_id, &node_state);
2486
2487    // +spec:box-model:17c0e0 - computed border-width is 0 if border-style is none or hidden
2488    // +spec:box-model:5d2b66 - border-style none/hidden means no border
2489    // CSS 2.2 §8.5.1: "Computed value: absolute length; '0' if the border style is 'none' or 'hidden'"
2490    use azul_css::props::style::border::BorderStyle;
2491
2492    let style_zeroes_width = |s: BorderStyle| matches!(s, BorderStyle::None | BorderStyle::Hidden);
2493
2494    // Read border styles to check if widths should be zeroed.
2495    // FAST PATH: compact cache returns styles directly for normal state — no
2496    // cascade walks. Prior code here did 4 cascade walks × 586 nodes.
2497    let (bs_top, bs_right, bs_bottom, bs_left) = {
2498        let cache_ptr = &styled_dom.css_property_cache.ptr;
2499        if node_state.is_normal() {
2500            if let Some(ref cc) = cache_ptr.compact_cache {
2501                let idx = dom_id.index();
2502                (cc.get_border_top_style(idx), cc.get_border_right_style(idx),
2503                 cc.get_border_bottom_style(idx), cc.get_border_left_style(idx))
2504            } else {
2505                (
2506                    cache_ptr.get_border_top_style(node_data, &dom_id, &node_state)
2507                        .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2508                    cache_ptr.get_border_right_style(node_data, &dom_id, &node_state)
2509                        .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2510                    cache_ptr.get_border_bottom_style(node_data, &dom_id, &node_state)
2511                        .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2512                    cache_ptr.get_border_left_style(node_data, &dom_id, &node_state)
2513                        .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2514                )
2515            }
2516        } else {
2517            (
2518                cache_ptr.get_border_top_style(node_data, &dom_id, &node_state)
2519                    .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2520                cache_ptr.get_border_right_style(node_data, &dom_id, &node_state)
2521                    .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2522                cache_ptr.get_border_bottom_style(node_data, &dom_id, &node_state)
2523                    .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2524                cache_ptr.get_border_left_style(node_data, &dom_id, &node_state)
2525                    .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
2526            )
2527        }
2528    };
2529
2530    // Build unresolved border, zeroing width when style is none or hidden
2531    let unresolved_border = UnresolvedEdge {
2532        top: if style_zeroes_width(bs_top) { PixelValue::const_px(0) } else { to_pixel_value(border_top_mv) },
2533        right: if style_zeroes_width(bs_right) { PixelValue::const_px(0) } else { to_pixel_value(border_right_mv) },
2534        bottom: if style_zeroes_width(bs_bottom) { PixelValue::const_px(0) } else { to_pixel_value(border_bottom_mv) },
2535        left: if style_zeroes_width(bs_left) { PixelValue::const_px(0) } else { to_pixel_value(border_left_mv) },
2536    };
2537    unsafe { core::ptr::write_volatile(0x400D0 as *mut u32, 0xC0_000007u32); } // after border block (incl is_normal/compact_cache fast-path)
2538
2539    // +spec:box-model:8538a9 - Internal table elements do not have margins (CSS 2.2 §17.5)
2540    // "These boxes have content and borders and cells have padding as well.
2541    //  Internal table elements do not have margins."
2542    // +spec:box-model:b4923a - Internal table elements do not have margins (CSS 2.2 § 17.5)
2543    // +spec:box-model:0a9f8e - Internal table elements do not have margins (CSS 2.2 § 17.5)
2544    let display_type = get_display_type(styled_dom, dom_id);
2545    let unresolved_margin = match display_type {
2546        LayoutDisplay::TableRow
2547        | LayoutDisplay::TableRowGroup
2548        | LayoutDisplay::TableHeaderGroup
2549        | LayoutDisplay::TableFooterGroup
2550        | LayoutDisplay::TableCell
2551        | LayoutDisplay::TableColumn
2552        | LayoutDisplay::TableColumnGroup => UnresolvedEdge {
2553            top: UnresolvedMargin::Zero,
2554            right: UnresolvedMargin::Zero,
2555            bottom: UnresolvedMargin::Zero,
2556            left: UnresolvedMargin::Zero,
2557        },
2558        // +spec:box-model:1197a5 - height property does not apply to non-replaced inline elements; vertical margins zeroed
2559        // +spec:replaced-elements:f07118 - non-replaced elements have rendering dictated by CSS model
2560        // "These properties apply to all elements, but vertical margins will not have
2561        //  any effect on non-replaced inline elements."
2562        LayoutDisplay::Inline => {
2563            let is_replaced = matches!(
2564                node_data.get_node_type(),
2565                NodeType::Image(_) | NodeType::VirtualView
2566            );
2567            if is_replaced {
2568                unresolved_margin
2569            } else {
2570                UnresolvedEdge {
2571                    top: UnresolvedMargin::Zero,
2572                    bottom: UnresolvedMargin::Zero,
2573                    ..unresolved_margin
2574                }
2575            }
2576        },
2577        _ => unresolved_margin,
2578    };
2579
2580    // Build the UnresolvedBoxProps
2581    let unresolved = UnresolvedBoxProps {
2582        margin: unresolved_margin,
2583        padding: unresolved_padding,
2584        border: unresolved_border,
2585    };
2586
2587    // Create initial resolution params (with viewport as containing block for now)
2588    let params = crate::solver3::geometry::ResolutionParams {
2589        containing_block: viewport_size,
2590        viewport_size,
2591        element_font_size: context.parent_font_size,
2592        root_font_size: context.root_font_size,
2593    };
2594
2595    // Resolve to get initial box_props
2596    let resolved = unresolved.resolve(&params);
2597
2598    // Debug ALL node box props (padding, margin, border) for cascade debugging
2599    if let Some(msgs) = debug_messages.as_mut() {
2600        msgs.push(LayoutDebugMessage::box_props(format!(
2601            "[BOX] node[{}] {:?} pad=[{:.1} {:.1} {:.1} {:.1}] mar=[{:.1} {:.1} {:.1} {:.1}] bor=[{:.1} {:.1} {:.1} {:.1}]",
2602            dom_id.index(), node_data.node_type,
2603            resolved.padding.top, resolved.padding.right, resolved.padding.bottom, resolved.padding.left,
2604            resolved.margin.top, resolved.margin.right, resolved.margin.bottom, resolved.margin.left,
2605            resolved.border.top, resolved.border.right, resolved.border.bottom, resolved.border.left,
2606        )));
2607    }
2608
2609    // Debug nodes with non-zero margins or vh units
2610    if let Some(msgs) = debug_messages.as_mut() {
2611        // Check if any margin uses vh
2612        let has_vh = match &unresolved_margin.top {
2613            UnresolvedMargin::Length(pv) => pv.metric == azul_css::props::basic::SizeMetric::Vh,
2614            _ => false,
2615        };
2616        if has_vh || resolved.margin.top > 0.0 || resolved.margin.left > 0.0 {
2617            msgs.push(LayoutDebugMessage::box_props(format!(
2618                "NodeId {:?} ({:?}): unresolved_margin_top={:?}, resolved_margin_top={:.2}, viewport_size={:?}",
2619                dom_id, node_data.node_type,
2620                unresolved_margin.top,
2621                resolved.margin.top,
2622                viewport_size
2623            )));
2624        }
2625    }
2626
2627    // Debug margin_auto detection
2628    if let Some(msgs) = debug_messages.as_mut() {
2629        msgs.push(LayoutDebugMessage::box_props(format!(
2630            "NodeId {:?} ({:?}): margin_auto: left={}, right={}, top={}, bottom={} | margin_left={:?}",
2631            dom_id, node_data.node_type,
2632            resolved.margin_auto.left, resolved.margin_auto.right,
2633            resolved.margin_auto.top, resolved.margin_auto.bottom,
2634            unresolved_margin.left
2635        )));
2636    }
2637
2638    // Debug for Body nodes
2639    if matches!(node_data.node_type, azul_core::dom::NodeType::Body) {
2640        if let Some(msgs) = debug_messages.as_mut() {
2641            msgs.push(LayoutDebugMessage::box_props(format!(
2642                "Body margin resolved: top={:.2}, right={:.2}, bottom={:.2}, left={:.2}",
2643                resolved.margin.top, resolved.margin.right,
2644                resolved.margin.bottom, resolved.margin.left
2645            )));
2646        }
2647    }
2648
2649    CollectedBoxProps { unresolved, resolved }
2650}
2651
2652/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 1:
2653/// "Remove all irrelevant boxes. These are boxes that do not contain table-related boxes
2654/// and do not themselves have 'display' set to a table-related value. In this context,
2655/// 'irrelevant boxes' means anonymous inline boxes that contain only white space."
2656///
2657/// Checks if a DOM node is whitespace-only text (for table anonymous box generation).
2658/// Returns true if the node is a text node containing only whitespace characters
2659/// that would be collapsed away by the white-space property.
2660// according to the 'white-space' property does not generate any anonymous inline boxes (CSS2§9.2.2.1)
2661pub fn is_whitespace_only_text(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2662    let binding = styled_dom.node_data.as_container();
2663    let node_data = binding.get(node_id);
2664    if let Some(data) = node_data {
2665        if let NodeType::Text(text) = data.get_node_type() {
2666            // Check if the text contains only CSS document white space characters
2667            // Per CSS Text 3 §4.1: document white space = U+0020, U+0009, segment breaks
2668            if !text.chars().all(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')) {
2669                return false;
2670            }
2671            // Per CSS2§9.2.2.1: "White space content that would subsequently be
2672            // collapsed away according to the 'white-space' property does not
2673            // generate any anonymous inline boxes."
2674            // For white-space: pre / pre-wrap / break-spaces, whitespace is preserved
2675            // and should NOT be treated as collapsible.
2676            let white_space = styled_dom
2677                .styled_nodes
2678                .as_container()
2679                .get(node_id)
2680                .map(|n| {
2681                    match get_white_space_property(styled_dom, node_id, &n.styled_node_state) {
2682                        MultiValue::Exact(ws) => ws,
2683                        _ => StyleWhiteSpace::Normal,
2684                    }
2685                })
2686                .unwrap_or(StyleWhiteSpace::Normal);
2687            return match white_space {
2688                // These values collapse whitespace — whitespace-only text is collapsible
2689                StyleWhiteSpace::Normal | StyleWhiteSpace::Nowrap | StyleWhiteSpace::PreLine => true,
2690                // These values preserve whitespace — whitespace-only text is NOT collapsible
2691                StyleWhiteSpace::Pre | StyleWhiteSpace::PreWrap | StyleWhiteSpace::BreakSpaces => false,
2692            };
2693        }
2694    }
2695
2696    false
2697}
2698
2699/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 1:
2700/// Determines if a node should be skipped in table structure generation.
2701/// Whitespace-only text nodes are "irrelevant" and should not generate boxes
2702/// when they appear between table-related elements.
2703///
2704/// Returns true if the node should be skipped (i.e., it's whitespace-only text
2705/// and the parent is a table structural element).
2706fn should_skip_for_table_structure(
2707    styled_dom: &StyledDom,
2708    node_id: NodeId,
2709    parent_display: LayoutDisplay,
2710) -> bool {
2711    // CSS 2.2 Section 17.2.1: Only skip whitespace text nodes when parent is
2712    // a table structural element (table, row group, row)
2713    matches!(
2714        parent_display,
2715        LayoutDisplay::Table
2716            | LayoutDisplay::InlineTable
2717            | LayoutDisplay::TableRowGroup
2718            | LayoutDisplay::TableHeaderGroup
2719            | LayoutDisplay::TableFooterGroup
2720            | LayoutDisplay::TableRow
2721    ) && is_whitespace_only_text(styled_dom, node_id)
2722}
2723
2724/// Returns true if the given display type is a "proper table child" of a table/inline-table box.
2725/// Per CSS 2.2 §17.2.1, proper table children are: table-row-group, table-header-group,
2726/// table-footer-group, table-row, table-column-group, table-column, table-caption.
2727fn is_proper_table_child(display: LayoutDisplay) -> bool {
2728    matches!(
2729        display,
2730        LayoutDisplay::TableRowGroup
2731            | LayoutDisplay::TableHeaderGroup
2732            | LayoutDisplay::TableFooterGroup
2733            | LayoutDisplay::TableRow
2734            | LayoutDisplay::TableColumnGroup
2735            | LayoutDisplay::TableColumn
2736            | LayoutDisplay::TableCaption
2737    )
2738}
2739
2740// Determines the display type of a node based on its tag and CSS properties.
2741// Delegates to getters::get_display_property which uses the compact cache fast path.
2742// M12.7 ROOT: get_display_type (and every layout enum getter) mis-lifts to wasm via the
2743// remill enum-return/decode path — the geometry-chain blocker. FOUR Rust workarounds all
2744// FAILED to advance (none reached collect_box_props past get_display_type):
2745//   1. skip the get_css_property! enum compact-cache fast path  → no change
2746//   2. replace the LayoutDisplay `match` with a branchless bitmask → no change
2747//   3. #[inline(never)] (wrap the call w/ enforce_sp_preservation) → made it diverge earlier
2748//   4. bypass MultiValue<LayoutDisplay> by reading cc.get_display() directly → diverges earlier
2749// So it is NOT the match codegen, NOT the MultiValue wrapper, NOT a frame/SP issue — it is
2750// the lift of a fn RETURNING a small fieldless enum (LayoutDisplay) corrupting control flow
2751// (pixel/i16-returning getters lift fine). Needs the remill m12-q-reg-x8-sret fork's
2752// enum-return handling — not fixable in Rust. (Original kept.)
2753pub fn get_display_type(styled_dom: &StyledDom, node_id: NodeId) -> LayoutDisplay {
2754    use crate::solver3::getters::get_display_property;
2755    get_display_property(styled_dom, Some(node_id)).unwrap_or(LayoutDisplay::Inline)
2756}
2757
2758// +spec:display-contents:95faa5 - blockification has no effect on none/contents (other => other)
2759// +spec:display-property:f68848 - Automatic box type transformations: blockification of computed display values
2760/// Blockify a display type per CSS Display 3 §2.7.
2761// +spec:display-property:760c5f - blockification sets computed outer display type to block
2762/// +spec:display-property:d50f70 - blockification affects computed values, determining principal box type only
2763/// // +spec:inline-block:692e44 - blockification of inline-block per CSS2 compatibility
2764// +spec:display-property:c3aca2 - inline-block blockifies to block, not flow-root
2765// +spec:display-property:ee2d65 - blockification of inline-level display types (CSS Display 3 §2.7)
2766// +spec:display-property:e4a8b7 - layout-internal boxes blockified to flow (block container)
2767/// CSS Flexbox §3: flex items with table-internal display values
2768/// (table-cell, table-row, table-row-group, table-header-group, table-footer-group,
2769/// table-column, table-column-group, table-caption) are blockified to display:block
2770/// before anonymous table box generation can occur. E.g. two consecutive
2771/// display:table-cell flex items become two separate display:block flex items.
2772fn blockify_flex_item_if_table_internal(nodes: &mut Vec<LayoutNode>, node_idx: usize) {
2773    if let Some(node) = nodes.get_mut(node_idx) {
2774        let is_table_internal = matches!(
2775            node.formatting_context,
2776            FormattingContext::TableCell
2777                | FormattingContext::TableRow
2778                | FormattingContext::TableRowGroup
2779                | FormattingContext::TableColumnGroup
2780                | FormattingContext::TableCaption
2781                | FormattingContext::Table
2782        );
2783        if is_table_internal {
2784            node.formatting_context = FormattingContext::Block {
2785                establishes_new_context: true,
2786            };
2787        }
2788    }
2789}
2790
2791/// Returns true if the node is a replaced element per CSS Display 3 Appendix B.
2792/// Replaced elements (img, canvas, embed, object, audio, video, input, textarea,
2793/// select, br, wbr, meter, progress, virtual views) cannot be un-boxed by
2794/// `display: contents` and always establish an independent formatting context.
2795fn is_replaced_element(node_data: &NodeData) -> bool {
2796    matches!(
2797        node_data.get_node_type(),
2798        NodeType::Image(_)
2799        | NodeType::VirtualView
2800        | NodeType::Br
2801        | NodeType::Wbr
2802        | NodeType::Meter
2803        | NodeType::Progress
2804        | NodeType::Canvas
2805        | NodeType::Embed
2806        | NodeType::Object
2807        | NodeType::Audio
2808        | NodeType::Video
2809        | NodeType::Input
2810        | NodeType::TextArea
2811        | NodeType::Select
2812    )
2813}
2814
2815// +spec:display-property:285fe7 - block box establishing a BFC (block-level block container with new BFC)
2816/// **Corrected:** Checks for all conditions that create a new Block Formatting Context.
2817/// A BFC contains floats and prevents margin collapse.
2818fn establishes_new_block_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2819    let display = get_display_type(styled_dom, node_id);
2820    if matches!(
2821        display,
2822        LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::TableCaption | LayoutDisplay::FlowRoot
2823    ) {
2824        return true;
2825    }
2826
2827    if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
2828        let overflow_x = get_overflow_x(styled_dom, node_id, &styled_node.styled_node_state);
2829        if !overflow_x.is_visible_or_clip() {
2830            return true;
2831        }
2832
2833        let overflow_y = get_overflow_y(styled_dom, node_id, &styled_node.styled_node_state);
2834        if !overflow_y.is_visible_or_clip() {
2835            return true;
2836        }
2837
2838        let position = get_position(styled_dom, node_id, &styled_node.styled_node_state);
2839        if position.is_absolute_or_fixed() {
2840            return true;
2841        }
2842
2843        let float = get_float(styled_dom, node_id, &styled_node.styled_node_state);
2844        if !float.is_none() {
2845            return true;
2846        }
2847    }
2848
2849    // CSS Writing Modes 4 § 3.2: block container with different writing-mode than parent establishes BFC
2850    if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
2851        let hierarchy = styled_dom.node_hierarchy.as_container();
2852        if let Some(parent_dom_id) = hierarchy[node_id].parent_id() {
2853            let parent_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
2854            let child_wm = get_writing_mode(styled_dom, node_id, &styled_node.styled_node_state).unwrap_or_default();
2855            let parent_wm = get_writing_mode(styled_dom, parent_dom_id, parent_state).unwrap_or_default();
2856            if child_wm != parent_wm {
2857                return true;
2858            }
2859        }
2860    }
2861
2862    // +spec:replaced-elements:4f494d - replaced elements always establish an independent formatting context
2863    let node_data = &styled_dom.node_data.as_container()[node_id];
2864    if is_replaced_element(node_data) {
2865        return true;
2866    }
2867
2868    // The root element (<html>) also establishes a BFC.
2869    if styled_dom.root.into_crate_internal() == Some(node_id) {
2870        return true;
2871    }
2872
2873    false
2874}
2875
2876// +spec:display-property:0d93f1 - maps display value to box generation (principal box, none, or contents)
2877/// Like `determine_formatting_context`, but uses an explicit (possibly blockified) display type
2878/// instead of reading it from the DOM. Used when blockification changes the display.
2879// +spec:display-property:80f43f - inner display type defines formatting context for non-replaced elements
2880// +spec:display-property:46e71c - Maps outer display (block/inline) and inner display (flow/flow-root/table/flex/grid) to FormattingContext
2881// +spec:display-property:aa582d - maps display types to formatting contexts (inline-level, block-level, atomic inline, block container)
2882fn determine_formatting_context_for_display(
2883    styled_dom: &StyledDom,
2884    node_id: NodeId,
2885    display_type: LayoutDisplay,
2886) -> FormattingContext {
2887    let node_data = &styled_dom.node_data.as_container()[node_id];
2888    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
2889        return FormattingContext::Inline;
2890    }
2891    // +spec:display-property:2a8d62 - block containers with inline-level content establish an IFC
2892    match display_type {
2893        // +spec:display-property:37bcf3 - inline outer display type generates an inline box
2894        // +spec:display-property:30a935 - outer display without inner defaults to flow (block/inline both use flow context)
2895        LayoutDisplay::Inline => FormattingContext::Inline,
2896        // +spec:block-formatting-context:97b03b - flow-root always establishes a new BFC; block/list-item may establish one based on other conditions
2897        // +spec:display-property:0bac26 - list-item limited to flow layout inner types (block/flow-root)
2898        // +spec:display-property:0beffc - block container with only inline children establishes IFC
2899        // +spec:display-property:7c49c1 - block container with only inline children establishes an IFC
2900        // +spec:display-property:90ba2a - flow-root always establishes a new BFC
2901        LayoutDisplay::FlowRoot => FormattingContext::Block {
2902            establishes_new_context: true,
2903        },
2904        LayoutDisplay::Block | LayoutDisplay::ListItem => {
2905            if has_only_inline_children(styled_dom, node_id) {
2906                FormattingContext::Inline
2907            } else {
2908                FormattingContext::Block {
2909                    establishes_new_context: establishes_new_block_formatting_context(
2910                        styled_dom, node_id,
2911                    ),
2912                }
2913            }
2914        }
2915        LayoutDisplay::InlineBlock => FormattingContext::InlineBlock,
2916        // +spec:display-property:723fe8 - CSS 2.2 §17.2 table model: display types map to formatting contexts, table-column/column-group not rendered, anonymous table objects generated
2917        // +spec:table-layout:023714 - map display values to table formatting contexts per CSS 2.2 §17.2
2918        // +spec:table-layout:6c5039 - row-primary table model: rows/cells/captions/columns mapped here
2919        // +spec:table-layout:75eea9 - display property values for table elements (table, tr, td, etc.)
2920        // +spec:table-layout:3ee121 - layout-internal display types map to table formatting context
2921        // +spec:display-property:b02b7f - table display types map to table formatting contexts;
2922        // table-column/table-column-group not rendered (treated as display:none for box generation)
2923        LayoutDisplay::Table | LayoutDisplay::InlineTable => FormattingContext::Table,
2924        LayoutDisplay::TableRowGroup
2925        | LayoutDisplay::TableHeaderGroup
2926        | LayoutDisplay::TableFooterGroup => FormattingContext::TableRowGroup,
2927        LayoutDisplay::TableRow => FormattingContext::TableRow,
2928        LayoutDisplay::TableCell => FormattingContext::TableCell,
2929        // +spec:display-property:da3fc7 - display:none/contents generate no boxes (no inner/outer display types)
2930        // +spec:display-property:e370af - display:none generates no boxes or text sequences
2931        LayoutDisplay::None => FormattingContext::None,
2932        LayoutDisplay::Flex | LayoutDisplay::InlineFlex => FormattingContext::Flex,
2933        LayoutDisplay::TableColumnGroup => FormattingContext::TableColumnGroup,
2934        LayoutDisplay::TableCaption => FormattingContext::TableCaption,
2935        LayoutDisplay::Grid | LayoutDisplay::InlineGrid => FormattingContext::Grid,
2936        // table-column elements are used only for column styling, not for generating boxes
2937        LayoutDisplay::TableColumn => FormattingContext::None,
2938        // +spec:display-contents:584072 - no special behavior for legend/HTML elements; contents handled normally
2939        // display:contents - element generates no box, children are promoted to parent
2940        LayoutDisplay::Contents => FormattingContext::Contents,
2941        // +spec:display-property:b89b80 - run-in box falls back to block (merging into next block not implemented)
2942        // +spec:display-property:ccd4e6 - run-in falls back to block; reparenting not implemented
2943        // These less common display types default to block behavior
2944        // +spec:display-property:7d77f5 - run-in treated as block (run-in sequencing fixup not yet implemented)
2945        // +spec:display-property:0c30c4 - run-in boxes fall back to block (run-in reparenting not implemented, matches browser behavior)
2946        // +spec:display-property:2f5c52 - run-in treated as block (full run-in merging not implemented)
2947        LayoutDisplay::RunIn | LayoutDisplay::Marker => {
2948            FormattingContext::Block {
2949                establishes_new_context: true,
2950            }
2951        }
2952    }
2953}
2954
2955/// The logic now correctly identifies all BFC roots.
2956fn determine_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> FormattingContext {
2957    let node_data = &styled_dom.node_data.as_container()[node_id];
2958    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
2959        return FormattingContext::Inline;
2960    }
2961    let display_type = get_display_type(styled_dom, node_id);
2962    determine_formatting_context_for_display(styled_dom, node_id, display_type)
2963}