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    cell::Cell,
4    collections::BTreeMap,
5    hash::{Hash, Hasher},
6    sync::Arc,
7};
8
9use azul_core::diff::NodeDataFingerprint;
10
11use crate::text3::cache::UnifiedConstraints;
12
13thread_local! {
14    /// Per-thread counter for IFC IDs, reset to 0 at the start of each layout
15    /// pass (see [`IfcId::reset_counter`]).
16    ///
17    /// This was previously a process-global `AtomicU32`. Two `layout_document`
18    /// calls running concurrently on different threads (the `Sync` layout bound
19    /// permits this) shared that single counter, so their IFC IDs interleaved and
20    /// collided. A thread-local counter gives each pass its own sequence — a single
21    /// pass is single-threaded (`LayoutContext` holds non-`Sync` `RefCell` caches),
22    /// so IDs stay deterministic and stable across frames while never colliding
23    /// across concurrent passes.
24    static IFC_ID_COUNTER: Cell<u32> = const { Cell::new(0) };
25}
26
27/// Unique identifier for an Inline Formatting Context (IFC).
28///
29/// An IFC represents a region where inline content (text, inline-blocks, images)
30/// is laid out together. One IFC can contain content from multiple DOM nodes
31/// (e.g., `<p>Hello <span>world</span>!</p>` is one IFC with 3 text runs).
32///
33/// The ID is generated using a per-thread counter that resets at the start
34/// of each layout pass. This ensures:
35/// - IDs are unique within a layout pass
36/// - The same logical IFC gets the same ID across frames (for selection stability)
37/// - Concurrent `layout_document` passes on different threads can't collide
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
39pub struct IfcId(pub u32);
40
41impl IfcId {
42    /// Generate a new unique IFC ID (within the current thread's layout pass).
43    #[must_use] pub fn unique() -> Self {
44        IFC_ID_COUNTER.with(|c| {
45            let v = c.get();
46            c.set(v.wrapping_add(1));
47            Self(v)
48        })
49    }
50
51    /// Reset the IFC ID counter. Called at the start of each layout pass.
52    pub fn reset_counter() {
53        IFC_ID_COUNTER.with(|c| c.set(0));
54    }
55}
56
57/// Tracks a layout node's membership in an Inline Formatting Context.
58///
59/// Text nodes don't store their own `inline_layout_result` - instead, they
60/// participate in their parent's IFC. This struct provides the link from
61/// a text node back to its IFC's layout data.
62///
63/// # Architecture
64///
65/// ```text
66/// DOM:  <p>Hello <span>world</span>!</p>
67///
68/// Layout Tree:
69/// ├── LayoutNode (p) - IFC root
70/// │   └── inline_layout_result: Some(UnifiedLayout)
71/// │   └── ifc_id: IfcId(5)
72/// │
73/// ├── LayoutNode (::text "Hello ")
74/// │   └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 0 })
75/// │
76/// ├── LayoutNode (span)
77/// │   └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 1 })
78/// │   └── LayoutNode (::text "world")
79/// │       └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 1 })
80/// │
81/// └── LayoutNode (::text "!")
82///     └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 2 })
83/// ```
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub struct IfcMembership {
86    /// The IFC ID this node's content was laid out in.
87    pub ifc_id: IfcId,
88    /// The index of the IFC root `LayoutNode` in the layout tree.
89    /// Used to quickly find the node with `inline_layout_result`.
90    pub ifc_root_layout_index: usize,
91    /// Which run index within the IFC corresponds to this node's text.
92    /// Maps to `ContentIndex::run_index` in the shaped items.
93    pub run_index: u32,
94}
95
96use azul_core::{
97    dom::{FormattingContext, NodeData, NodeId, NodeType},
98    geom::{LogicalPosition, LogicalRect, LogicalSize},
99    styled_dom::StyledDom,
100};
101use azul_css::{
102    corety::LayoutDebugMessage,
103    css::CssPropertyValue,
104    codegen::format::GetHash,
105    props::{
106        basic::{
107            pixel::DEFAULT_FONT_SIZE, PhysicalSize, PixelValue, PropertyContext, ResolutionContext,
108        },
109        layout::{
110            LayoutDisplay, LayoutFloat, LayoutHeight, LayoutMaxHeight, LayoutMaxWidth,
111            LayoutMinHeight, LayoutMinWidth, LayoutOverflow, LayoutPosition, LayoutWidth,
112            LayoutWritingMode,
113        },
114        property::{CssProperty, CssPropertyType},
115        style::{StyleTextAlign, StyleWhiteSpace},
116    },
117};
118use taffy::{Cache as TaffyCache, Layout, LayoutInput, LayoutOutput};
119
120#[cfg(feature = "text_layout")]
121use crate::text3;
122use crate::{
123    debug_log,
124    font::parsed::ParsedFont,
125    font_traits::{FontLoaderTrait, ParsedFontTrait, UnifiedLayout},
126    solver3::{
127        geometry::{BoxProps, IntrinsicSizes, PositionedRectangle},
128        getters::{
129            get_css_height, get_css_max_height, get_css_max_width, get_css_min_height,
130            get_css_min_width, get_css_width, get_direction_property as get_direction,
131            get_display_property, get_float, get_overflow_x,
132            get_overflow_y, get_position, get_text_align,
133            get_text_orientation_property as get_text_orientation,
134            get_white_space_property, get_writing_mode, MultiValue,
135        },
136        scrollbar::ScrollbarRequirements,
137        LayoutContext, Result,
138    },
139    text3::cache::AvailableSpace,
140};
141
142/// Represents the invalidation state of a layout node.
143///
144/// The states are ordered by severity, allowing for easy "upgrading" of the dirty state.
145/// A node marked for `Layout` does not also need to be marked for `Paint`.
146///
147/// Because this enum derives `PartialOrd` and `Ord`, you can directly compare variants:
148///
149/// - `DirtyFlag::Layout > DirtyFlag::Paint` is `true`
150/// - `DirtyFlag::Paint >= DirtyFlag::None` is `true`
151/// - `DirtyFlag::Paint < DirtyFlag::Layout` is `true`
152#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
153pub enum DirtyFlag {
154    /// The node's layout is valid and no repaint is needed. This is the "clean" state.
155    #[default]
156    None,
157    /// The node's geometry is valid, but its appearance (e.g., color) has changed.
158    /// Requires a display list update only.
159    Paint,
160    /// The node's geometry (size or position) is invalid.
161    /// Requires a full layout pass and a display list update.
162    Layout,
163}
164
165/// A hash that represents the content and style of a node PLUS all of its descendants.
166/// If two `SubtreeHashes` are equal, their entire subtrees are considered identical for layout
167/// purposes.
168#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
169pub struct SubtreeHash(pub u64);
170
171/// Per-item metrics cached from the last IFC layout.
172///
173/// These metrics enable incremental IFC relayout (Phase 2 optimization):
174/// when a single inline item changes, we can check whether its advance width
175/// changed and potentially skip full line-breaking for unaffected lines.
176///
177/// Index in `CachedInlineLayout::item_metrics` matches the item order in
178/// `UnifiedLayout::items`.
179#[derive(Copy, Debug, Clone)]
180pub struct InlineItemMetrics {
181    /// The DOM `NodeId` of the source node for this item (for dirty checking).
182    /// `None` for generated content (list markers, hyphens, etc.)
183    pub source_node_id: Option<NodeId>,
184    /// Advance width of this item (glyph run width, inline-block width, etc.)
185    pub advance_width: f32,
186    /// Advance height contribution from this item to its line box.
187    pub line_height_contribution: f32,
188    /// Whether this item can participate in line breaking.
189    /// `false` for items inside `white-space: nowrap` or `white-space: pre`.
190    pub can_break: bool,
191    /// Which line this item was placed on (0-indexed).
192    pub line_index: u32,
193    /// X offset within its line.
194    pub x_offset: f32,
195}
196
197/// Cached inline layout result with the constraints used to compute it.
198///
199/// This structure solves a fundamental architectural problem: inline layouts
200/// (text wrapping, inline-block positioning) depend on the available width.
201/// Different layout phases may compute the layout with different widths:
202///
203/// 1. **Min-content measurement**: width = `MinContent` (effectively 0)
204/// 2. **Max-content measurement**: width = `MaxContent` (effectively infinite)
205/// 3. **Final layout**: width = `Definite(actual_column_width)`
206///
207/// Without tracking which constraints were used, a cached result from phase 1
208/// would incorrectly be reused in phase 3, causing text to wrap at the wrong
209/// positions (the root cause of table cell width bugs).
210///
211/// By storing the constraints alongside the result, we can:
212/// - Invalidate the cache when constraints change
213/// - Keep multiple cached results for different constraint types if needed
214/// - Ensure the final render always uses a layout computed with correct widths
215#[derive(Debug, Clone)]
216pub struct CachedInlineLayout {
217    /// The computed inline layout
218    pub layout: Arc<UnifiedLayout>,
219    /// The available width constraint used to compute this layout.
220    /// This is the key for cache validity checking.
221    /// +spec:writing-modes:1dcba2 - "available width" (CSS2.1) = auto size in inline axis
222    pub available_width: AvailableSpace,
223    /// Whether this layout was computed with float exclusions.
224    /// Float-aware layouts should not be overwritten by non-float layouts.
225    pub has_floats: bool,
226    /// The full constraints used to compute this layout.
227    /// Used for quick relayout after text edits without rebuilding from CSS.
228    pub constraints: Option<UnifiedConstraints>,
229    /// Per-item metrics for incremental IFC relayout (Phase 2).
230    ///
231    /// Each entry corresponds to one `PositionedItem` in `layout.items`.
232    /// These metrics enable the IFC relayout decision tree:
233    /// - Check if a dirty node's `advance_width` changed → skip repositioning if not
234    /// - Use `can_break` + `line_index` for the nowrap fast path
235    /// - Use `x_offset` for shifting subsequent items without full line-breaking
236    pub item_metrics: Vec<InlineItemMetrics>,
237    /// Cached line break boundaries for incremental relayout.
238    /// Enables checking if a width change fits on the same line without
239    /// re-running the full line-breaking algorithm.
240    pub line_breaks: Option<crate::text3::cache::CachedLineBreaks>,
241    /// Hash of the `InlineContent` this layout was shaped from. The Phase 2d
242    /// fast-path reuse in fc.rs keys cache validity on WIDTH only; without this,
243    /// a same-width `RefreshDom` whose text CHANGED would reuse the stale shaped
244    /// layout (#11 stale display list). 0 = unknown ⇒ never fast-path-reuse.
245    pub inline_content_hash: u64,
246}
247
248impl CachedInlineLayout {
249    /// Creates a new cached inline layout.
250    #[must_use] pub fn new(
251        layout: Arc<UnifiedLayout>,
252        available_width: AvailableSpace,
253        has_floats: bool,
254    ) -> Self {
255        let item_metrics = Self::extract_item_metrics(&layout);
256        Self {
257            layout,
258            available_width,
259            has_floats,
260            constraints: None,
261            item_metrics,
262            line_breaks: None,
263            inline_content_hash: 0,
264        }
265    }
266
267    /// Creates a new cached inline layout with full constraints.
268    #[must_use] pub fn new_with_constraints(
269        layout: Arc<UnifiedLayout>,
270        available_width: AvailableSpace,
271        has_floats: bool,
272        constraints: UnifiedConstraints,
273    ) -> Self {
274        let item_metrics = Self::extract_item_metrics(&layout);
275        let available_width_px = match available_width {
276            AvailableSpace::Definite(w) => w,
277            _ => f32::MAX,
278        };
279        let line_breaks = Some(crate::text3::cache::extract_line_breaks(
280            &layout.items, available_width_px,
281        ));
282        Self {
283            layout,
284            available_width,
285            has_floats,
286            constraints: Some(constraints),
287            item_metrics,
288            line_breaks,
289            inline_content_hash: 0,
290        }
291    }
292
293    /// Extracts per-item metrics from a computed `UnifiedLayout`.
294    ///
295    /// This is called automatically by the constructors. The metrics
296    /// enable incremental IFC relayout in Phase 2c/2d by providing
297    /// cached advance widths, line assignments, and break information
298    /// for each positioned item.
299    #[allow(clippy::cast_possible_truncation)] // bounded layout/render numeric cast
300    fn extract_item_metrics(layout: &UnifiedLayout) -> Vec<InlineItemMetrics> {
301        use crate::text3::cache::{ShapedItem, get_item_vertical_metrics_approx};
302
303        layout.items.iter().map(|positioned_item| {
304            let bounds = positioned_item.item.bounds();
305            let (ascent, descent) = get_item_vertical_metrics_approx(&positioned_item.item);
306
307            let source_node_id = match &positioned_item.item {
308                ShapedItem::Cluster(c) => c.source_node_id,
309                // Objects (inline-blocks, images) and other generated items
310                // don't expose source_node_id directly on ShapedItem.
311                // Phase 2c will refine this via the ContentIndex mapping.
312                ShapedItem::Object { .. }
313                | ShapedItem::CombinedBlock { .. }
314                | ShapedItem::Tab { .. }
315                | ShapedItem::Break { .. } => None,
316            };
317
318            // For Phase 2a, default can_break = true for all items.
319            // Phase 2c will refine this by checking the white-space property
320            // on the IFC root's style or the item's own style context.
321            // (Note: text3::StyleProperties doesn't carry white-space;
322            //  that's resolved at the IFC/BFC boundary level.)
323            let can_break = !matches!(&positioned_item.item, ShapedItem::Break { .. });
324
325            InlineItemMetrics {
326                source_node_id,
327                advance_width: bounds.width,
328                line_height_contribution: ascent + descent,
329                can_break,
330                line_index: positioned_item.line_index as u32,
331                x_offset: positioned_item.position.x,
332            }
333        }).collect()
334    }
335
336    /// Checks if this cached layout is valid for the given constraints.
337    ///
338    /// A cached layout is valid if:
339    /// 1. The available width matches (definite widths must be equal, or both are the same
340    ///    indefinite type)
341    /// 2. OR the new request doesn't have floats but the cached one does (keep float-aware layout)
342    ///
343    /// The second condition preserves float-aware layouts, which are more "correct" than
344    /// non-float layouts and shouldn't be overwritten.
345    #[must_use] pub fn is_valid_for(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
346        // A cached layout with NO floats must not be reused when the new request DOES
347        // have floats: the line boxes have to re-wrap around the float exclusions.
348        // Without this, the two-pass IFC path (no-float sizing pass, then float-aware
349        // re-layout) reuses the pass-1 no-float line breaks and text ignores the float
350        // entirely (#19). Mirrors should_replace_with()'s gain-float branch so the two
351        // stay consistent.
352        if new_has_floats && !self.has_floats {
353            return false;
354        }
355
356        // If we have a float-aware layout and the new request doesn't have floats,
357        // keep the float-aware layout (it's more accurate)
358        if self.has_floats && !new_has_floats {
359            // But only if the width constraint type matches
360            return self.width_constraint_matches(new_width);
361        }
362
363        // Otherwise, require exact width match
364        self.width_constraint_matches(new_width)
365    }
366
367    /// Tolerance for comparing definite layout widths (in logical pixels).
368    /// Sub-pixel differences below this threshold are treated as identical
369    /// to avoid unnecessary relayout from floating-point rounding.
370    const LAYOUT_WIDTH_EPSILON: f32 = 0.1;
371
372    /// Checks if the width constraint matches.
373    #[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
374    fn width_constraint_matches(&self, new_width: AvailableSpace) -> bool {
375        match (self.available_width, new_width) {
376            // Definite widths must match within a small epsilon
377            (AvailableSpace::Definite(old), AvailableSpace::Definite(new)) => {
378                (old - new).abs() < Self::LAYOUT_WIDTH_EPSILON
379            }
380            // MinContent matches MinContent
381            (AvailableSpace::MinContent, AvailableSpace::MinContent) => true,
382            // MaxContent matches MaxContent
383            (AvailableSpace::MaxContent, AvailableSpace::MaxContent) => true,
384            // Different constraint types don't match
385            _ => false,
386        }
387    }
388
389    /// Determines if this cached layout should be replaced by a new layout.
390    ///
391    /// Returns true if the new layout should replace this one.
392    #[must_use] pub fn should_replace_with(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
393        // Always replace if we gain float information
394        if new_has_floats && !self.has_floats {
395            return true;
396        }
397
398        // Replace if width constraint changed
399        !self.width_constraint_matches(new_width)
400    }
401
402    /// Returns a reference to the inner `UnifiedLayout`.
403    ///
404    /// This is a convenience method for code that only needs the layout data
405    /// and doesn't care about the caching metadata.
406    #[inline]
407    #[must_use] pub const fn get_layout(&self) -> &Arc<UnifiedLayout> {
408        &self.layout
409    }
410
411    /// Returns a clone of the inner Arc<UnifiedLayout>.
412    ///
413    /// This is useful for APIs that need to return an owned reference
414    /// to the layout without exposing the caching metadata.
415    #[inline]
416    #[must_use] pub fn clone_layout(&self) -> Arc<UnifiedLayout> {
417        self.layout.clone()
418    }
419}
420
421/// A layout tree node representing the CSS box model.
422///
423/// ## Memory Layout Optimization (`#[repr(C)]`)
424///
425/// Fields are ordered by access frequency (hottest first) to maximize CPU
426/// cache line utilization during tree traversal. With `#[repr(C)]`, the
427/// compiler preserves this ordering. The 6 hottest fields (~140 bytes)
428/// occupy the first 2-3 cache lines (64 bytes each), which are loaded
429/// first by the hardware prefetcher.
430///
431/// | Tier   | Fields                                  | ~Bytes | Accesses |
432/// |--------|-----------------------------------------|--------|----------|
433/// | HOT    | `box_props`, `dom_node_id`, children,       |  ~140  |  410+    |
434/// |        | `used_size`, `formatting_context`, parent    |        |          |
435/// | WARM   | `intrinsic_sizes..computed_style`          |  ~220  |  ~80     |
436/// | COLD   | `dirty_flag..is_anonymous`                 |  ~190  |  ~20     |
437///
438/// Note: An absolute position is a final paint-time value and shouldn't be
439/// cached on the node itself, as it can change even if the node's
440/// layout is clean (e.g., if a sibling changes size). We will calculate
441/// it in a separate map.
442#[derive(Debug, Clone)]
443#[repr(C)]
444pub struct LayoutNode {
445    // ── HOT tier: accessed on every node in every layout pass ────────────
446    // These fields should fit in the first 2-3 cache lines (~128-192 bytes).
447
448    /// The resolved box model properties (margin, border, padding)
449    /// in logical pixels. Cached after first resolution.
450    /// (148 accesses — hottest field)
451    pub box_props: BoxProps,
452    /// Reference back to the original DOM node (None for anonymous boxes)
453    /// (111 accesses)
454    pub dom_node_id: Option<NodeId>,
455    /// Children indices in the layout tree
456    /// (53 accesses)
457    pub children: Vec<usize>,
458    /// The size used during the last layout pass.
459    /// (43 accesses)
460    pub used_size: Option<LogicalSize>,
461    /// The formatting context this node establishes or participates in.
462    /// (30 accesses)
463    pub formatting_context: FormattingContext,
464    /// Parent index (None for root)
465    /// (25 accesses)
466    pub parent: Option<usize>,
467
468    // ── WARM tier: frequently accessed but not on every node ─────────────
469
470    /// Cached intrinsic sizes (min-content, max-content, etc.)
471    /// (16 accesses — sizing pass only)
472    pub intrinsic_sizes: Option<IntrinsicSizes>,
473    // +spec:display-property:af3a89 - alignment baseline for inline-level boxes
474    /// The baseline of this box, if applicable, measured from its content-box top edge.
475    /// (14 accesses — IFC/table alignment)
476    pub baseline: Option<f32>,
477    /// Cached inline layout result with the constraints used to compute it.
478    ///
479    /// This field stores both the computed layout AND the constraints (available width,
480    /// float state) under which it was computed. This is essential for correctness:
481    /// 
482    /// - Table cells are measured multiple times with different widths
483    /// - Min-content/max-content intrinsic sizing uses special constraint values
484    /// - The final layout must use the actual available width, not a measurement width
485    ///
486    /// By tracking the constraints, we avoid the bug where a min-content measurement
487    /// (with width=0) would be incorrectly reused for final rendering.
488    /// (13 accesses — IFC roots / table cells)
489    pub inline_layout_result: Option<CachedInlineLayout>,
490    /// Cached scrollbar information (calculated during layout)
491    /// Used to determine if scrollbars appeared/disappeared requiring reflow
492    /// (12 accesses — scrollable containers only)
493    pub scrollbar_info: Option<ScrollbarRequirements>,
494    /// The position of this node *relative to its parent's content box*.
495    /// (9 accesses — positioning pass)
496    pub relative_position: Option<LogicalPosition>,
497    /// The actual content size (children overflow size) for scrollable containers.
498    /// This is the size of all content that might need to be scrolled, which can
499    /// be larger than `used_size` when content overflows the container.
500    /// (7 accesses — scrollable containers)
501    pub overflow_content_size: Option<LogicalSize>,
502    /// Cache for Taffy layout computations for this node.
503    /// (6 accesses — Taffy bridge)
504    pub taffy_cache: TaffyCache,
505    /// Pre-computed CSS properties needed during layout.
506    /// Computed once during layout tree build to avoid repeated style lookups.
507    /// (5 accesses — cache.rs only)
508    pub computed_style: ComputedLayoutStyle,
509    /// Pseudo-element type (`::marker`, `::before`, `::after`) if this node is a pseudo-element
510    /// (5 accesses — pseudo-elements only)
511    pub pseudo_element: Option<PseudoElement>,
512    /// Escaped top margin (CSS 2.1 margin collapsing)
513    /// If this BFC's first child's top margin "escaped" the BFC, this contains
514    /// the collapsed margin that should be applied by the parent.
515    /// (4 accesses — BFC margin collapsing)
516    pub escaped_top_margin: Option<f32>,
517    /// Escaped bottom margin (CSS 2.1 margin collapsing)\
518    /// If this BFC's last child's bottom margin "escaped" the BFC, this contains
519    /// the collapsed margin that should be applied by the parent.
520    /// (4 accesses)
521    pub escaped_bottom_margin: Option<f32>,
522    /// Parent's formatting context (needed to determine if stretch applies)
523    /// (4 accesses — flex/grid children)
524    pub parent_formatting_context: Option<FormattingContext>,
525    /// If this node participates in an IFC (is inline content like text),
526    /// stores the reference back to the IFC root and the run index.
527    /// This allows text nodes to find their layout data in the parent's IFC.
528    /// (3 accesses — text nodes only)
529    pub ifc_membership: Option<IfcMembership>,
530    /// The layout tree index of this node's containing block.
531    /// - For abs-pos elements: nearest positioned (non-static) ancestor
532    /// - For fixed elements: root / None (viewport)
533    /// - For normal-flow: parent (None = implicit)
534    ///   Used for clip exemption: abs-pos elements whose containing block
535    ///   is above an overflow clipper should not be clipped.
536    pub containing_block_index: Option<usize>,
537
538    // ── COLD tier: construction / reconciliation / debugging only ────────
539
540    /// Type of anonymous box (if applicable)
541    /// (2 accesses)
542    pub anonymous_type: Option<AnonymousBoxType>,
543    /// Multi-field fingerprint of this node's data (style, text, etc.)
544    /// for granular change detection during reconciliation.
545    /// (2 accesses — reconciliation only)
546    pub node_data_fingerprint: NodeDataFingerprint,
547    /// A hash of this node's data and all of its descendants. Used for
548    /// fast reconciliation.
549    /// (9 accesses — all in cache.rs reconciliation)
550    pub subtree_hash: SubtreeHash,
551    /// Dirty flags to track what needs recalculation.
552    /// (7 accesses — reconciliation setup)
553    pub dirty_flag: DirtyFlag,
554    /// Unresolved box model properties (raw CSS values).
555    /// These are resolved lazily during layout when containing block is known.
556    /// (1 access — initial resolution only)
557    pub unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps,
558    /// If this node is an IFC root, stores the IFC ID.
559    /// Used to identify which IFC this node's `inline_layout_result` belongs to.
560    /// (1 access — IFC creation only)
561    pub ifc_id: Option<IfcId>,
562}
563
564/// Pre-computed CSS properties needed during layout.
565/// 
566/// This struct stores resolved CSS values that are frequently accessed during
567/// layout calculations. By computing these once during layout tree construction,
568/// we avoid O(n * m) style lookups where n = nodes and m = layout passes.
569///
570/// All values are resolved to their final form (no 'inherit', 'initial', etc.)
571#[derive(Debug, Clone, Default)]
572pub struct ComputedLayoutStyle {
573    /// CSS `display` property
574    pub display: LayoutDisplay,
575    /// CSS `position` property
576    pub position: LayoutPosition,
577    /// CSS `float` property
578    pub float: LayoutFloat,
579    /// CSS `overflow-x` property
580    pub overflow_x: LayoutOverflow,
581    /// CSS `overflow-y` property
582    pub overflow_y: LayoutOverflow,
583    /// CSS `writing-mode` property
584    pub writing_mode: azul_css::props::layout::LayoutWritingMode,
585    /// CSS `direction` property (ltr/rtl)
586    pub direction: azul_css::props::style::StyleDirection,
587    /// CSS `text-orientation` property (for vertical writing modes)
588    pub text_orientation: azul_css::props::style::effects::StyleTextOrientation,
589    /// CSS `width` property (None = auto)
590    pub width: Option<azul_css::props::layout::LayoutWidth>,
591    /// CSS `height` property (None = auto)
592    pub height: Option<azul_css::props::layout::LayoutHeight>,
593    /// CSS `min-width` property
594    pub min_width: Option<azul_css::props::layout::LayoutMinWidth>,
595    /// CSS `min-height` property
596    pub min_height: Option<azul_css::props::layout::LayoutMinHeight>,
597    /// CSS `max-width` property
598    pub max_width: Option<azul_css::props::layout::LayoutMaxWidth>,
599    /// CSS `max-height` property
600    pub max_height: Option<azul_css::props::layout::LayoutMaxHeight>,
601    /// CSS `text-align` property
602    pub text_align: azul_css::props::style::StyleTextAlign,
603}
604
605// Note: LayoutNode methods that cross hot/warm/cold boundaries have been
606// moved to LayoutTree methods (resolve_box_props, get_content_size).
607
608/// CSS pseudo-elements that can be generated
609#[derive(Debug, Clone, Copy, PartialEq, Eq)]
610pub enum PseudoElement {
611    /// `::marker` pseudo-element for list items
612    Marker,
613    /// `::before` pseudo-element
614    Before,
615    /// `::after` pseudo-element
616    After,
617}
618
619// +spec:display-property:b7f4bf - anonymous inline/block boxes are both called "anonymous boxes"
620/// Types of anonymous boxes that can be generated
621// +spec:display-property:ae4f16 - anonymous boxes are treated as descendants alongside pseudo-elements
622#[derive(Debug, Clone, Copy, PartialEq, Eq)]
623pub enum AnonymousBoxType {
624    /// Anonymous block box wrapping inline content
625    InlineWrapper,
626    /// Anonymous box for a list item marker (bullet or number)
627    /// DEPRECATED: Use `PseudoElement::Marker` instead
628    ListItemMarker,
629    /// Anonymous table wrapper
630    TableWrapper,
631    /// Anonymous table row group (tbody)
632    TableRowGroup,
633    /// Anonymous table row
634    TableRow,
635    /// Anonymous table cell
636    TableCell,
637}
638
639// =============================================================================
640// SoA (struct-of-arrays) layout node split for cache performance
641// =============================================================================
642
643/// Hot layout node fields — accessed on every node in every layout pass.
644///
645/// Stored in a separate `Vec` for cache locality. At ~100 bytes per node,
646/// 1000 nodes fit in ~100 KB (L2 cache), vs ~550 KB with the monolithic struct.
647// ~100B per-node hot type stored/moved in Vecs across every layout pass; kept
648// non-Copy on purpose so it isn't silently bulk-copied (Copy would mask the
649// cost and churn the many `.clone()` call sites).
650#[allow(missing_copy_implementations)]
651#[derive(Debug, Clone)]
652pub struct LayoutNodeHot {
653    /// The resolved box model properties (margin, border, padding)
654    /// Stored in packed i16×10 encoding to reduce cache footprint.
655    /// Use `box_props.unpack()` to get f32 `ResolvedBoxProps` for computation.
656    pub box_props: crate::solver3::geometry::PackedBoxProps,
657    /// Reference back to the original DOM node (None for anonymous boxes)
658    pub dom_node_id: Option<NodeId>,
659    /// The size used during the last layout pass.
660    pub used_size: Option<LogicalSize>,
661    /// The formatting context this node establishes or participates in.
662    pub formatting_context: FormattingContext,
663    /// Parent index (None for root)
664    pub parent: Option<usize>,
665}
666
667/// Warm layout node fields — accessed frequently but not on every node.
668///
669/// Stored in a separate `Vec`. These fields are accessed during specific
670/// layout phases (sizing, IFC, table alignment) but not during the main
671/// constraint-solving loop.
672#[derive(Debug, Clone, Default)]
673pub struct LayoutNodeWarm {
674    /// Cached intrinsic sizes (min-content, max-content, etc.)
675    pub intrinsic_sizes: Option<IntrinsicSizes>,
676    /// The baseline of this box, measured from its content-box top edge.
677    pub baseline: Option<f32>,
678    /// Cached inline layout result with the constraints used to compute it.
679    pub inline_layout_result: Option<CachedInlineLayout>,
680    /// Cached scrollbar information
681    pub scrollbar_info: Option<ScrollbarRequirements>,
682    /// The position relative to parent's content box.
683    pub relative_position: Option<LogicalPosition>,
684    /// The actual content size for scrollable containers.
685    pub overflow_content_size: Option<LogicalSize>,
686    /// Cache for Taffy layout computations.
687    pub taffy_cache: TaffyCache,
688    /// Pre-computed CSS properties needed during layout.
689    pub computed_style: ComputedLayoutStyle,
690    /// Pseudo-element type if this node is a pseudo-element
691    pub pseudo_element: Option<PseudoElement>,
692    /// Escaped top margin (CSS 2.1 margin collapsing)
693    pub escaped_top_margin: Option<f32>,
694    /// Escaped bottom margin (CSS 2.1 margin collapsing)
695    pub escaped_bottom_margin: Option<f32>,
696    /// Parent's formatting context
697    pub parent_formatting_context: Option<FormattingContext>,
698    /// IFC membership for text nodes
699    pub ifc_membership: Option<IfcMembership>,
700    /// Containing block index for clip exemption
701    pub containing_block_index: Option<usize>,
702}
703
704/// Cold layout node fields — construction / reconciliation / debugging only.
705///
706/// Stored in a separate `Vec`. These fields are rarely accessed during layout;
707/// mostly used during tree construction, reconciliation, and dirty tracking.
708#[derive(Debug, Clone)]
709#[derive(Default)]
710pub struct LayoutNodeCold {
711    /// Type of anonymous box (if applicable)
712    pub anonymous_type: Option<AnonymousBoxType>,
713    /// Multi-field fingerprint for granular change detection.
714    pub node_data_fingerprint: NodeDataFingerprint,
715    /// Hash of this node's data + all descendants.
716    pub subtree_hash: SubtreeHash,
717    /// Dirty flags for recalculation tracking.
718    pub dirty_flag: DirtyFlag,
719    /// Unresolved box model properties (raw CSS values).
720    pub unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps,
721    /// IFC ID if this node is an IFC root.
722    pub ifc_id: Option<IfcId>,
723}
724
725
726impl LayoutNode {
727    /// Split this full layout node into hot/warm/cold components.
728    /// Used during `LayoutTreeBuilder::build()` to create the `SoA` layout.
729    #[must_use] pub fn split(self) -> (LayoutNodeHot, LayoutNodeWarm, LayoutNodeCold) {
730        (
731            LayoutNodeHot {
732                box_props: crate::solver3::geometry::PackedBoxProps::pack(&self.box_props),
733                dom_node_id: self.dom_node_id,
734                used_size: self.used_size,
735                formatting_context: self.formatting_context,
736                parent: self.parent,
737            },
738            LayoutNodeWarm {
739                intrinsic_sizes: self.intrinsic_sizes,
740                baseline: self.baseline,
741                inline_layout_result: self.inline_layout_result,
742                scrollbar_info: self.scrollbar_info,
743                relative_position: self.relative_position,
744                overflow_content_size: self.overflow_content_size,
745                taffy_cache: self.taffy_cache,
746                computed_style: self.computed_style,
747                pseudo_element: self.pseudo_element,
748                escaped_top_margin: self.escaped_top_margin,
749                escaped_bottom_margin: self.escaped_bottom_margin,
750                parent_formatting_context: self.parent_formatting_context,
751                ifc_membership: self.ifc_membership,
752                containing_block_index: self.containing_block_index,
753            },
754            LayoutNodeCold {
755                anonymous_type: self.anonymous_type,
756                node_data_fingerprint: self.node_data_fingerprint,
757                subtree_hash: self.subtree_hash,
758                dirty_flag: self.dirty_flag,
759                unresolved_box_props: self.unresolved_box_props,
760                ifc_id: self.ifc_id,
761            },
762        )
763    }
764}
765
766/// The complete layout tree structure.
767///
768/// Uses a struct-of-arrays (`SoA`) layout for cache performance:
769/// - `nodes` (hot): accessed on every node in every layout pass
770/// - `warm`: accessed during specific layout phases
771/// - `cold`: construction / reconciliation only
772#[derive(Debug, Clone)]
773pub struct LayoutTree {
774    /// Hot layout data — box props, parent, `used_size`, formatting context
775    pub nodes: Vec<LayoutNodeHot>,
776    /// Warm layout data — intrinsic sizes, baseline, inline layout, etc.
777    pub warm: Vec<LayoutNodeWarm>,
778    /// Cold layout data — dirty flags, fingerprints, reconciliation data
779    pub cold: Vec<LayoutNodeCold>,
780    /// Root node index
781    pub root: usize,
782    /// Mapping from DOM node IDs to layout node indices
783    // BTreeMap (not HashMap): std HashMap's RandomState hasher needs an RNG seed
784    // that isn't available in the remill-lifted wasm (no getrandom), so inserts
785    // silently no-op there — dom_to_layout came back empty (node mapping lost,
786    // get_node_size/position returned None → 0-rects). BTreeMap is deterministic,
787    // matches the rest of azul-core, and lifts reliably (M12.7).
788    pub dom_to_layout: BTreeMap<NodeId, Vec<usize>>,
789    /// Flat arena holding all children indices contiguously.
790    pub children_arena: Vec<usize>,
791    /// Per-node (start, len) into `children_arena`. Indexed by node index.
792    pub children_offsets: Vec<(u32, u32)>,
793    /// Per-node bit: this node or any descendant establishes a shrink-to-fit
794    /// (STF) context whose sizing algorithm reads children's intrinsic sizes
795    /// (flex/grid/table/inline-block containers, floats, or abspos elements).
796    ///
797    /// If `subtree_needs_intrinsic[i]` is false AND no ancestor of `i` is STF
798    /// either, the intrinsic sizing pass can skip the entire subtree — nothing
799    /// will ever read those values. This is the static-DOM optimization from
800    /// §58 Win #3 (the "safely re-enabled Fix C").
801    ///
802    /// Computed once at tree build time in `generate_layout_tree`. An empty
803    /// vec means "assume every subtree needs intrinsics" (safe fallback for
804    /// code paths that construct `LayoutTree` without going through the
805    /// builder — currently none, but preserves the invariant for tests).
806    pub subtree_needs_intrinsic: Vec<bool>,
807}
808
809/// Approximate per-field heap-byte breakdown of a [`LayoutTree`].
810#[derive(Copy, Debug, Clone, Default)]
811pub struct LayoutTreeMemoryReport {
812    pub node_count: usize,
813    pub hot_bytes: usize,
814    pub warm_bytes: usize,
815    pub warm_inline_layout_bytes: usize,
816    pub warm_taffy_cache_bytes: usize,
817    pub cold_bytes: usize,
818    pub dom_to_layout_bytes: usize,
819    pub children_arena_bytes: usize,
820    pub children_offsets_bytes: usize,
821}
822
823impl LayoutTreeMemoryReport {
824    #[must_use] pub const fn total_bytes(&self) -> usize {
825        self.hot_bytes
826            + self.warm_bytes
827            + self.warm_inline_layout_bytes
828            + self.warm_taffy_cache_bytes
829            + self.cold_bytes
830            + self.dom_to_layout_bytes
831            + self.children_arena_bytes
832            + self.children_offsets_bytes
833    }
834}
835
836impl LayoutTree {
837    /// Approximate heap bytes retained by this `LayoutTree`.
838    #[must_use] pub fn memory_report(&self) -> LayoutTreeMemoryReport {
839        let mut report = LayoutTreeMemoryReport {
840            node_count: self.nodes.len(),
841            hot_bytes: self.nodes.capacity() * size_of::<LayoutNodeHot>(),
842            warm_bytes: self.warm.capacity() * size_of::<LayoutNodeWarm>(),
843            cold_bytes: self.cold.capacity() * size_of::<LayoutNodeCold>(),
844            children_arena_bytes: self.children_arena.capacity() * size_of::<usize>(),
845            children_offsets_bytes: self.children_offsets.capacity() * size_of::<(u32, u32)>(),
846            dom_to_layout_bytes: 0,
847            warm_inline_layout_bytes: 0,
848            warm_taffy_cache_bytes: 0,
849        };
850        // HashMap<NodeId, Vec<usize>> — approximate: (key + Vec-header) per entry
851        // plus heap for each inner Vec.
852        let entries = self.dom_to_layout.len();
853        report.dom_to_layout_bytes = entries * (size_of::<NodeId>() + size_of::<Vec<usize>>());
854        for v in self.dom_to_layout.values() {
855            report.dom_to_layout_bytes += v.capacity() * size_of::<usize>();
856        }
857        // Inline layout data lives behind Arc — count Arc heap-shares once
858        // per node that has a cached layout. Counted conservatively.
859        for w in &self.warm {
860            if let Some(cached) = &w.inline_layout_result {
861                // Arc<UnifiedLayout> — count the UnifiedLayout header + its items.
862                report.warm_inline_layout_bytes += size_of::<UnifiedLayout>();
863                report.warm_inline_layout_bytes += cached.layout.items.capacity()
864                    * size_of::<crate::text3::cache::PositionedItem>();
865                report.warm_inline_layout_bytes += cached.item_metrics.capacity()
866                    * size_of::<InlineItemMetrics>();
867                // Glyph bytes inside ShapedItem::Cluster — unbounded but bounded
868                // per entry. Approximate by counting clusters × 32 bytes/glyph.
869                for item in &cached.layout.items {
870                    if let crate::text3::cache::ShapedItem::Cluster(c) = &item.item {
871                        report.warm_inline_layout_bytes += c.glyphs.capacity()
872                            * size_of::<crate::text3::cache::ShapedGlyph>();
873                        report.warm_inline_layout_bytes += c.text.capacity();
874                    }
875                }
876            }
877            // Taffy cache — each slot is an Option, ~50 B empty
878            report.warm_taffy_cache_bytes += size_of::<TaffyCache>();
879        }
880        report
881    }
882
883    /// Returns the children of node `index` as a contiguous slice from the arena.
884    #[inline]
885    #[must_use] pub fn children(&self, index: usize) -> &[usize] {
886        if let Some(&(start, len)) = self.children_offsets.get(index) {
887            &self.children_arena[(start as usize)..((start as usize) + (len as usize))]
888        } else {
889            &[]
890        }
891    }
892
893    /// Get hot layout data for a node (`box_props`, `dom_node_id`, `used_size`, etc.)
894    #[inline]
895    #[must_use] pub fn get(&self, index: usize) -> Option<&LayoutNodeHot> {
896        self.nodes.get(index)
897    }
898
899    /// Get mutable hot layout data for a node.
900    #[inline]
901    pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNodeHot> {
902        self.nodes.get_mut(index)
903    }
904
905    /// Get warm layout data for a node (`intrinsic_sizes`, baseline, `inline_layout`, etc.)
906    #[inline]
907    #[must_use] pub fn warm(&self, index: usize) -> Option<&LayoutNodeWarm> {
908        self.warm.get(index)
909    }
910
911    /// Get mutable warm layout data for a node.
912    #[inline]
913    pub fn warm_mut(&mut self, index: usize) -> Option<&mut LayoutNodeWarm> {
914        self.warm.get_mut(index)
915    }
916
917    /// Get cold layout data for a node (`dirty_flag`, `subtree_hash`, fingerprint, etc.)
918    #[inline]
919    #[must_use] pub fn cold(&self, index: usize) -> Option<&LayoutNodeCold> {
920        self.cold.get(index)
921    }
922
923    /// Get mutable cold layout data for a node.
924    #[inline]
925    pub fn cold_mut(&mut self, index: usize) -> Option<&mut LayoutNodeCold> {
926        self.cold.get_mut(index)
927    }
928
929    fn root_node(&self) -> &LayoutNodeHot {
930        &self.nodes[self.root]
931    }
932
933    /// Reconstruct a full `LayoutNode` from the split hot/warm/cold arrays.
934    ///
935    /// Used when passing node data to `LayoutTreeBuilder::clone_node_from_old()`.
936    #[must_use] pub fn get_full_node(&self, index: usize) -> Option<LayoutNode> {
937        let hot = self.nodes.get(index)?;
938        let warm = self.warm.get(index).cloned().unwrap_or_default();
939        let cold = self.cold.get(index).cloned().unwrap_or_default();
940        let children = self.children(index).to_vec();
941        Some(LayoutNode {
942            box_props: hot.box_props.unpack(),
943            dom_node_id: hot.dom_node_id,
944            children,
945            used_size: hot.used_size,
946            formatting_context: hot.formatting_context,
947            parent: hot.parent,
948            intrinsic_sizes: warm.intrinsic_sizes,
949            baseline: warm.baseline,
950            inline_layout_result: warm.inline_layout_result,
951            scrollbar_info: warm.scrollbar_info,
952            relative_position: warm.relative_position,
953            overflow_content_size: warm.overflow_content_size,
954            taffy_cache: warm.taffy_cache,
955            computed_style: warm.computed_style,
956            pseudo_element: warm.pseudo_element,
957            escaped_top_margin: warm.escaped_top_margin,
958            escaped_bottom_margin: warm.escaped_bottom_margin,
959            parent_formatting_context: warm.parent_formatting_context,
960            ifc_membership: warm.ifc_membership,
961            containing_block_index: warm.containing_block_index,
962            anonymous_type: cold.anonymous_type,
963            node_data_fingerprint: cold.node_data_fingerprint,
964            subtree_hash: cold.subtree_hash,
965            dirty_flag: cold.dirty_flag,
966            unresolved_box_props: cold.unresolved_box_props,
967            ifc_id: cold.ifc_id,
968        })
969    }
970
971    /// Re-resolve box properties for a node with the actual containing block size.
972    fn resolve_box_props(
973        &mut self,
974        node_index: usize,
975        containing_block: LogicalSize,
976        viewport_size: LogicalSize,
977        element_font_size: f32,
978        root_font_size: f32,
979    ) {
980        let params = crate::solver3::geometry::ResolutionParams {
981            containing_block,
982            viewport_size,
983            element_font_size,
984            root_font_size,
985        };
986        if let (Some(hot), Some(cold)) = (self.nodes.get_mut(node_index), self.cold.get(node_index)) {
987            hot.box_props = crate::solver3::geometry::PackedBoxProps::pack(&cold.unresolved_box_props.resolve(&params));
988        }
989    }
990
991    /// Marks a node and its ancestors as dirty with the given flag.
992    pub fn mark_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
993        if flag == DirtyFlag::None {
994            return;
995        }
996
997        let mut current_index = Some(start_index);
998        while let Some(index) = current_index {
999            let Some(cold) = self.cold.get_mut(index) else {
1000                break;
1001            };
1002            if cold.dirty_flag >= flag {
1003                break;
1004            }
1005            cold.dirty_flag = flag;
1006            current_index = self.nodes.get(index).and_then(|n| n.parent);
1007        }
1008    }
1009
1010    /// Marks a node and its entire subtree of descendants with the given dirty flag.
1011    fn mark_subtree_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
1012        if flag == DirtyFlag::None {
1013            return;
1014        }
1015
1016        let mut stack = vec![start_index];
1017        while let Some(index) = stack.pop() {
1018            let children = self.children(index).to_vec();
1019            if let Some(cold) = self.cold.get_mut(index) {
1020                if cold.dirty_flag < flag {
1021                    cold.dirty_flag = flag;
1022                }
1023                stack.extend_from_slice(&children);
1024            }
1025        }
1026    }
1027
1028    /// Resets the dirty flags of all nodes in the tree to `None` after layout is complete.
1029    fn clear_all_dirty_flags(&mut self) {
1030        for cold in &mut self.cold {
1031            cold.dirty_flag = DirtyFlag::None;
1032        }
1033    }
1034
1035    /// Get inline layout for a node, navigating through IFC membership if needed.
1036    #[must_use] pub fn get_inline_layout_for_node(&self, layout_index: usize) -> Option<&Arc<UnifiedLayout>> {
1037        let warm = self.warm.get(layout_index)?;
1038
1039        // First, check if this node has its own inline_layout_result (it's an IFC root)
1040        if let Some(cached) = &warm.inline_layout_result {
1041            return Some(cached.get_layout());
1042        }
1043
1044        // For text nodes, check if they have ifc_membership pointing to the IFC root
1045        if let Some(ifc_membership) = &warm.ifc_membership {
1046            let ifc_root_warm = self.warm.get(ifc_membership.ifc_root_layout_index)?;
1047            if let Some(cached) = &ifc_root_warm.inline_layout_result {
1048                return Some(cached.get_layout());
1049            }
1050        }
1051
1052        None
1053    }
1054
1055    /// Return the layout index of the IFC root that owns `layout_index`'s inline content.
1056    /// If the node IS an IFC root (has its own `inline_layout_result`) or has no
1057    /// `ifc_membership`, returns `layout_index` unchanged. Inline text nodes never get
1058    /// their own box position (it stays the `f32::MIN` sentinel) — their geometry lives
1059    /// in the IFC root's content box, so selection/inline painting must anchor to the
1060    /// IFC root's position, not the text node's. See `get_inline_layout_for_node`.
1061    #[must_use] pub fn get_ifc_root_layout_index(&self, layout_index: usize) -> usize {
1062        if let Some(warm) = self.warm.get(layout_index) {
1063            if warm.inline_layout_result.is_none() {
1064                if let Some(ifc_membership) = &warm.ifc_membership {
1065                    return ifc_membership.ifc_root_layout_index;
1066                }
1067            }
1068        }
1069        layout_index
1070    }
1071
1072    /// Get the content size of a node (for scrollbar calculations).
1073    #[must_use] pub fn get_content_size(&self, index: usize) -> LogicalSize {
1074        let Some(warm) = self.warm.get(index) else {
1075            return LogicalSize::default();
1076        };
1077
1078        if let Some(content_size) = warm.overflow_content_size {
1079            return content_size;
1080        }
1081
1082        let Some(hot) = self.nodes.get(index) else {
1083            return LogicalSize::default();
1084        };
1085
1086        let mut content_size = hot.used_size.unwrap_or_default();
1087
1088        if let Some(ref cached_layout) = warm.inline_layout_result {
1089            let text_layout = &cached_layout.layout;
1090            let mut max_x: f32 = 0.0;
1091            let mut max_y: f32 = 0.0;
1092            for positioned_item in &text_layout.items {
1093                let item_bounds = positioned_item.item.bounds();
1094                max_x = max_x.max(positioned_item.position.x + item_bounds.width);
1095                max_y = max_y.max(positioned_item.position.y + item_bounds.height);
1096            }
1097            content_size.width = content_size.width.max(max_x);
1098            content_size.height = content_size.height.max(max_y);
1099        }
1100
1101        content_size
1102    }
1103}
1104
1105/// Generate layout tree from styled DOM with proper anonymous box generation
1106/// # Errors
1107///
1108/// Returns a `LayoutError` if the layout tree cannot be built.
1109pub fn generate_layout_tree<T: ParsedFontTrait>(
1110    ctx: &mut LayoutContext<'_, T>,
1111) -> Result<LayoutTree> {
1112    let mut builder = LayoutTreeBuilder::new(ctx.viewport_size);
1113    let root_id = ctx
1114        .styled_dom
1115        .root
1116        .into_crate_internal()
1117        .unwrap_or(NodeId::ZERO);
1118    let root_index =
1119        builder.process_node(ctx.styled_dom, root_id, None, ctx.debug_messages)?;
1120    let mut layout_tree = builder.build(root_index);
1121
1122    // Pre-compute the STF (shrink-to-fit) subtree bitmap. This is static-DOM
1123    // information: whether a subtree establishes any shrink-to-fit context
1124    // depends only on the DOM structure + formatting context, both of which
1125    // are frozen from here until the next layout-tree rebuild. The intrinsic
1126    // sizing pass reads this to skip subtrees whose intrinsics are never
1127    // consumed (§58 Win #3).
1128    layout_tree.subtree_needs_intrinsic = compute_subtree_needs_intrinsic(ctx.styled_dom, &layout_tree);
1129
1130    debug_log!(
1131        ctx,
1132        "Generated layout tree with {} nodes (incl. anonymous)",
1133        layout_tree.nodes.len()
1134    );
1135
1136    Ok(layout_tree)
1137}
1138
1139/// Returns true if `(dom_node_id, fc)` establishes a formatting context whose
1140/// sizing algorithm reads children's intrinsic sizes. Covers:
1141/// - flex containers (flex item sizing uses child min/max-content),
1142/// - grid containers (grid-track sizing likewise),
1143/// - tables and table cells,
1144/// - inline-block (its own width may be shrink-to-fit),
1145/// - floats and abspos elements (their `auto` width resolves to shrink-to-fit).
1146///
1147/// A `FormattingContext::Block` with a definite CSS width is NOT shrink-to-fit —
1148/// its inner layout gets the width top-down, so descendant intrinsics don't
1149/// feed back up. That's the path Fix C short-circuits.
1150pub(crate) fn is_shrink_to_fit_context(
1151    styled_dom: &StyledDom,
1152    dom_node_id: Option<NodeId>,
1153    fc: FormattingContext,
1154) -> bool {
1155    use crate::solver3::getters::{get_float, MultiValue};
1156    use crate::solver3::positioning::get_position_type;
1157    use azul_css::props::layout::{LayoutFloat, LayoutPosition};
1158
1159    match fc {
1160        FormattingContext::Flex
1161        | FormattingContext::Grid
1162        | FormattingContext::Table
1163        | FormattingContext::InlineBlock => return true,
1164        _ => {}
1165    }
1166    let Some(dom_id) = dom_node_id else { return false; };
1167    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
1168    let float_val = match get_float(styled_dom, dom_id, node_state) {
1169        MultiValue::Exact(v) => v,
1170        _ => LayoutFloat::None,
1171    };
1172    if float_val != LayoutFloat::None {
1173        return true;
1174    }
1175    let pos = get_position_type(styled_dom, Some(dom_id));
1176    if pos == LayoutPosition::Absolute || pos == LayoutPosition::Fixed {
1177        // Abspos only becomes shrink-to-fit when width is `auto`.
1178        // Being conservative: treat as STF whenever abspos so we still
1179        // compute intrinsics for the auto-width case. Misses no work.
1180        return true;
1181    }
1182    false
1183}
1184
1185/// Per-node bitmap of "this node or any descendant establishes a shrink-to-fit
1186/// context." Post-order walk: `out[i] = self_stf(i) || any(out[child_of_i])`.
1187/// Layout tree nodes are built top-down (pre-order), so iterating from the end
1188/// visits children before parents.
1189fn compute_subtree_needs_intrinsic(
1190    styled_dom: &StyledDom,
1191    tree: &LayoutTree,
1192) -> Vec<bool> {
1193    let n = tree.nodes.len();
1194    let mut out = vec![false; n];
1195    for idx in (0..n).rev() {
1196        let hot = &tree.nodes[idx];
1197        let self_stf = is_shrink_to_fit_context(styled_dom, hot.dom_node_id, hot.formatting_context);
1198        let mut any = self_stf;
1199        if !any {
1200            for &child in tree.children(idx) {
1201                if out.get(child).copied().unwrap_or(false) {
1202                    any = true;
1203                    break;
1204                }
1205            }
1206        }
1207        out[idx] = any;
1208    }
1209    out
1210}
1211
1212/// Incrementally builds a [`LayoutTree`] from a [`StyledDom`].
1213///
1214/// Usage: create via [`LayoutTreeBuilder::new`], call [`process_node`](Self::process_node)
1215/// on the root DOM node, then call [`build`](Self::build) to produce the final
1216/// SoA-split `LayoutTree`. During `process_node`, anonymous boxes are generated
1217/// as required by CSS 2.2 §9.2.1.1 (inline wrappers) and §17.2.1 (table fixup).
1218#[derive(Debug)]
1219pub struct LayoutTreeBuilder {
1220    nodes: Vec<LayoutNode>,
1221    dom_to_layout: BTreeMap<NodeId, Vec<usize>>,
1222    viewport_size: LogicalSize,
1223}
1224
1225impl LayoutTreeBuilder {
1226    #[must_use] pub const fn new(viewport_size: LogicalSize) -> Self {
1227        Self {
1228            nodes: Vec::new(),
1229            dom_to_layout: BTreeMap::new(),
1230            viewport_size,
1231        }
1232    }
1233
1234    #[must_use] pub fn get(&self, index: usize) -> Option<&LayoutNode> {
1235        self.nodes.get(index)
1236    }
1237
1238    pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNode> {
1239        self.nodes.get_mut(index)
1240    }
1241
1242    // +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
1243    /// Main entry point for recursively building the layout tree.
1244    /// This function dispatches to specialized handlers based on the node's
1245    /// `display` property to correctly generate anonymous boxes.
1246    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
1247    fn process_node(
1248        &mut self,
1249        styled_dom: &StyledDom,
1250        dom_id: NodeId,
1251        parent_idx: Option<usize>,
1252        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1253    ) -> Result<usize> {
1254        let node_data = &styled_dom.node_data.as_container()[dom_id];
1255        let node_idx = self.create_node_from_dom(styled_dom, dom_id, parent_idx, debug_messages);
1256        let raw_display = get_display_type(styled_dom, dom_id);
1257
1258        // +spec:display-property:042f56 - replaced elements with layout-internal display use inline
1259        // CSS Display 3 §2.4: "When the display property of a replaced element computes to
1260        // one of the layout-internal values, it is handled as having a used value of inline."
1261        let raw_display = if raw_display.is_layout_internal() && is_replaced_element(node_data) {
1262            LayoutDisplay::Inline
1263        } else {
1264            raw_display
1265        };
1266
1267        // +spec:display-property:0b40af - display/position/float interaction per CSS 2.2 §9.7
1268        // +spec:display-property:ba53ba - float!=none or position!=static causes display to blockify
1269        // +spec:positioning:69468c - absolute/fixed blockifies the box, float computes to none
1270        // +spec:table-layout:cfc60a - CSS 2.2 §9.7: display/position/float interaction
1271        // Blockification rules (CSS Display 3 §2.7 / §2.8):
1272        // 1. Root element → blockify
1273        // 2. position:absolute or position:fixed → float computes to 'none', blockify
1274        // 3. float is not 'none' → blockify
1275        // 4. Flex/Grid children → blockify
1276        let node_position = self.nodes.get(node_idx).map(|n| n.computed_style.position).unwrap_or_default();
1277        let node_float = self.nodes.get(node_idx).map(|n| n.computed_style.float).unwrap_or_default();
1278        let is_absolute_or_fixed = matches!(node_position, LayoutPosition::Absolute | LayoutPosition::Fixed);
1279        let is_floated = node_float != LayoutFloat::None;
1280        let is_root = parent_idx.is_none();
1281
1282        // Per CSS 2.2 §9.7: if position is absolute or fixed, float computes to 'none'
1283        if is_absolute_or_fixed && is_floated {
1284            if let Some(node) = self.nodes.get_mut(node_idx) {
1285                node.computed_style.float = LayoutFloat::None;
1286            }
1287        }
1288
1289        let is_flex_grid_child = parent_idx
1290            .and_then(|p| self.nodes.get(p).map(|n| matches!(n.formatting_context, FormattingContext::Flex | FormattingContext::Grid)))
1291            .unwrap_or(false);
1292
1293        let display_type = crate::solver3::getters::get_computed_display(
1294            raw_display, is_absolute_or_fixed, is_floated, is_root, is_flex_grid_child,
1295        );
1296
1297        // If blockification changed the display type, update the node's formatting context
1298        if display_type != raw_display {
1299            if let Some(node) = self.nodes.get_mut(node_idx) {
1300                node.computed_style.display = display_type;
1301                node.formatting_context = determine_formatting_context_for_display(
1302                    styled_dom, dom_id, display_type,
1303                );
1304            }
1305        }
1306
1307        // Compute containing block index for abs-pos clip exemption
1308        if is_absolute_or_fixed {
1309            let cb_index = if matches!(node_position, LayoutPosition::Fixed) {
1310                // Fixed elements: containing block is the root (viewport)
1311                None
1312            } else {
1313                // Absolute elements: containing block is nearest positioned ancestor
1314                let mut ancestor = parent_idx;
1315                loop {
1316                    match ancestor {
1317                        Some(idx) => {
1318                            let pos = self.nodes.get(idx)
1319                                .map(|n| n.computed_style.position)
1320                                .unwrap_or_default();
1321                            if pos.is_positioned() {
1322                                break Some(idx);
1323                            }
1324                            ancestor = self.nodes.get(idx).and_then(|n| n.parent);
1325                        }
1326                        None => break None, // root
1327                    }
1328                }
1329            };
1330            if let Some(node) = self.nodes.get_mut(node_idx) {
1331                node.containing_block_index = cb_index;
1332            }
1333        }
1334
1335        if parent_idx.is_none() {
1336            if let Some(node) = self.nodes.get_mut(node_idx) {
1337                if let FormattingContext::Block { ref mut establishes_new_context } = node.formatting_context {
1338                    *establishes_new_context = true;
1339                }
1340            }
1341        }
1342
1343        // +spec:display-property:1f4039 - list-item generates ::marker pseudo-element + principal box
1344        // +spec:display-property:2bb592 - list-item generates ::marker pseudo-element with list-style content
1345        // +spec:display-property:3b507e - list-item generates ::marker pseudo-element
1346        // +spec:display-property:a48f00 - additional boxes (marker, table wrapper) placed w.r.t. principal box
1347        // +spec:display-property:998063 - list-item generates principal block box + marker box
1348        // If this is a list-item, inject a ::marker pseudo-element as its first child
1349        // +spec:display-property:a42905 - list-item generates ::marker pseudo-element with list-style content, principal box outer=block inner=flow
1350        if display_type == LayoutDisplay::ListItem {
1351            self.create_marker_pseudo_element(styled_dom, dom_id, node_idx);
1352        }
1353
1354        // +spec:display-contents:376f2e - display:contents removes principal box, children render normally
1355        // +spec:display-contents:3c7066 - display:contents strips element from formatting tree, hoists children
1356        // +spec:display-contents:3f4884 - replaced elements / form controls not specially handled yet (spec note: use display:none instead)
1357        // +spec:display-contents:4f9129 - semantic container role preserved: children promoted but DOM structure unchanged
1358        // +spec:display-contents:7558e8 - display:contents is rendering-time only; DOM relationships unaffected
1359        // +spec:display-contents:a079e3 - display:contents generates no box; children promoted to nearest non-contents ancestor (writing-mode parent lookup skips these)
1360        // +spec:display-contents:e202d5 - display:contents removes principal box, children render as normal
1361        // +spec:display-contents:6bbdf4 - display:contents preserves semantic container role (visibility context)
1362        // +spec:display-property:d7a8de - display:none/contents elements generate no box; anonymous box generation ignores them
1363        // +spec:display-property:dc2132 - display:none and display:contents control box generation
1364        // display:contents - element generates no box; promote children to parent
1365        // +spec:display-contents:61992e - element itself generates no boxes, children promoted to parent
1366        // +spec:display-contents:af8feb - treated as if replaced in element tree by its contents
1367        // +spec:display-contents:353e71 - display:contents box generation behavior
1368        // +spec:display-contents:b0a76b - display:contents generates no box; children promoted to parent
1369        // +spec:display-property:e370af - display:contents generates no box; children promoted to parent
1370        //
1371        // +spec:display-contents:852a59 - display:contents computes to display:none for replaced elements
1372        // +spec:display-contents:4a524e - display:contents computes to display:none on replaced elements
1373        // +spec:replaced-elements:af1e68 - display:contents on replaced elements has no effect (element renders normally)
1374        // Per CSS Display 3 §2.5 / Appendix B: replaced elements (img, canvas, embed, object,
1375        // audio, iframe, video, input, textarea, select, br, wbr, meter, progress)
1376        // and similar cannot be "un-boxed" — display:contents becomes display:none.
1377        if display_type == LayoutDisplay::Contents && is_replaced_element(node_data) {
1378            // Treat as display:none — remove node from parent and skip children
1379            if let Some(parent) = parent_idx {
1380                if let Some(p) = self.nodes.get_mut(parent) {
1381                    p.children.retain(|&c| c != node_idx);
1382                }
1383            }
1384            if let Some(node) = self.nodes.get_mut(node_idx) {
1385                node.computed_style.display = LayoutDisplay::None;
1386                node.formatting_context = FormattingContext::None;
1387            }
1388            return Ok(node_idx);
1389        }
1390
1391        if display_type == LayoutDisplay::Contents {
1392            // Remove the node we just created — it shouldn't generate a box
1393            if let Some(parent) = parent_idx {
1394                if let Some(p) = self.nodes.get_mut(parent) {
1395                    p.children.retain(|&c| c != node_idx);
1396                }
1397            }
1398            // Process children as if they belong to the parent (or root if no parent)
1399            let effective_parent = parent_idx.unwrap_or(node_idx);
1400            for child_dom_id in dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1401                self.process_node(styled_dom, child_dom_id, Some(effective_parent), debug_messages)?;
1402            }
1403            return Ok(node_idx);
1404        }
1405
1406        match display_type {
1407            LayoutDisplay::Block
1408            | LayoutDisplay::InlineBlock
1409            | LayoutDisplay::FlowRoot
1410            | LayoutDisplay::ListItem => {
1411                self.process_block_children(styled_dom, dom_id, node_idx, debug_messages)?;
1412            }
1413            // +spec:table-layout:d52e09 - display:table/inline-table cause element to behave like a table element
1414            // +spec:table-layout:360da0 - table display values cause table formatting behavior
1415            LayoutDisplay::Table | LayoutDisplay::InlineTable => {
1416                self.process_table_children(styled_dom, dom_id, node_idx, debug_messages)?;
1417            }
1418            LayoutDisplay::TableRowGroup
1419            | LayoutDisplay::TableHeaderGroup
1420            | LayoutDisplay::TableFooterGroup => {
1421                self.process_table_row_group_children(styled_dom, dom_id, node_idx, debug_messages)?;
1422            }
1423            LayoutDisplay::TableRow => {
1424                self.process_table_row_children(styled_dom, dom_id, node_idx, debug_messages)?;
1425            }
1426            LayoutDisplay::TableColumn => {
1427                // +spec:table-layout:77974f - Stage 1: all children of table-column treated as display:none
1428                // +spec:table-layout:c8dc69 - Stage 1: remove irrelevant boxes from table-column
1429                // CSS 2.2 §17.2.1: "All child boxes of a 'table-column' parent are
1430                // treated as if they had 'display: none'." - skip all children.
1431            }
1432            LayoutDisplay::TableColumnGroup => {
1433                // CSS 2.2 §17.2.1: "If a child C of a 'table-column-group' parent is not
1434                // a 'table-column' box, then it is treated as if it had 'display: none'."
1435                for child_dom_id in dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1436                    let child_display = get_display_type(styled_dom, child_dom_id);
1437                    if child_display == LayoutDisplay::TableColumn {
1438                        self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
1439                    }
1440                    // Non-table-column children are suppressed (treated as display:none)
1441                }
1442            }
1443            // Inline, TableCell, etc., have their children processed as part of their
1444            // formatting context layout and don't require anonymous box generation at this stage.
1445            // of table-internal display values is handled via blockify_flex_item_if_table_internal
1446            _ => {
1447                // +spec:display-contents:34008d - display:none elements generate no boxes; excluded from formatting structure
1448                // +spec:display-property:1f38b2 - display:none creates no box at all, filter from layout tree
1449                // +spec:display-property:eb53f7 - display:none suppresses box generation; visibility:hidden boxes still affect layout
1450                // Filter out display: none children - they don't participate in layout
1451                // +spec:display-property:d1600a - display:none suppresses box generation; visibility:hidden boxes still affect layout
1452                // ALSO filter out whitespace-only text nodes for Flex/Grid/etc containers
1453                // to prevent them from becoming unwanted anonymous items.
1454                let children: Vec<NodeId> = dom_id
1455                    .az_children(&styled_dom.node_hierarchy.as_container())
1456                    // +spec:display-property:9f02c6 - display:none elements generate no boxes
1457                    .filter(|&child_id| {
1458                        // +spec:display-property:3b507e - display:none excludes subtree from box tree
1459                        if get_display_type(styled_dom, child_id) == LayoutDisplay::None {
1460                            return false;
1461                        }
1462                        // Check for whitespace-only text
1463                        let node_data = &styled_dom.node_data.as_container()[child_id];
1464                        if let NodeType::Text(text) = node_data.get_node_type() {
1465                            // Skip if text is empty or just whitespace
1466                            return !text.as_str().trim().is_empty();
1467                        }
1468                        true
1469                    })
1470                    .collect();
1471
1472                let is_flex_or_grid = matches!(
1473                    display_type,
1474                    LayoutDisplay::Flex | LayoutDisplay::InlineFlex
1475                    | LayoutDisplay::Grid | LayoutDisplay::InlineGrid
1476                );
1477
1478                for child_dom_id in children {
1479                    // +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
1480                    // +spec:width-calculation:59d456 - table wrapper box is block-level, establishes BFC (CSS 2.2 §17.4)
1481                    // the table wrapper box becomes the flex item; align-self applies to the
1482                    // wrapper, flex longhands apply to the inner table box, caption contents
1483                    // contribute to wrapper min/max-content sizes
1484                    let child_display = get_display_type(styled_dom, child_dom_id);
1485                    if is_flex_or_grid && child_display.creates_table_context() {
1486                        let wrapper_idx = self.create_anonymous_node(
1487                            node_idx,
1488                            AnonymousBoxType::TableWrapper,
1489                            FormattingContext::Block { establishes_new_context: true },
1490                        );
1491                        self.process_node(styled_dom, child_dom_id, Some(wrapper_idx), debug_messages)?;
1492                    } else {
1493                        let child_idx = self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
1494                        // table-internal flex items are blockified, preventing anonymous table
1495                        // box generation (e.g. two display:table-cell flex items become two
1496                        // separate display:block flex items)
1497                        if is_flex_or_grid {
1498                            blockify_flex_item_if_table_internal(&mut self.nodes, child_idx);
1499                        }
1500                    }
1501                }
1502            }
1503        }
1504        Ok(node_idx)
1505    }
1506
1507    // +spec:display-property:5572e7 - Anonymous block boxes: wrap inline runs when block container has mixed block/inline children
1508    // +spec:display-property:090043 - Anonymous block box properties inherited from enclosing non-anonymous box; non-inherited props get initial values
1509    // +spec:display-property:7b9f7a - Block-level vs inline-level classification and anonymous block box creation
1510    // +spec:display-property:078fe5 - Anonymous block boxes wrapping inline content in mixed block/inline contexts
1511    // +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
1512    // +spec:display-property:1fe2be - inline box construction with anonymous text interspersed with inline elements
1513    // +spec:display-property:be80e3 - Anonymous inline boxes: text in block containers treated as anonymous inlines, whitespace-only runs collapsed
1514    /// Handles children of a block-level element, creating anonymous block
1515    /// wrappers for consecutive runs of inline-level children if necessary.
1516    // +spec:display-property:b73c50 - blockify inline content by wrapping in anonymous block containers
1517    fn process_block_children(
1518        &mut self,
1519        styled_dom: &StyledDom,
1520        parent_dom_id: NodeId,
1521        parent_idx: usize,
1522        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1523    ) -> Result<()> {
1524        // Filter out display: none children - they don't participate in layout
1525        let children: Vec<NodeId> = parent_dom_id
1526            .az_children(&styled_dom.node_hierarchy.as_container())
1527            .filter(|&child_id| get_display_type(styled_dom, child_id) != LayoutDisplay::None)
1528            .collect();
1529
1530        // Debug: log which children we found
1531        if let Some(msgs) = debug_messages.as_mut() {
1532            msgs.push(LayoutDebugMessage::info(format!(
1533                "[process_block_children] DOM node {} has {} children: {:?}",
1534                parent_dom_id.index(),
1535                children.len(),
1536                children.iter().map(NodeId::index).collect::<Vec<_>>()
1537            )));
1538        }
1539
1540        let has_block_child = children.iter().any(|&id| is_block_level(styled_dom, id));
1541
1542        if let Some(msgs) = debug_messages.as_mut() {
1543            msgs.push(LayoutDebugMessage::info(format!(
1544                "[process_block_children] has_block_child={}, children display types: {:?}",
1545                has_block_child,
1546                children
1547                    .iter()
1548                    .map(|c| {
1549                        let dt = get_display_type(styled_dom, *c);
1550                        let is_block = is_block_level(styled_dom, *c);
1551                        format!("{}:{:?}(block={})", c.index(), dt, is_block)
1552                    })
1553                    .collect::<Vec<_>>()
1554            )));
1555        }
1556
1557        if !has_block_child {
1558            // All children are inline, no anonymous boxes needed.
1559            if let Some(msgs) = debug_messages.as_mut() {
1560                msgs.push(LayoutDebugMessage::info(format!(
1561                    "[process_block_children] All inline, processing {} children directly",
1562                    children.len()
1563                )));
1564            }
1565            for child_id in children {
1566                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1567            }
1568            return Ok(());
1569        }
1570
1571        // Mixed block and inline content requires anonymous wrappers.
1572        let mut inline_run = Vec::new();
1573
1574        for child_id in children {
1575            if is_block_level(styled_dom, child_id) {
1576                // +spec:display-contents:02a534 - contiguous text sequences with no text don't generate boxes
1577                // End the current inline run — but skip if all nodes are whitespace-only text.
1578                // +spec:display-property:7d1570 - whitespace-only text that would be collapsed does not generate anonymous inline boxes
1579                // +spec:white-space-processing:b32f69 - whitespace-only inline runs between blocks don't generate anonymous inline boxes
1580                // CSS 2.1 §9.2.2.1: "White space content that would subsequently be collapsed
1581                // away according to the 'white-space' property does not generate any anonymous
1582                // inline boxes."
1583                if !inline_run.is_empty() {
1584                    self.flush_inline_run(styled_dom, parent_idx, &mut inline_run, debug_messages)?;
1585                }
1586                // Process the block-level child directly
1587                if let Some(msgs) = debug_messages.as_mut() {
1588                    msgs.push(LayoutDebugMessage::info(format!(
1589                        "[process_block_children] Processing block child DOM {}",
1590                        child_id.index()
1591                    )));
1592                }
1593                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1594            } else {
1595                inline_run.push(child_id);
1596            }
1597        }
1598        // Process any remaining inline children at the end — skip if all whitespace
1599        if !inline_run.is_empty() {
1600            self.flush_inline_run(styled_dom, parent_idx, &mut inline_run, debug_messages)?;
1601        }
1602
1603        Ok(())
1604    }
1605
1606    // +spec:table-layout:6bb84e - Anonymous table object generation (stages 1-3: remove irrelevant boxes, generate missing child wrappers, generate missing parents)
1607    // +spec:table-layout:77974f - Stage 2: generate missing child wrappers for table/inline-table
1608    // +spec:table-layout:c8dc69 - Stage 2: wrap non-proper children in anonymous table-row
1609    // +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
1610    fn process_table_level_children(
1611        &mut self,
1612        styled_dom: &StyledDom,
1613        parent_dom_id: NodeId,
1614        parent_idx: usize,
1615        is_expected_child: fn(LayoutDisplay) -> bool,
1616        anon_type: AnonymousBoxType,
1617        anon_fc: FormattingContext,
1618        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1619    ) -> Result<()> {
1620        let parent_display = get_display_type(styled_dom, parent_dom_id);
1621        let mut non_matching_children = Vec::new();
1622
1623        for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
1624            if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
1625                continue;
1626            }
1627
1628            let child_display = get_display_type(styled_dom, child_id);
1629
1630            if is_expected_child(child_display) {
1631                if !non_matching_children.is_empty() {
1632                    let anon_idx = self.create_anonymous_node(
1633                        parent_idx,
1634                        anon_type,
1635                        anon_fc,
1636                    );
1637                    #[allow(clippy::iter_with_drain)] // accumulator Vec reused across runs; drain(..) empties it while retaining the allocation
1638                    for np_id in non_matching_children.drain(..) {
1639                        self.process_node(styled_dom, np_id, Some(anon_idx), debug_messages)?;
1640                    }
1641                }
1642                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
1643            } else {
1644                non_matching_children.push(child_id);
1645            }
1646        }
1647
1648        if !non_matching_children.is_empty() {
1649            let anon_idx = self.create_anonymous_node(
1650                parent_idx,
1651                anon_type,
1652                anon_fc,
1653            );
1654            for np_id in non_matching_children {
1655                self.process_node(styled_dom, np_id, Some(anon_idx), debug_messages)?;
1656            }
1657        }
1658
1659        Ok(())
1660    }
1661
1662    fn process_table_children(
1663        &mut self,
1664        styled_dom: &StyledDom,
1665        parent_dom_id: NodeId,
1666        parent_idx: usize,
1667        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1668    ) -> Result<()> {
1669        self.process_table_level_children(
1670            styled_dom, parent_dom_id, parent_idx,
1671            is_proper_table_child,
1672            AnonymousBoxType::TableRow,
1673            FormattingContext::TableRow,
1674            debug_messages,
1675        )
1676    }
1677
1678    fn process_table_row_group_children(
1679        &mut self,
1680        styled_dom: &StyledDom,
1681        parent_dom_id: NodeId,
1682        parent_idx: usize,
1683        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1684    ) -> Result<()> {
1685        self.process_table_level_children(
1686            styled_dom, parent_dom_id, parent_idx,
1687            |d| d == LayoutDisplay::TableRow,
1688            AnonymousBoxType::TableRow,
1689            FormattingContext::TableRow,
1690            debug_messages,
1691        )
1692    }
1693
1694    fn process_table_row_children(
1695        &mut self,
1696        styled_dom: &StyledDom,
1697        parent_dom_id: NodeId,
1698        parent_idx: usize,
1699        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1700    ) -> Result<()> {
1701        self.process_table_level_children(
1702            styled_dom, parent_dom_id, parent_idx,
1703            |d| d == LayoutDisplay::TableCell,
1704            AnonymousBoxType::TableCell,
1705            FormattingContext::Block { establishes_new_context: true },
1706            debug_messages,
1707        )
1708    }
1709    // +spec:display-property:7d1570 - whitespace-only text that would be collapsed does not generate anonymous inline boxes
1710    // +spec:white-space-processing:b32f69 - whitespace-only inline runs between blocks don't generate anonymous inline boxes
1711    fn flush_inline_run(
1712        &mut self,
1713        styled_dom: &StyledDom,
1714        parent_idx: usize,
1715        inline_run: &mut Vec<NodeId>,
1716        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1717    ) -> Result<()> {
1718        let all_whitespace = inline_run
1719            .iter()
1720            .all(|id| is_whitespace_only_text(styled_dom, *id));
1721        if all_whitespace {
1722            if let Some(msgs) = debug_messages.as_mut() {
1723                msgs.push(LayoutDebugMessage::info(format!(
1724                    "[process_block_children] Skipping whitespace-only inline run: {:?}",
1725                    inline_run.iter().map(|c: &NodeId| c.index()).collect::<Vec<_>>()
1726                )));
1727            }
1728            inline_run.clear();
1729        } else {
1730            if let Some(msgs) = debug_messages.as_mut() {
1731                msgs.push(LayoutDebugMessage::info(format!(
1732                    "[process_block_children] Creating anon wrapper for inline run: {:?}",
1733                    inline_run.iter().map(|c: &NodeId| c.index()).collect::<Vec<_>>()
1734                )));
1735            }
1736            let anon_idx = self.create_anonymous_node(
1737                parent_idx,
1738                AnonymousBoxType::InlineWrapper,
1739                FormattingContext::Block {
1740                    establishes_new_context: true,
1741                },
1742            );
1743            for inline_child_id in inline_run.drain(..) {
1744                self.process_node(styled_dom, inline_child_id, Some(anon_idx), debug_messages)?;
1745            }
1746        }
1747        Ok(())
1748    }
1749
1750    // +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())
1751    /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
1752    /// "In this process, inline-level boxes are wrapped in anonymous boxes as needed
1753    /// to satisfy the constraints of the table model."
1754    ///
1755    // +spec:display-property:ee83bf - Anonymous box generation: boxes not associated with elements, inheriting through box tree parentage
1756    /// Helper to create an anonymous node in the tree.
1757    /// Anonymous boxes don't have a corresponding DOM node and are used to enforce
1758    /// the CSS box model structure (e.g., wrapping inline content in blocks,
1759    /// or creating missing table structural elements).
1760    // +spec:display-property:6ff51a - anonymous block boxes have no styles (box_props default), so parent element properties still apply to its content
1761    pub fn create_anonymous_node(
1762        &mut self,
1763        parent: usize,
1764        anon_type: AnonymousBoxType,
1765        fc: FormattingContext,
1766    ) -> usize {
1767        let index = self.nodes.len();
1768
1769        // +spec:display-property:e67146 - Anonymous boxes inherit from enclosing non-anonymous box; non-inherited props use initial values
1770        let parent_fc = self.nodes.get(parent).map(|n| n.formatting_context);
1771
1772        self.nodes.push(LayoutNode {
1773            // ── HOT ──
1774            box_props: BoxProps::default(),
1775            dom_node_id: None,
1776            children: Vec::new(),
1777            used_size: None,
1778            formatting_context: fc,
1779            parent: Some(parent),
1780            // ── WARM ──
1781            intrinsic_sizes: None,
1782            baseline: None,
1783            inline_layout_result: None,
1784            scrollbar_info: None,
1785            relative_position: None,
1786            overflow_content_size: None,
1787            taffy_cache: TaffyCache::new(),
1788            computed_style: ComputedLayoutStyle::default(),
1789            pseudo_element: None,
1790            escaped_top_margin: None,
1791            escaped_bottom_margin: None,
1792            parent_formatting_context: parent_fc,
1793            ifc_membership: None,
1794            containing_block_index: None,
1795            // ── COLD ──
1796            anonymous_type: Some(anon_type),
1797            node_data_fingerprint: NodeDataFingerprint::default(),
1798            subtree_hash: SubtreeHash(0),
1799            dirty_flag: DirtyFlag::Layout,
1800            unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
1801            ifc_id: None,
1802        });
1803
1804        self.nodes[parent].children.push(index);
1805        index
1806    }
1807
1808    /// Creates a `::marker` pseudo-element as the first child of a list-item.
1809    ///
1810    /// Per CSS Lists Module Level 3, Section 3.1:
1811    /// "For elements with display: list-item, user agents must generate a
1812    /// `::marker` pseudo-element as the first child of the principal box."
1813    ///
1814    /// The `::marker` references the same DOM node as its parent list-item,
1815    /// but is marked as a pseudo-element for proper counter resolution and styling.
1816    pub fn create_marker_pseudo_element(
1817        &mut self,
1818        styled_dom: &StyledDom,
1819        list_item_dom_id: NodeId,
1820        list_item_idx: usize,
1821    ) -> usize {
1822        let index = self.nodes.len();
1823
1824        // The marker references the same DOM node as the list-item
1825        // This is important for style resolution (the marker inherits from the list-item)
1826        let parent_fc = self
1827            .nodes
1828            .get(list_item_idx)
1829            .map(|n| n.formatting_context);
1830        self.nodes.push(LayoutNode {
1831            // ── HOT ──
1832            box_props: BoxProps::default(),
1833            dom_node_id: Some(list_item_dom_id),
1834            children: Vec::new(),
1835            used_size: None,
1836            formatting_context: FormattingContext::Inline,
1837            parent: Some(list_item_idx),
1838            // ── WARM ──
1839            intrinsic_sizes: None,
1840            baseline: None,
1841            inline_layout_result: None,
1842            scrollbar_info: None,
1843            relative_position: None,
1844            overflow_content_size: None,
1845            taffy_cache: TaffyCache::new(),
1846            computed_style: ComputedLayoutStyle::default(),
1847            pseudo_element: Some(PseudoElement::Marker),
1848            escaped_top_margin: None,
1849            escaped_bottom_margin: None,
1850            parent_formatting_context: parent_fc,
1851            ifc_membership: None,
1852            containing_block_index: None,
1853            // ── COLD ──
1854            anonymous_type: None,
1855            node_data_fingerprint: NodeDataFingerprint::default(),
1856            subtree_hash: SubtreeHash(0),
1857            dirty_flag: DirtyFlag::Layout,
1858            unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
1859            ifc_id: None,
1860        });
1861
1862        // Insert as FIRST child (per spec)
1863        self.nodes[list_item_idx].children.insert(0, index);
1864
1865        // Register with DOM mapping for counter resolution
1866        self.dom_to_layout
1867            .entry(list_item_dom_id)
1868            .or_default()
1869            .push(index);
1870
1871        index
1872    }
1873
1874    // M12.7: returns `usize`, NOT `Result<usize>` — this fn has no error path
1875    // (always `Ok(index)`). The `Result` forced callers to use `?`, whose lifted
1876    // discriminant decode mis-reads the Ok as Err (the rc=5 root cause: reconcile
1877    // reaches this fn but returns Err before its own Ok). Dropping the Result
1878    // removes that mis-lifting `?`.
1879    /// Apply CSS Display 3 §2.7/§2.8 blockification to a freshly-created node:
1880    /// a flex/grid item (or root / abs-pos / floated box) whose specified display
1881    /// is inline-level computes to its block-level equivalent.
1882    ///
1883    /// `process_node` (the full tree build) does this inline, but the INCREMENTAL
1884    /// tree builder (`cache.rs` reconcile → `create_node_from_dom`) bypassed it.
1885    /// Without it, a replaced inline flex item — e.g. an `<img>` canvas with
1886    /// `flex-grow: 1` (`AzulPaint`) — stayed inline, so its flex-grow was ignored
1887    /// and it was laid out 300×0 (the replaced-element default width, 0 height).
1888    /// Must be called AFTER the node is created and AFTER its parent's
1889    /// formatting context is known (the build is top-down, so the parent exists).
1890    pub fn blockify_node_display(
1891        &mut self,
1892        styled_dom: &StyledDom,
1893        dom_id: NodeId,
1894        node_idx: usize,
1895        parent_idx: Option<usize>,
1896    ) {
1897        let node_data = &styled_dom.node_data.as_container()[dom_id];
1898        // CSS Display 3 §2.4: a replaced element with a layout-internal display
1899        // value uses 'inline' — so it's inline-level and thus blockifiable.
1900        let raw_display = {
1901            let d = get_display_type(styled_dom, dom_id);
1902            if d.is_layout_internal() && is_replaced_element(node_data) {
1903                LayoutDisplay::Inline
1904            } else {
1905                d
1906            }
1907        };
1908        let (position, float) = self
1909            .nodes
1910            .get(node_idx)
1911            .map(|n| (n.computed_style.position, n.computed_style.float))
1912            .unwrap_or_default();
1913        let is_absolute_or_fixed =
1914            matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed);
1915        let is_floated = float != LayoutFloat::None;
1916        let is_root = parent_idx.is_none();
1917        let is_flex_grid_child = parent_idx
1918            .and_then(|p| self.nodes.get(p))
1919            .is_some_and(|n| {
1920                matches!(
1921                    n.formatting_context,
1922                    FormattingContext::Flex | FormattingContext::Grid
1923                )
1924            });
1925        let display_type = crate::solver3::getters::get_computed_display(
1926            raw_display,
1927            is_absolute_or_fixed,
1928            is_floated,
1929            is_root,
1930            is_flex_grid_child,
1931        );
1932        if display_type != raw_display {
1933            if let Some(node) = self.nodes.get_mut(node_idx) {
1934                node.computed_style.display = display_type;
1935                node.formatting_context =
1936                    determine_formatting_context_for_display(styled_dom, dom_id, display_type);
1937            }
1938        }
1939    }
1940
1941    #[allow(clippy::cast_possible_truncation)] // bounded layout/render numeric cast
1942    pub fn create_node_from_dom(
1943        &mut self,
1944        styled_dom: &StyledDom,
1945        dom_id: NodeId,
1946        parent: Option<usize>,
1947        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
1948    ) -> usize {
1949        let index = self.nodes.len();
1950        // as IT sees it). If this is 0 but build() sees 0 nodes, the push is lost
1951        // between here and build (builder &mut threading); if garbage, len mis-reads.
1952        { let _ = (0xCE00_0000u32 | (index as u32 & 0xffff)); }
1953        let parent_fc =
1954            parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context));
1955        // this is reached but step A is NOT, collect_box_props diverges; if this is
1956        // NOT reached, the parent Option discriminant mis-lifts (None→Some garbage).
1957        { let _ = (0xCD00_0001u32 | (u32::from(parent_fc.is_some()) << 8)); }
1958        let collected = collect_box_props(styled_dom, dom_id, debug_messages, self.viewport_size);
1959        { let _ = (0xCA00_0001u32); }
1960        self.nodes.push(LayoutNode {
1961            // ── HOT ──
1962            box_props: collected.resolved,
1963            dom_node_id: Some(dom_id),
1964            children: Vec::new(),
1965            used_size: None,
1966            formatting_context: determine_formatting_context(styled_dom, dom_id),
1967            parent,
1968            // ── WARM ──
1969            intrinsic_sizes: None,
1970            baseline: None,
1971            inline_layout_result: None,
1972            scrollbar_info: None,
1973            relative_position: None,
1974            overflow_content_size: None,
1975            taffy_cache: TaffyCache::new(),
1976            // +spec:overflow:8f9f7e - viewport overflow propagation: visible→auto, clip→hidden
1977            computed_style: {
1978                let mut style = compute_layout_style(styled_dom, dom_id);
1979                if parent.is_none() {
1980                    // CSS Overflow 3 §3.3: If visible is applied to the viewport,
1981                    // it must be interpreted as auto. If clip is applied to the
1982                    // viewport, it must be interpreted as hidden.
1983                    use azul_css::props::layout::LayoutOverflow;
1984                    if style.overflow_x == LayoutOverflow::Visible {
1985                        style.overflow_x = LayoutOverflow::Auto;
1986                    } else if style.overflow_x == LayoutOverflow::Clip {
1987                        style.overflow_x = LayoutOverflow::Hidden;
1988                    }
1989                    if style.overflow_y == LayoutOverflow::Visible {
1990                        style.overflow_y = LayoutOverflow::Auto;
1991                    } else if style.overflow_y == LayoutOverflow::Clip {
1992                        style.overflow_y = LayoutOverflow::Hidden;
1993                    }
1994                }
1995                style
1996            },
1997            pseudo_element: None,
1998            escaped_top_margin: None,
1999            escaped_bottom_margin: None,
2000            parent_formatting_context: parent_fc,
2001            ifc_membership: None,
2002            containing_block_index: None,
2003            // ── COLD ──
2004            anonymous_type: None,
2005            node_data_fingerprint: NodeDataFingerprint::compute(
2006                &styled_dom.node_data.as_container()[dom_id],
2007                styled_dom.styled_nodes.as_container().get(dom_id).map(|n| &n.styled_node_state),
2008            ),
2009            subtree_hash: SubtreeHash(0),
2010            dirty_flag: DirtyFlag::Layout,
2011            unresolved_box_props: collected.unresolved,
2012            ifc_id: None,
2013        });
2014        { let _ = (0xCB00_0001u32 | ((self.nodes.len() as u32 & 0xff) << 8)); }
2015        if let Some(p) = parent {
2016            self.nodes[p].children.push(index);
2017        }
2018        self.dom_to_layout.entry(dom_id).or_default().push(index);
2019        // DEBUG (2026-06-02 children-None tree-build): count create_node_from_dom
2020        // calls @0x40500 + record each dom_id into a 14-slot ring @0x40504. REVERT
2021        // before commit. Runs only in lifted wasm (server lifts, never runs natively).
2022        unsafe {
2023            let c = crate::az_mark_read(0x40500);
2024            crate::az_mark(0x60500_u32, (c.wrapping_add(1)));
2025            if (c as usize) < 14 {
2026                crate::az_mark((0x40504 + (c as usize) * 4) as u32, (0xDD00_0000 | (dom_id.index() as u32 & 0xffff)));
2027            }
2028        }
2029        index
2030    }
2031
2032    pub fn clone_node_from_old(&mut self, old_node: &LayoutNode, parent: Option<usize>) -> usize {
2033        let index = self.nodes.len();
2034        let mut new_node = old_node.clone();
2035        new_node.parent = parent;
2036        new_node.parent_formatting_context =
2037            parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context));
2038        new_node.children = Vec::new();
2039        new_node.dirty_flag = DirtyFlag::None;
2040        self.nodes.push(new_node);
2041        if let Some(p) = parent {
2042            self.nodes[p].children.push(index);
2043        }
2044        if let Some(dom_id) = old_node.dom_node_id {
2045            self.dom_to_layout.entry(dom_id).or_default().push(index);
2046        }
2047        index
2048    }
2049
2050    #[allow(clippy::cast_possible_truncation)] // bounded layout/render numeric cast
2051    #[must_use] pub fn build(self, root_idx: usize) -> LayoutTree {
2052        let nodes = self.nodes;
2053        let node_count = nodes.len();
2054
2055        // Flatten per-node children Vecs into a single contiguous arena.
2056        let total_children: usize = nodes.iter().map(|n| n.children.len()).sum();
2057        let mut arena = Vec::with_capacity(total_children);
2058        let mut offsets = Vec::with_capacity(node_count);
2059
2060        // Split monolithic LayoutNodes into hot/warm/cold SoA arrays
2061        let mut hot_nodes = Vec::with_capacity(node_count);
2062        let mut warm_nodes = Vec::with_capacity(node_count);
2063        let mut cold_nodes = Vec::with_capacity(node_count);
2064
2065        for node in nodes {
2066            // Flatten children into arena first
2067            let start = arena.len() as u32;
2068            let len = node.children.len() as u32;
2069            arena.extend_from_slice(&node.children);
2070            offsets.push((start, len));
2071
2072            // Split into hot/warm/cold
2073            let (hot, warm, cold) = node.split();
2074            hot_nodes.push(hot);
2075            warm_nodes.push(warm);
2076            cold_nodes.push(cold);
2077        }
2078
2079        // discriminant). If len>0 but calculate_intrinsic_recursive's
2080        // `tree.get(root).ok_or(InvalidTree)?` still errors, that `?`/null-check
2081        // mis-discriminates Some→None. If len==0, build's input was empty.
2082        // if build>0 but get_node_size sees 0, the tree.clone() (hashbrown) drops the map.
2083
2084        LayoutTree {
2085            nodes: hot_nodes,
2086            warm: warm_nodes,
2087            cold: cold_nodes,
2088            root: root_idx,
2089            dom_to_layout: self.dom_to_layout,
2090            children_arena: arena,
2091            children_offsets: offsets,
2092            // Populated by `generate_layout_tree` after the tree is built,
2093            // since the computation needs styled_dom for float/position lookup.
2094            subtree_needs_intrinsic: Vec::new(),
2095        }
2096    }
2097}
2098
2099// +spec:display-property:697082 - outer display type determines principal box's role in flow layout (block vs inline)
2100// +spec:display-property:0d251b - Block-level elements: display 'block', 'list-item', 'table' generate block-level boxes
2101// +spec:display-property:9464be - block-level vs block container distinction: not all block-level boxes are block containers (e.g. replaced elements, flex containers)
2102#[must_use] pub fn is_block_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2103    matches!(
2104        get_display_type(styled_dom, node_id),
2105        LayoutDisplay::Block
2106            | LayoutDisplay::FlowRoot
2107            | LayoutDisplay::Flex
2108            | LayoutDisplay::Grid
2109            | LayoutDisplay::Table
2110            | LayoutDisplay::TableCaption
2111            | LayoutDisplay::TableRow
2112            | LayoutDisplay::TableRowGroup
2113            | LayoutDisplay::TableHeaderGroup
2114            | LayoutDisplay::TableFooterGroup
2115            | LayoutDisplay::TableCell
2116            | LayoutDisplay::ListItem
2117    )
2118}
2119
2120// +spec:display-property:23f111 - Inline-level elements: inline, inline-block, inline-table, inline-flex, inline-grid
2121/// Checks if a node is inline-level (including text nodes).
2122/// According to CSS spec, inline-level content includes:
2123///
2124/// - Elements with display: inline, inline-block, inline-table, inline-flex, inline-grid
2125/// - Text nodes
2126/// - Generated content
2127fn is_inline_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2128    // Text nodes are always inline-level
2129    let node_data = &styled_dom.node_data.as_container()[node_id];
2130    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
2131        return true;
2132    }
2133
2134    // Check the display property
2135    matches!(
2136        get_display_type(styled_dom, node_id),
2137        LayoutDisplay::Inline
2138            | LayoutDisplay::InlineBlock
2139            | LayoutDisplay::InlineTable
2140            | LayoutDisplay::InlineFlex
2141            | LayoutDisplay::InlineGrid
2142    )
2143}
2144
2145// +spec:display-property:c2520b - Block containers with only inline-level children establish IFC; mixed content gets anonymous block wrappers
2146/// Checks if a block container has only inline-level children.
2147/// According to CSS 2.2 Section 9.4.2: "An inline formatting context is established
2148/// by a block container box that contains no block-level boxes."
2149// +spec:display-property:75d642 - block container with only inline-level content establishes IFC
2150// +spec:display-property:c188d6 - IFC: all inline content within a containing block flows together as continuous text
2151pub(crate) fn has_only_inline_children(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2152    let hierarchy = styled_dom.node_hierarchy.as_container();
2153    let Some(node_hier) = hierarchy.get(node_id) else {
2154        return false;
2155    };
2156
2157    // Get the first child
2158    let mut current_child = node_hier.first_child_id(node_id);
2159
2160    // If there are no children, it's not an IFC (it's empty)
2161    if current_child.is_none() {
2162        return false;
2163    }
2164
2165    // Check all children
2166    while let Some(child_id) = current_child {
2167        let is_inline = is_inline_level(styled_dom, child_id);
2168
2169        if !is_inline {
2170            // Found a block-level child
2171            return false;
2172        }
2173
2174        // Move to next sibling
2175        if let Some(child_hier) = hierarchy.get(child_id) {
2176            current_child = child_hier.next_sibling_id();
2177        } else {
2178            break;
2179        }
2180    }
2181
2182    // All children are inline-level
2183    true
2184}
2185
2186/// Pre-computes all CSS properties needed during layout for a single node.
2187/// 
2188/// This is called once per node during layout tree construction, avoiding
2189/// repeated style lookups during the actual layout pass (O(n) vs O(n²)).
2190fn compute_layout_style(styled_dom: &StyledDom, dom_id: NodeId) -> ComputedLayoutStyle {
2191    let styled_node_state = styled_dom
2192        .styled_nodes
2193        .as_container()
2194        .get(dom_id)
2195        .map(|n| n.styled_node_state)
2196        .unwrap_or_default();
2197
2198    // Get display property
2199    let display = match get_display_property(styled_dom, Some(dom_id)) {
2200        MultiValue::Exact(d) => d,
2201        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => LayoutDisplay::Block,
2202    };
2203
2204    // Get position property
2205    let position = get_position(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2206
2207    // Get float property  
2208    let float = get_float(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2209
2210    // Get overflow properties
2211    // +spec:overflow:48890c - overflow:hidden treated as overflow:clip on replaced elements
2212    let is_replaced = matches!(
2213        styled_dom.node_data.as_container()[dom_id].get_node_type(),
2214        NodeType::Image(_) | NodeType::VirtualView
2215    );
2216    let overflow_x = {
2217        let v = get_overflow_x(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2218        if is_replaced && v == LayoutOverflow::Hidden { LayoutOverflow::Clip } else { v }
2219    };
2220    let overflow_y = {
2221        let v = get_overflow_y(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2222        if is_replaced && v == LayoutOverflow::Hidden { LayoutOverflow::Clip } else { v }
2223    };
2224
2225    // Get writing mode, direction, and text-orientation
2226    // +spec:writing-modes:2af307 - Propagate used writing-mode from <body> to <html> root
2227    let writing_mode = {
2228        let own_wm = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2229        let nd = &styled_dom.node_data.as_container()[dom_id];
2230        if matches!(nd.node_type, NodeType::Html) {
2231            // If root <html>, propagate writing-mode from first <body> child
2232            styled_dom
2233                .node_hierarchy
2234                .as_container()
2235                .get(dom_id)
2236                .and_then(|node| node.first_child_id(dom_id))
2237                .and_then(|child_id| {
2238                    let child_data = &styled_dom.node_data.as_container()[child_id];
2239                    if matches!(child_data.node_type, NodeType::Body) {
2240                        let child_state = &styled_dom
2241                            .styled_nodes
2242                            .as_container()[child_id]
2243                            .styled_node_state;
2244                        Some(get_writing_mode(styled_dom, child_id, child_state)
2245                            .unwrap_or_default())
2246                    } else {
2247                        None
2248                    }
2249                })
2250                .unwrap_or(own_wm)
2251        } else {
2252            own_wm
2253        }
2254    };
2255    let direction = get_direction(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2256    let text_orientation = get_text_orientation(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2257
2258    // Get text-align
2259    let text_align = get_text_align(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
2260
2261    // Get explicit width/height (None = auto)
2262    let width = match get_css_width(styled_dom, dom_id, &styled_node_state) {
2263        MultiValue::Exact(w) => Some(w),
2264        _ => None,
2265    };
2266    let height = match get_css_height(styled_dom, dom_id, &styled_node_state) {
2267        MultiValue::Exact(h) => Some(h),
2268        _ => None,
2269    };
2270
2271    // Get min/max constraints
2272    let min_width = match get_css_min_width(styled_dom, dom_id, &styled_node_state) {
2273        MultiValue::Exact(v) => Some(v),
2274        _ => None,
2275    };
2276    let min_height = match get_css_min_height(styled_dom, dom_id, &styled_node_state) {
2277        MultiValue::Exact(v) => Some(v),
2278        _ => None,
2279    };
2280    let max_width = match get_css_max_width(styled_dom, dom_id, &styled_node_state) {
2281        MultiValue::Exact(v) => Some(v),
2282        _ => None,
2283    };
2284    let max_height = match get_css_max_height(styled_dom, dom_id, &styled_node_state) {
2285        MultiValue::Exact(v) => Some(v),
2286        _ => None,
2287    };
2288
2289    ComputedLayoutStyle {
2290        display,
2291        position,
2292        float,
2293        overflow_x,
2294        overflow_y,
2295        writing_mode,
2296        direction,
2297        text_orientation,
2298        width,
2299        height,
2300        min_width,
2301        min_height,
2302        max_width,
2303        max_height,
2304        text_align,
2305    }
2306}
2307
2308// hash_node_data() removed — replaced by NodeDataFingerprint::compute()
2309
2310/// Helper function to get element's computed font-size
2311fn get_element_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
2312    { let _ = (0xC3_000001u32); } // 2-arg wrapper entered
2313    let node_state = styled_dom
2314        .styled_nodes
2315        .as_container()
2316        .get(dom_id)
2317        .map(|n| &n.styled_node_state)
2318        .copied()
2319        .unwrap_or_default();
2320    { let _ = (0xC3_000002u32); } // after node_state (clone); next = 3-arg call
2321
2322    crate::solver3::getters::get_element_font_size(styled_dom, dom_id, &node_state)
2323}
2324
2325/// Helper function to get parent's computed font-size
2326fn get_parent_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
2327    styled_dom
2328        .node_hierarchy
2329        .as_container()
2330        .get(dom_id)
2331        .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id)
2332        .map_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE, |parent_id| get_element_font_size(styled_dom, parent_id))
2333}
2334
2335/// Helper function to get root element's font-size
2336fn get_root_font_size(styled_dom: &StyledDom) -> f32 {
2337    // Root is always NodeId(0) in Azul
2338    get_element_font_size(styled_dom, NodeId::new(0))
2339}
2340
2341/// Create a `ResolutionContext` for a given node
2342fn create_resolution_context(
2343    styled_dom: &StyledDom,
2344    dom_id: NodeId,
2345    containing_block_size: Option<PhysicalSize>,
2346    viewport_size: LogicalSize,
2347) -> ResolutionContext {
2348    { let _ = (0xC1_000001u32); } // create_resolution_context entered
2349    let element_font_size = get_element_font_size(styled_dom, dom_id);
2350    { let _ = (0xC1_000002u32); } // after get_element_font_size
2351    let parent_font_size = get_parent_font_size(styled_dom, dom_id);
2352    { let _ = (0xC1_000003u32); } // after get_parent_font_size
2353    let root_font_size = get_root_font_size(styled_dom);
2354    { let _ = (0xC1_000004u32); } // after get_root_font_size
2355
2356    ResolutionContext {
2357        element_font_size,
2358        parent_font_size,
2359        root_font_size,
2360        // +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
2361        containing_block_size: containing_block_size.unwrap_or(PhysicalSize::new(0.0, 0.0)),
2362        element_size: None, // Not yet laid out
2363        viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
2364    }
2365}
2366
2367/// Result of collecting box properties from the styled DOM.
2368struct CollectedBoxProps {
2369    unresolved: crate::solver3::geometry::UnresolvedBoxProps,
2370    resolved: BoxProps,
2371}
2372
2373/// Collects box properties from the styled DOM and returns both unresolved and resolved forms.
2374///
2375/// The unresolved form stores the raw CSS values for later re-resolution when
2376/// the containing block size is known. The resolved form is an initial resolution
2377/// using `viewport_size` for viewport-relative units.
2378#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
2379fn collect_box_props(
2380    styled_dom: &StyledDom,
2381    dom_id: NodeId,
2382    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
2383    viewport_size: LogicalSize,
2384) -> CollectedBoxProps {
2385    use crate::solver3::geometry::{UnresolvedBoxProps, UnresolvedEdge, UnresolvedMargin};
2386    #[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
2387    use crate::solver3::getters::*;
2388    use azul_css::props::style::border::BorderStyle;
2389    // before create_node step A is the diverging call.
2390    { let _ = (0xC0_000001u32); } // entered
2391
2392    let node_data = &styled_dom.node_data.as_container()[dom_id];
2393
2394    // Get styled node state
2395    let node_state = styled_dom
2396        .styled_nodes
2397        .as_container()
2398        .get(dom_id)
2399        .map(|n| &n.styled_node_state)
2400        .copied()
2401        .unwrap_or_default();
2402    { let _ = (0xC0_000002u32); } // after node_state (clone)
2403
2404    // Create resolution context for this element
2405    // Note: containing_block_size is None here because we don't have it yet
2406    // This is fine for initial resolution - will be re-resolved during layout
2407    let context = create_resolution_context(styled_dom, dom_id, None, viewport_size);
2408    { let _ = (0xC0_000003u32); } // after create_resolution_context
2409
2410    // Read margin values from styled_dom
2411    let margin_top_mv = get_css_margin_top(styled_dom, dom_id, &node_state);
2412    { let _ = (0xC0_000004u32); } // after get_css_margin_top
2413    let margin_right_mv = get_css_margin_right(styled_dom, dom_id, &node_state);
2414    let margin_bottom_mv = get_css_margin_bottom(styled_dom, dom_id, &node_state);
2415    let margin_left_mv = get_css_margin_left(styled_dom, dom_id, &node_state);
2416
2417    // Convert MultiValue to UnresolvedMargin
2418    let to_unresolved_margin = |mv: &MultiValue<PixelValue>| -> UnresolvedMargin {
2419        match mv {
2420            MultiValue::Auto => UnresolvedMargin::Auto,
2421            MultiValue::Exact(pv) => UnresolvedMargin::Length(*pv),
2422            _ => UnresolvedMargin::Zero,
2423        }
2424    };
2425
2426    // Build unresolved margins
2427    let unresolved_margin = UnresolvedEdge {
2428        top: to_unresolved_margin(&margin_top_mv),
2429        right: to_unresolved_margin(&margin_right_mv),
2430        bottom: to_unresolved_margin(&margin_bottom_mv),
2431        left: to_unresolved_margin(&margin_left_mv),
2432    };
2433    { let _ = (0xC0_000005u32); } // after margin block
2434
2435    // Read padding values
2436    let padding_top_mv = get_css_padding_top(styled_dom, dom_id, &node_state);
2437    let padding_right_mv = get_css_padding_right(styled_dom, dom_id, &node_state);
2438    let padding_bottom_mv = get_css_padding_bottom(styled_dom, dom_id, &node_state);
2439    let padding_left_mv = get_css_padding_left(styled_dom, dom_id, &node_state);
2440
2441    // Convert MultiValue to PixelValue (default to 0px)
2442    let to_pixel_value = |mv: MultiValue<PixelValue>| -> PixelValue {
2443        match mv {
2444            MultiValue::Exact(pv) => pv,
2445            _ => PixelValue::const_px(0),
2446        }
2447    };
2448
2449    // Build unresolved padding
2450    let unresolved_padding = UnresolvedEdge {
2451        top: to_pixel_value(padding_top_mv),
2452        right: to_pixel_value(padding_right_mv),
2453        bottom: to_pixel_value(padding_bottom_mv),
2454        left: to_pixel_value(padding_left_mv),
2455    };
2456    { let _ = (0xC0_000056u32); } // after padding getters+values, before get_display_type
2457
2458    // +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
2459    // Non-cell internal table elements (rows, row groups, columns, column groups) do not have padding.
2460    // 0xC0_57<dt> the CALL returned (dt = LayoutDisplay discriminant) and the MATCH below
2461    // diverges; if it stays 0x56, get_display_type (the enum extraction) itself diverges.
2462    // M12.7 NOTE: get_display_type RETURNS a valid dt here (captured =2), but the code
2463    // immediately after diverges — and replacing the `match` below with a branchless
2464    // bitmask test did NOT help (so it's NOT the multi-way-branch codegen). So the
2465    // get_display_type CALL corrupts the caller frame / control flow (same class as
2466    // create_node's return 0→48704), specific to ENUM-returning getters (pixel getters
2467    // like get_css_margin_* lift fine). Remill-level. The match is kept (original).
2468    let unresolved_padding = match get_display_type(styled_dom, dom_id) {
2469        LayoutDisplay::TableRow
2470        | LayoutDisplay::TableRowGroup
2471        | LayoutDisplay::TableHeaderGroup
2472        | LayoutDisplay::TableFooterGroup
2473        | LayoutDisplay::TableColumn
2474        | LayoutDisplay::TableColumnGroup => UnresolvedEdge {
2475            top: PixelValue::const_px(0),
2476            right: PixelValue::const_px(0),
2477            bottom: PixelValue::const_px(0),
2478            left: PixelValue::const_px(0),
2479        },
2480        _ => unresolved_padding,
2481    };
2482    { let _ = (0xC0_000006u32); } // after padding block
2483
2484    // Read border values
2485    let border_top_mv = get_css_border_top_width(styled_dom, dom_id, &node_state);
2486    let border_right_mv = get_css_border_right_width(styled_dom, dom_id, &node_state);
2487    let border_bottom_mv = get_css_border_bottom_width(styled_dom, dom_id, &node_state);
2488    let border_left_mv = get_css_border_left_width(styled_dom, dom_id, &node_state);
2489
2490    // +spec:box-model:17c0e0 - computed border-width is 0 if border-style is none or hidden
2491    // +spec:box-model:5d2b66 - border-style none/hidden means no border
2492    // CSS 2.2 §8.5.1: "Computed value: absolute length; '0' if the border style is 'none' or 'hidden'"
2493    let style_zeroes_width = |s: BorderStyle| matches!(s, BorderStyle::None | BorderStyle::Hidden);
2494
2495    // Read border styles to check if widths should be zeroed.
2496    // FAST PATH: compact cache returns styles directly for normal state — no
2497    // cascade walks. Prior code here did 4 cascade walks × 586 nodes.
2498    let (bs_top, bs_right, bs_bottom, bs_left) = {
2499        let cache_ptr = &styled_dom.css_property_cache.ptr;
2500        if node_state.is_normal() {
2501            cache_ptr.compact_cache.as_ref().map_or_else(|| (
2502                    cache_ptr.get_border_top_style(node_data, &dom_id, &node_state)
2503                        .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2504                    cache_ptr.get_border_right_style(node_data, &dom_id, &node_state)
2505                        .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2506                    cache_ptr.get_border_bottom_style(node_data, &dom_id, &node_state)
2507                        .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2508                    cache_ptr.get_border_left_style(node_data, &dom_id, &node_state)
2509                        .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2510                ), |cc| {
2511                let idx = dom_id.index();
2512                (cc.get_border_top_style(idx), cc.get_border_right_style(idx),
2513                 cc.get_border_bottom_style(idx), cc.get_border_left_style(idx))
2514            })
2515        } else {
2516            (
2517                cache_ptr.get_border_top_style(node_data, &dom_id, &node_state)
2518                    .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2519                cache_ptr.get_border_right_style(node_data, &dom_id, &node_state)
2520                    .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2521                cache_ptr.get_border_bottom_style(node_data, &dom_id, &node_state)
2522                    .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2523                cache_ptr.get_border_left_style(node_data, &dom_id, &node_state)
2524                    .and_then(|v| v.get_property()).map_or(BorderStyle::None, |s| s.inner),
2525            )
2526        }
2527    };
2528
2529    // Build unresolved border, zeroing width when style is none or hidden
2530    let unresolved_border = UnresolvedEdge {
2531        top: if style_zeroes_width(bs_top) { PixelValue::const_px(0) } else { to_pixel_value(border_top_mv) },
2532        right: if style_zeroes_width(bs_right) { PixelValue::const_px(0) } else { to_pixel_value(border_right_mv) },
2533        bottom: if style_zeroes_width(bs_bottom) { PixelValue::const_px(0) } else { to_pixel_value(border_bottom_mv) },
2534        left: if style_zeroes_width(bs_left) { PixelValue::const_px(0) } else { to_pixel_value(border_left_mv) },
2535    };
2536    { let _ = (0xC0_000007u32); } // after border block (incl is_normal/compact_cache fast-path)
2537
2538    // +spec:box-model:8538a9 - Internal table elements do not have margins (CSS 2.2 §17.5)
2539    // "These boxes have content and borders and cells have padding as well.
2540    //  Internal table elements do not have margins."
2541    // +spec:box-model:b4923a - Internal table elements do not have margins (CSS 2.2 § 17.5)
2542    // +spec:box-model:0a9f8e - Internal table elements do not have margins (CSS 2.2 § 17.5)
2543    let display_type = get_display_type(styled_dom, dom_id);
2544    let unresolved_margin = match display_type {
2545        LayoutDisplay::TableRow
2546        | LayoutDisplay::TableRowGroup
2547        | LayoutDisplay::TableHeaderGroup
2548        | LayoutDisplay::TableFooterGroup
2549        | LayoutDisplay::TableCell
2550        | LayoutDisplay::TableColumn
2551        | LayoutDisplay::TableColumnGroup => UnresolvedEdge {
2552            top: UnresolvedMargin::Zero,
2553            right: UnresolvedMargin::Zero,
2554            bottom: UnresolvedMargin::Zero,
2555            left: UnresolvedMargin::Zero,
2556        },
2557        // +spec:box-model:1197a5 - height property does not apply to non-replaced inline elements; vertical margins zeroed
2558        // +spec:replaced-elements:f07118 - non-replaced elements have rendering dictated by CSS model
2559        // "These properties apply to all elements, but vertical margins will not have
2560        //  any effect on non-replaced inline elements."
2561        LayoutDisplay::Inline => {
2562            let is_replaced = matches!(
2563                node_data.get_node_type(),
2564                NodeType::Image(_) | NodeType::VirtualView
2565            );
2566            if is_replaced {
2567                unresolved_margin
2568            } else {
2569                UnresolvedEdge {
2570                    top: UnresolvedMargin::Zero,
2571                    bottom: UnresolvedMargin::Zero,
2572                    ..unresolved_margin
2573                }
2574            }
2575        },
2576        _ => unresolved_margin,
2577    };
2578
2579    // Build the UnresolvedBoxProps
2580    let unresolved = UnresolvedBoxProps {
2581        margin: unresolved_margin,
2582        padding: unresolved_padding,
2583        border: unresolved_border,
2584    };
2585
2586    // Create initial resolution params (with viewport as containing block for now)
2587    let params = crate::solver3::geometry::ResolutionParams {
2588        containing_block: viewport_size,
2589        viewport_size,
2590        element_font_size: context.parent_font_size,
2591        root_font_size: context.root_font_size,
2592    };
2593
2594    // Resolve to get initial box_props
2595    let resolved = unresolved.resolve(&params);
2596
2597    if let Some(msgs) = debug_messages.as_mut() {
2598        msgs.push(LayoutDebugMessage::box_props(format!(
2599            "[BOX] node[{}] {:?} pad=[{:.1} {:.1} {:.1} {:.1}] mar=[{:.1} {:.1} {:.1} {:.1}] bor=[{:.1} {:.1} {:.1} {:.1}]",
2600            dom_id.index(), node_data.node_type,
2601            resolved.padding.top, resolved.padding.right, resolved.padding.bottom, resolved.padding.left,
2602            resolved.margin.top, resolved.margin.right, resolved.margin.bottom, resolved.margin.left,
2603            resolved.border.top, resolved.border.right, resolved.border.bottom, resolved.border.left,
2604        )));
2605
2606        let has_vh = match &unresolved_margin.top {
2607            UnresolvedMargin::Length(pv) => pv.metric == azul_css::props::basic::SizeMetric::Vh,
2608            _ => false,
2609        };
2610        if has_vh || resolved.margin.top > 0.0 || resolved.margin.left > 0.0 {
2611            msgs.push(LayoutDebugMessage::box_props(format!(
2612                "NodeId {:?} ({:?}): unresolved_margin_top={:?}, resolved_margin_top={:.2}, viewport_size={:?}",
2613                dom_id, node_data.node_type,
2614                unresolved_margin.top,
2615                resolved.margin.top,
2616                viewport_size
2617            )));
2618        }
2619
2620        msgs.push(LayoutDebugMessage::box_props(format!(
2621            "NodeId {:?} ({:?}): margin_auto: left={}, right={}, top={}, bottom={} | margin_left={:?}",
2622            dom_id, node_data.node_type,
2623            resolved.margin_auto.left, resolved.margin_auto.right,
2624            resolved.margin_auto.top, resolved.margin_auto.bottom,
2625            unresolved_margin.left
2626        )));
2627
2628        if matches!(node_data.node_type, NodeType::Body) {
2629            msgs.push(LayoutDebugMessage::box_props(format!(
2630                "Body margin resolved: top={:.2}, right={:.2}, bottom={:.2}, left={:.2}",
2631                resolved.margin.top, resolved.margin.right,
2632                resolved.margin.bottom, resolved.margin.left
2633            )));
2634        }
2635    }
2636
2637    CollectedBoxProps { unresolved, resolved }
2638}
2639
2640/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 1:
2641///
2642/// "Remove all irrelevant boxes. These are boxes that do not contain table-related boxes
2643/// and do not themselves have 'display' set to a table-related value. In this context,
2644/// 'irrelevant boxes' means anonymous inline boxes that contain only white space."
2645///
2646/// Checks if a DOM node is whitespace-only text (for table anonymous box generation).
2647/// Returns true if the node is a text node containing only whitespace characters
2648/// that would be collapsed away by the white-space property.
2649// according to the 'white-space' property does not generate any anonymous inline boxes (CSS2§9.2.2.1)
2650#[must_use] pub fn is_whitespace_only_text(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2651    let binding = styled_dom.node_data.as_container();
2652    let node_data = binding.get(node_id);
2653    if let Some(data) = node_data {
2654        if let NodeType::Text(text) = data.get_node_type() {
2655            // Check if the text contains only CSS document white space characters
2656            // Per CSS Text 3 §4.1: document white space = U+0020, U+0009, segment breaks
2657            if !text.chars().all(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')) {
2658                return false;
2659            }
2660            // Per CSS2§9.2.2.1: "White space content that would subsequently be
2661            // collapsed away according to the 'white-space' property does not
2662            // generate any anonymous inline boxes."
2663            // For white-space: pre / pre-wrap / break-spaces, whitespace is preserved
2664            // and should NOT be treated as collapsible.
2665            let white_space = styled_dom
2666                .styled_nodes
2667                .as_container()
2668                .get(node_id)
2669                .map_or(StyleWhiteSpace::Normal, |n| {
2670                    match get_white_space_property(styled_dom, node_id, &n.styled_node_state) {
2671                        MultiValue::Exact(ws) => ws,
2672                        _ => StyleWhiteSpace::Normal,
2673                    }
2674                });
2675            return match white_space {
2676                // These values collapse whitespace — whitespace-only text is collapsible
2677                StyleWhiteSpace::Normal | StyleWhiteSpace::Nowrap | StyleWhiteSpace::PreLine => true,
2678                // These values preserve whitespace — whitespace-only text is NOT collapsible
2679                StyleWhiteSpace::Pre | StyleWhiteSpace::PreWrap | StyleWhiteSpace::BreakSpaces => false,
2680            };
2681        }
2682    }
2683
2684    false
2685}
2686
2687/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 1:
2688/// Determines if a node should be skipped in table structure generation.
2689/// Whitespace-only text nodes are "irrelevant" and should not generate boxes
2690/// when they appear between table-related elements.
2691///
2692/// Returns true if the node should be skipped (i.e., it's whitespace-only text
2693/// and the parent is a table structural element).
2694fn should_skip_for_table_structure(
2695    styled_dom: &StyledDom,
2696    node_id: NodeId,
2697    parent_display: LayoutDisplay,
2698) -> bool {
2699    // CSS 2.2 Section 17.2.1: Only skip whitespace text nodes when parent is
2700    // a table structural element (table, row group, row)
2701    matches!(
2702        parent_display,
2703        LayoutDisplay::Table
2704            | LayoutDisplay::InlineTable
2705            | LayoutDisplay::TableRowGroup
2706            | LayoutDisplay::TableHeaderGroup
2707            | LayoutDisplay::TableFooterGroup
2708            | LayoutDisplay::TableRow
2709    ) && is_whitespace_only_text(styled_dom, node_id)
2710}
2711
2712/// Returns true if the given display type is a "proper table child" of a table/inline-table box.
2713/// Per CSS 2.2 §17.2.1, proper table children are: table-row-group, table-header-group,
2714/// table-footer-group, table-row, table-column-group, table-column, table-caption.
2715const fn is_proper_table_child(display: LayoutDisplay) -> bool {
2716    matches!(
2717        display,
2718        LayoutDisplay::TableRowGroup
2719            | LayoutDisplay::TableHeaderGroup
2720            | LayoutDisplay::TableFooterGroup
2721            | LayoutDisplay::TableRow
2722            | LayoutDisplay::TableColumnGroup
2723            | LayoutDisplay::TableColumn
2724            | LayoutDisplay::TableCaption
2725    )
2726}
2727
2728// Determines the display type of a node based on its tag and CSS properties.
2729// Delegates to getters::get_display_property which uses the compact cache fast path.
2730// M12.7 ROOT: get_display_type (and every layout enum getter) mis-lifts to wasm via the
2731// remill enum-return/decode path — the geometry-chain blocker. FOUR Rust workarounds all
2732// FAILED to advance (none reached collect_box_props past get_display_type):
2733//   1. skip the get_css_property! enum compact-cache fast path  → no change
2734//   2. replace the LayoutDisplay `match` with a branchless bitmask → no change
2735//   3. #[inline(never)] (wrap the call w/ enforce_sp_preservation) → made it diverge earlier
2736//   4. bypass MultiValue<LayoutDisplay> by reading cc.get_display() directly → diverges earlier
2737// So it is NOT the match codegen, NOT the MultiValue wrapper, NOT a frame/SP issue — it is
2738// the lift of a fn RETURNING a small fieldless enum (LayoutDisplay) corrupting control flow
2739// (pixel/i16-returning getters lift fine). Needs the remill m12-q-reg-x8-sret fork's
2740// enum-return handling — not fixable in Rust. (Original kept.)
2741#[must_use] pub fn get_display_type(styled_dom: &StyledDom, node_id: NodeId) -> LayoutDisplay {
2742    use crate::solver3::getters::get_display_property;
2743    get_display_property(styled_dom, Some(node_id)).unwrap_or(LayoutDisplay::Inline)
2744}
2745
2746// +spec:display-contents:95faa5 - blockification has no effect on none/contents (other => other)
2747// +spec:display-property:f68848 - Automatic box type transformations: blockification of computed display values
2748/// Blockify a display type per CSS Display 3 §2.7.
2749// +spec:display-property:760c5f - blockification sets computed outer display type to block
2750/// +spec:display-property:d50f70 - blockification affects computed values, determining principal box type only
2751/// // +spec:inline-block:692e44 - blockification of inline-block per CSS2 compatibility
2752// +spec:display-property:c3aca2 - inline-block blockifies to block, not flow-root
2753// +spec:display-property:ee2d65 - blockification of inline-level display types (CSS Display 3 §2.7)
2754// +spec:display-property:e4a8b7 - layout-internal boxes blockified to flow (block container)
2755/// CSS Flexbox §3: flex items with table-internal display values
2756/// (table-cell, table-row, table-row-group, table-header-group, table-footer-group,
2757/// table-column, table-column-group, table-caption) are blockified to display:block
2758/// before anonymous table box generation can occur. E.g. two consecutive
2759/// display:table-cell flex items become two separate display:block flex items.
2760fn blockify_flex_item_if_table_internal(nodes: &mut [LayoutNode], node_idx: usize) {
2761    if let Some(node) = nodes.get_mut(node_idx) {
2762        let is_table_internal = matches!(
2763            node.formatting_context,
2764            FormattingContext::TableCell
2765                | FormattingContext::TableRow
2766                | FormattingContext::TableRowGroup
2767                | FormattingContext::TableColumnGroup
2768                | FormattingContext::TableCaption
2769                | FormattingContext::Table
2770        );
2771        if is_table_internal {
2772            node.formatting_context = FormattingContext::Block {
2773                establishes_new_context: true,
2774            };
2775        }
2776    }
2777}
2778
2779/// Returns true if the node is a replaced element per CSS Display 3 Appendix B.
2780/// Replaced elements (img, canvas, embed, object, audio, video, input, textarea,
2781/// select, br, wbr, meter, progress, virtual views) cannot be un-boxed by
2782/// `display: contents` and always establish an independent formatting context.
2783const fn is_replaced_element(node_data: &NodeData) -> bool {
2784    matches!(
2785        node_data.get_node_type(),
2786        NodeType::Image(_)
2787        | NodeType::VirtualView
2788        | NodeType::Br
2789        | NodeType::Wbr
2790        | NodeType::Meter
2791        | NodeType::Progress
2792        | NodeType::Canvas
2793        | NodeType::Embed
2794        | NodeType::Object
2795        | NodeType::Audio
2796        | NodeType::Video
2797        | NodeType::Input
2798        | NodeType::TextArea
2799        | NodeType::Select
2800    )
2801}
2802
2803// +spec:display-property:285fe7 - block box establishing a BFC (block-level block container with new BFC)
2804/// **Corrected:** Checks for all conditions that create a new Block Formatting Context.
2805/// A BFC contains floats and prevents margin collapse.
2806fn establishes_new_block_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2807    let display = get_display_type(styled_dom, node_id);
2808    if matches!(
2809        display,
2810        LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::TableCaption | LayoutDisplay::FlowRoot
2811    ) {
2812        return true;
2813    }
2814
2815    if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
2816        // Only an EXPLICIT overflow of hidden/scroll/auto establishes a BFC. The
2817        // initial value is `visible` (no BFC), so an unset overflow — which the
2818        // slow cascade path returns as the `MultiValue::Auto` "not set" sentinel —
2819        // must NOT trigger one (`!is_visible_or_clip()` wrongly did, since the
2820        // sentinel is neither visible nor clip).
2821        let overflow_x = get_overflow_x(styled_dom, node_id, &styled_node.styled_node_state);
2822        let overflow_y = get_overflow_y(styled_dom, node_id, &styled_node.styled_node_state);
2823        if overflow_x.establishes_bfc() || overflow_y.establishes_bfc() {
2824            return true;
2825        }
2826
2827        let position = get_position(styled_dom, node_id, &styled_node.styled_node_state);
2828        if position.is_absolute_or_fixed() {
2829            return true;
2830        }
2831
2832        let float = get_float(styled_dom, node_id, &styled_node.styled_node_state);
2833        if !float.is_none() {
2834            return true;
2835        }
2836    }
2837
2838    // CSS Writing Modes 4 § 3.2: block container with different writing-mode than parent establishes BFC
2839    if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
2840        let hierarchy = styled_dom.node_hierarchy.as_container();
2841        if let Some(parent_dom_id) = hierarchy[node_id].parent_id() {
2842            let parent_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
2843            let child_wm = get_writing_mode(styled_dom, node_id, &styled_node.styled_node_state).unwrap_or_default();
2844            let parent_wm = get_writing_mode(styled_dom, parent_dom_id, parent_state).unwrap_or_default();
2845            if child_wm != parent_wm {
2846                return true;
2847            }
2848        }
2849    }
2850
2851    // +spec:replaced-elements:4f494d - replaced elements always establish an independent formatting context
2852    let node_data = &styled_dom.node_data.as_container()[node_id];
2853    if is_replaced_element(node_data) {
2854        return true;
2855    }
2856
2857    // The root element (<html>) also establishes a BFC.
2858    if styled_dom.root.into_crate_internal() == Some(node_id) {
2859        return true;
2860    }
2861
2862    false
2863}
2864
2865// +spec:display-property:0d93f1 - maps display value to box generation (principal box, none, or contents)
2866/// Like `determine_formatting_context`, but uses an explicit (possibly blockified) display type
2867/// instead of reading it from the DOM. Used when blockification changes the display.
2868// +spec:display-property:80f43f - inner display type defines formatting context for non-replaced elements
2869// +spec:display-property:46e71c - Maps outer display (block/inline) and inner display (flow/flow-root/table/flex/grid) to FormattingContext
2870// +spec:display-property:aa582d - maps display types to formatting contexts (inline-level, block-level, atomic inline, block container)
2871#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
2872fn determine_formatting_context_for_display(
2873    styled_dom: &StyledDom,
2874    node_id: NodeId,
2875    display_type: LayoutDisplay,
2876) -> FormattingContext {
2877    let node_data = &styled_dom.node_data.as_container()[node_id];
2878    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
2879        // [g147h az-web-lift DIAG] CONSTANT marker of the COMPUTED FC per DOM node_id (0x60B60+slot),
2880        // written WITHOUT reading the stored field. 1=text→Inline, 2=block-with-inline→Inline, 4=Block.
2881        // For the divs (node_id 1,3): 2 ⇒ computed Inline correctly (bug is store/clone/read); 4 ⇒
2882        // has_only_inline_children mis-lifted to false (computed Block).
2883        #[cfg(feature = "web_lift")]
2884        unsafe { crate::az_mark(((0x60B60 + (node_id.index() & 7) * 4)) as u32, (0xC0DE0001) as u32); }
2885        return FormattingContext::Inline;
2886    }
2887    // +spec:display-property:2a8d62 - block containers with inline-level content establish an IFC
2888    match display_type {
2889        // +spec:display-property:37bcf3 - inline outer display type generates an inline box
2890        // +spec:display-property:30a935 - outer display without inner defaults to flow (block/inline both use flow context)
2891        LayoutDisplay::Inline => FormattingContext::Inline,
2892        // +spec:block-formatting-context:97b03b - flow-root always establishes a new BFC; block/list-item may establish one based on other conditions
2893        // +spec:display-property:0bac26 - list-item limited to flow layout inner types (block/flow-root)
2894        // +spec:display-property:0beffc - block container with only inline children establishes IFC
2895        // +spec:display-property:7c49c1 - block container with only inline children establishes an IFC
2896        // +spec:display-property:90ba2a - flow-root always establishes a new BFC
2897        LayoutDisplay::FlowRoot => FormattingContext::Block {
2898            establishes_new_context: true,
2899        },
2900        LayoutDisplay::Block | LayoutDisplay::ListItem => {
2901            if has_only_inline_children(styled_dom, node_id) {
2902                #[cfg(feature = "web_lift")]
2903                unsafe { crate::az_mark(((0x60B60 + (node_id.index() & 7) * 4)) as u32, (0xC0DE0002) as u32); }
2904                FormattingContext::Inline
2905            } else {
2906                #[cfg(feature = "web_lift")]
2907                unsafe { crate::az_mark(((0x60B60 + (node_id.index() & 7) * 4)) as u32, (0xC0DE0004) as u32); }
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    // [g147j az-web-lift DIAG] OUTER determine_ entry (0x60BB0+slot): 1=Text early-exit,
2959    // 0x10|disc = went through for_display and returned that repr(C,u8) discriminant.
2960    // Discriminates "never called during the lifted build" (slot stays 0) vs "called but
2961    // the for_display match mis-routes" (here=0x10|x while the g147h inner markers stay 0)
2962    // vs "value correct at build, corrupted later" (here says Inline, dispatch reads garbage).
2963    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
2964        #[cfg(feature = "web_lift")]
2965        unsafe { crate::az_mark(0x60BB0 + (node_id.index() & 7) as u32 * 4, 0xC0DE0001); }
2966        return FormattingContext::Inline;
2967    }
2968    let display_type = get_display_type(styled_dom, node_id);
2969    let fc = determine_formatting_context_for_display(styled_dom, node_id, display_type);
2970    #[cfg(feature = "web_lift")]
2971    unsafe {
2972        let disc: u8 = core::ptr::read_volatile((&fc) as *const FormattingContext as *const u8);
2973        crate::az_mark(0x60BB0 + (node_id.index() & 7) as u32 * 4, 0xC0DE0010 | disc as u32);
2974    }
2975    fc
2976}
2977
2978#[cfg(test)]
2979#[allow(clippy::float_cmp, clippy::too_many_lines)]
2980mod autotest_generated {
2981    use azul_core::{
2982        dom::{Dom, IdOrClass},
2983        resources::{ImageRef, RawImageFormat},
2984        selection::ContentIndex,
2985    };
2986
2987    use super::*;
2988    use crate::{
2989        solver3::geometry::{EdgeSizes, PackedBoxProps},
2990        text3::cache::{
2991            BreakType, ClearType, InlineBreak, OverflowInfo, Point, PositionedItem, Rect,
2992            ShapedItem,
2993        },
2994    };
2995
2996    // ==================================================================
2997    // Fixtures
2998    // ==================================================================
2999
3000    const VIEWPORT: LogicalSize = LogicalSize {
3001        width: 800.0,
3002        height: 600.0,
3003    };
3004
3005    fn styled(dom: Dom, css_str: &str) -> StyledDom {
3006        let mut dom = dom;
3007        let (css, _warnings) = azul_css::parser2::new_from_str(css_str);
3008        StyledDom::create(&mut dom, css)
3009    }
3010
3011    fn div_class(class: &str) -> Dom {
3012        Dom::create_div().with_ids_and_classes(vec![IdOrClass::Class(class.into())].into())
3013    }
3014
3015    /// Runs the real build pipeline (`process_node` → `build` → STF bitmap) —
3016    /// i.e. everything `generate_layout_tree` does minus the `LayoutContext`
3017    /// (which would need a system-font `FontManager`).
3018    fn build_tree(styled_dom: &StyledDom) -> LayoutTree {
3019        let mut builder = LayoutTreeBuilder::new(VIEWPORT);
3020        let mut msgs: Option<Vec<LayoutDebugMessage>> = None;
3021        let root_id = styled_dom
3022            .root
3023            .into_crate_internal()
3024            .unwrap_or(NodeId::ZERO);
3025        let root_index = builder
3026            .process_node(styled_dom, root_id, None, &mut msgs)
3027            .expect("process_node on a well-formed DOM");
3028        let mut tree = builder.build(root_index);
3029        tree.subtree_needs_intrinsic = compute_subtree_needs_intrinsic(styled_dom, &tree);
3030        tree
3031    }
3032
3033    /// `body(0) > [ .block(1) > [ text "hello"(2), .inline(3) > text "world"(4) ],
3034    ///              .mixed(5) > [ text " \n\t"(6), .block2(7), text "tail"(8) ] ]`
3035    ///
3036    /// The `.mixed` subtree is the interesting one: a whitespace-only inline run
3037    /// followed by a block sibling and a real inline run — exactly the CSS 2.1
3038    /// §9.2.2.1 anonymous-box case.
3039    fn mixed_dom() -> StyledDom {
3040        styled(
3041            Dom::create_body()
3042                .with_child(
3043                    div_class("block")
3044                        .with_child(Dom::create_text("hello"))
3045                        .with_child(div_class("inline").with_child(Dom::create_text("world"))),
3046                )
3047                .with_child(
3048                    div_class("mixed")
3049                        .with_child(Dom::create_text(" \n\t"))
3050                        .with_child(div_class("block2"))
3051                        .with_child(Dom::create_text("tail")),
3052                ),
3053            ".block { display: block; } .inline { display: inline; } .mixed { display: block; } \
3054             .block2 { display: block; }",
3055        )
3056    }
3057
3058    /// Locates a DOM node by its exact text content. Keeps the tests structural
3059    /// instead of hard-coding `CompactDom` pre-order indices.
3060    fn text_node(styled_dom: &StyledDom, needle: &str) -> NodeId {
3061        let container = styled_dom.node_data.as_container();
3062        for i in 0..styled_dom.node_data.len() {
3063            let id = NodeId::new(i);
3064            if let NodeType::Text(text) = container[id].get_node_type() {
3065                if text.as_str() == needle {
3066                    return id;
3067                }
3068            }
3069        }
3070        panic!("no text node with content {needle:?}");
3071    }
3072
3073    fn empty_layout() -> Arc<UnifiedLayout> {
3074        Arc::new(UnifiedLayout {
3075            items: Vec::new(),
3076            overflow: OverflowInfo::default(),
3077        })
3078    }
3079
3080    fn layout_of(items: Vec<PositionedItem>) -> Arc<UnifiedLayout> {
3081        Arc::new(UnifiedLayout {
3082            items,
3083            overflow: OverflowInfo::default(),
3084        })
3085    }
3086
3087    fn tab_item(width: f32, height: f32, x: f32, line_index: usize) -> PositionedItem {
3088        PositionedItem {
3089            item: ShapedItem::Tab {
3090                source: ContentIndex {
3091                    run_index: 0,
3092                    item_index: 0,
3093                },
3094                bounds: Rect {
3095                    x: 0.0,
3096                    y: 0.0,
3097                    width,
3098                    height,
3099                },
3100            },
3101            position: Point { x, y: 0.0 },
3102            line_index,
3103        }
3104    }
3105
3106    fn break_item(line_index: usize) -> PositionedItem {
3107        PositionedItem {
3108            item: ShapedItem::Break {
3109                source: ContentIndex {
3110                    run_index: 0,
3111                    item_index: 0,
3112                },
3113                break_info: InlineBreak {
3114                    break_type: BreakType::Hard,
3115                    clear: ClearType::None,
3116                    content_index: 0,
3117                },
3118            },
3119            position: Point { x: 0.0, y: 0.0 },
3120            line_index,
3121        }
3122    }
3123
3124    fn hot(parent: Option<usize>) -> LayoutNodeHot {
3125        LayoutNodeHot {
3126            box_props: PackedBoxProps::default(),
3127            dom_node_id: None,
3128            used_size: None,
3129            formatting_context: FormattingContext::Block {
3130                establishes_new_context: false,
3131            },
3132            parent,
3133        }
3134    }
3135
3136    /// Hand-assembles a `LayoutTree` from raw hot nodes + child lists so the
3137    /// index/cycle edge cases the builder can never produce are still reachable.
3138    fn raw_tree(nodes: Vec<LayoutNodeHot>, child_lists: &[Vec<usize>]) -> LayoutTree {
3139        let n = nodes.len();
3140        let mut children_arena: Vec<usize> = Vec::new();
3141        let mut children_offsets: Vec<(u32, u32)> = Vec::with_capacity(n);
3142        for cl in child_lists {
3143            let start = u32::try_from(children_arena.len()).unwrap();
3144            children_arena.extend_from_slice(cl);
3145            children_offsets.push((start, u32::try_from(cl.len()).unwrap()));
3146        }
3147        while children_offsets.len() < n {
3148            children_offsets.push((0, 0));
3149        }
3150        LayoutTree {
3151            nodes,
3152            warm: vec![LayoutNodeWarm::default(); n],
3153            cold: vec![LayoutNodeCold::default(); n],
3154            root: 0,
3155            dom_to_layout: BTreeMap::new(),
3156            children_arena,
3157            children_offsets,
3158            subtree_needs_intrinsic: Vec::new(),
3159        }
3160    }
3161
3162    const ALL_DISPLAYS: [LayoutDisplay; 23] = [
3163        LayoutDisplay::None,
3164        LayoutDisplay::Block,
3165        LayoutDisplay::Inline,
3166        LayoutDisplay::InlineBlock,
3167        LayoutDisplay::Flex,
3168        LayoutDisplay::InlineFlex,
3169        LayoutDisplay::Table,
3170        LayoutDisplay::InlineTable,
3171        LayoutDisplay::TableRowGroup,
3172        LayoutDisplay::TableHeaderGroup,
3173        LayoutDisplay::TableFooterGroup,
3174        LayoutDisplay::TableRow,
3175        LayoutDisplay::TableColumnGroup,
3176        LayoutDisplay::TableColumn,
3177        LayoutDisplay::TableCell,
3178        LayoutDisplay::TableCaption,
3179        LayoutDisplay::FlowRoot,
3180        LayoutDisplay::ListItem,
3181        LayoutDisplay::RunIn,
3182        LayoutDisplay::Marker,
3183        LayoutDisplay::Grid,
3184        LayoutDisplay::InlineGrid,
3185        LayoutDisplay::Contents,
3186    ];
3187
3188    // ==================================================================
3189    // IfcId — thread-local counter (numeric / overflow)
3190    // ==================================================================
3191
3192    #[test]
3193    fn ifcid_unique_hands_out_a_fresh_id_per_call_after_reset() {
3194        IfcId::reset_counter();
3195        assert_eq!(IfcId::unique(), IfcId(0));
3196        assert_eq!(IfcId::unique(), IfcId(1));
3197        assert_eq!(IfcId::unique(), IfcId(2));
3198        IfcId::reset_counter();
3199        assert_eq!(
3200            IfcId::unique(),
3201            IfcId(0),
3202            "reset_counter must restart the sequence, not continue it"
3203        );
3204        IfcId::reset_counter();
3205    }
3206
3207    #[test]
3208    fn ifcid_reset_counter_is_idempotent() {
3209        IfcId::reset_counter();
3210        IfcId::reset_counter();
3211        IfcId::reset_counter();
3212        assert_eq!(IfcId::unique(), IfcId(0));
3213        IfcId::reset_counter();
3214    }
3215
3216    #[test]
3217    fn ifcid_unique_wraps_at_u32_max_instead_of_panicking() {
3218        // `wrapping_add` is deliberate: a layout pass with 2^32 IFCs is not a
3219        // thing, but a debug-mode overflow panic in the middle of layout is.
3220        IFC_ID_COUNTER.with(|c| c.set(u32::MAX));
3221        assert_eq!(IfcId::unique(), IfcId(u32::MAX));
3222        assert_eq!(IfcId::unique(), IfcId(0), "wraps rather than overflow-panics");
3223        assert_eq!(IfcId::unique(), IfcId(1));
3224        IfcId::reset_counter();
3225    }
3226
3227    // ==================================================================
3228    // CachedInlineLayout — constructors + metrics extraction
3229    // ==================================================================
3230
3231    #[test]
3232    fn cached_inline_layout_new_keeps_the_args_it_was_given() {
3233        let arc = empty_layout();
3234        let c = CachedInlineLayout::new(Arc::clone(&arc), AvailableSpace::Definite(123.5), true);
3235        assert!(Arc::ptr_eq(&c.layout, &arc));
3236        assert_eq!(c.available_width, AvailableSpace::Definite(123.5));
3237        assert!(c.has_floats);
3238        assert!(c.constraints.is_none(), "new() carries no constraints");
3239        assert!(c.line_breaks.is_none(), "new() computes no line breaks");
3240        assert_eq!(c.inline_content_hash, 0, "0 = unknown ⇒ never fast-path-reuse");
3241        assert!(c.item_metrics.is_empty(), "an empty layout has no item metrics");
3242    }
3243
3244    #[test]
3245    fn cached_inline_layout_new_survives_extreme_widths() {
3246        for w in [
3247            AvailableSpace::Definite(0.0),
3248            AvailableSpace::Definite(-1.0),
3249            AvailableSpace::Definite(f32::MAX),
3250            AvailableSpace::Definite(f32::MIN),
3251            AvailableSpace::Definite(f32::INFINITY),
3252            AvailableSpace::Definite(f32::NEG_INFINITY),
3253            AvailableSpace::Definite(f32::NAN),
3254            AvailableSpace::MinContent,
3255            AvailableSpace::MaxContent,
3256        ] {
3257            let c = CachedInlineLayout::new(empty_layout(), w, false);
3258            assert!(c.item_metrics.is_empty());
3259            assert!(c.layout.items.is_empty());
3260        }
3261    }
3262
3263    #[test]
3264    fn extract_item_metrics_mirrors_every_positioned_item() {
3265        let layout = layout_of(vec![tab_item(12.0, 20.0, 5.0, 3), tab_item(0.0, 0.0, 0.0, 0)]);
3266        let m = CachedInlineLayout::extract_item_metrics(&layout);
3267        assert_eq!(m.len(), 2, "one metric entry per PositionedItem, in order");
3268
3269        assert_eq!(m[0].advance_width, 12.0);
3270        assert_eq!(m[0].x_offset, 5.0);
3271        assert_eq!(m[0].line_index, 3);
3272        assert!(m[0].can_break, "a Tab is breakable");
3273        assert!(
3274            m[0].source_node_id.is_none(),
3275            "non-Cluster items expose no source_node_id"
3276        );
3277        // Tab metrics are the fallback ascent/descent split of the item height.
3278        assert!(
3279            (m[0].line_height_contribution - 20.0).abs() < 1e-3,
3280            "ascent+descent should reconstruct the height, got {}",
3281            m[0].line_height_contribution
3282        );
3283
3284        assert_eq!(m[1].advance_width, 0.0);
3285        assert_eq!(m[1].line_index, 0);
3286    }
3287
3288    #[test]
3289    fn extract_item_metrics_marks_break_items_as_unbreakable_and_zero_sized() {
3290        let layout = layout_of(vec![break_item(7)]);
3291        let m = CachedInlineLayout::extract_item_metrics(&layout);
3292        assert_eq!(m.len(), 1);
3293        assert!(!m[0].can_break, "ShapedItem::Break is the one non-breakable item");
3294        assert_eq!(m[0].advance_width, 0.0, "a break has no visual geometry");
3295        assert_eq!(m[0].line_height_contribution, 0.0);
3296        assert_eq!(m[0].line_index, 7);
3297    }
3298
3299    #[test]
3300    fn extract_item_metrics_on_an_empty_layout_is_empty_not_a_panic() {
3301        assert!(CachedInlineLayout::extract_item_metrics(&empty_layout()).is_empty());
3302    }
3303
3304    #[test]
3305    fn extract_item_metrics_does_not_choke_on_non_finite_item_bounds() {
3306        let layout = layout_of(vec![
3307            tab_item(f32::INFINITY, f32::NAN, f32::NEG_INFINITY, u32::MAX as usize),
3308            tab_item(f32::MAX, f32::MAX, f32::MIN, 0),
3309        ]);
3310        let m = CachedInlineLayout::extract_item_metrics(&layout);
3311        assert_eq!(m.len(), 2);
3312        assert!(m[0].advance_width.is_infinite());
3313        assert!(m[0].line_height_contribution.is_nan(), "NaN in, NaN out — but no panic");
3314        assert_eq!(m[1].advance_width, f32::MAX);
3315    }
3316
3317    #[test]
3318    fn extract_item_metrics_truncates_a_huge_line_index_into_u32() {
3319        // `line_index` is a usize on PositionedItem but a u32 in the metrics —
3320        // the cast is `as`, so it wraps rather than panicking.
3321        let huge = (u32::MAX as usize) + 5;
3322        let m = CachedInlineLayout::extract_item_metrics(&layout_of(vec![tab_item(
3323            1.0, 1.0, 0.0, huge,
3324        )]));
3325        assert_eq!(m[0].line_index, 4, "wrapping `as u32` truncation, not a panic");
3326    }
3327
3328    #[test]
3329    fn cached_inline_layout_new_with_constraints_records_constraints_and_line_breaks() {
3330        let arc = layout_of(vec![tab_item(10.0, 20.0, 0.0, 0)]);
3331        let c = CachedInlineLayout::new_with_constraints(
3332            Arc::clone(&arc),
3333            AvailableSpace::Definite(200.0),
3334            false,
3335            UnifiedConstraints::default(),
3336        );
3337        assert!(c.constraints.is_some());
3338        let lb = c.line_breaks.expect("new_with_constraints computes line breaks");
3339        assert_eq!(lb.available_width, 200.0);
3340        assert_eq!(c.item_metrics.len(), 1);
3341    }
3342
3343    #[test]
3344    fn new_with_constraints_treats_indefinite_widths_as_f32_max() {
3345        for w in [AvailableSpace::MinContent, AvailableSpace::MaxContent] {
3346            let c = CachedInlineLayout::new_with_constraints(
3347                empty_layout(),
3348                w,
3349                false,
3350                UnifiedConstraints::default(),
3351            );
3352            let lb = c.line_breaks.expect("line breaks");
3353            assert_eq!(
3354                lb.available_width,
3355                f32::MAX,
3356                "indefinite width collapses to f32::MAX for break extraction"
3357            );
3358            assert_eq!(c.available_width, w, "but the cache key keeps the real variant");
3359        }
3360    }
3361
3362    // ==================================================================
3363    // CachedInlineLayout — width matching / validity (predicates)
3364    // ==================================================================
3365
3366    fn cached(width: AvailableSpace, has_floats: bool) -> CachedInlineLayout {
3367        CachedInlineLayout::new(empty_layout(), width, has_floats)
3368    }
3369
3370    #[test]
3371    fn width_constraint_matches_definite_widths_within_the_epsilon() {
3372        let c = cached(AvailableSpace::Definite(100.0), false);
3373        assert!(c.width_constraint_matches(AvailableSpace::Definite(100.0)));
3374        assert!(
3375            c.width_constraint_matches(AvailableSpace::Definite(100.09)),
3376            "sub-0.1px drift must not force a relayout"
3377        );
3378        // 100.1 is NOT exactly 0.1 away: the f32 literal is 100.0999984741211, so the
3379        // real diff is ~0.09999847, which genuinely IS < 0.1. Binary floats, not
3380        // decimal. (Pick a literal that clears the epsilon after rounding to test the
3381        // miss branch -- see below.)
3382        assert!(c.width_constraint_matches(AvailableSpace::Definite(100.1)));
3383        assert!(
3384            !c.width_constraint_matches(AvailableSpace::Definite(100.2)),
3385            "the epsilon is strict (`< 0.1`), so a 0.2 drift must miss"
3386        );
3387        assert!(!c.width_constraint_matches(AvailableSpace::Definite(0.0)));
3388    }
3389
3390    #[test]
3391    fn width_constraint_matches_only_pairs_like_with_like() {
3392        let min = cached(AvailableSpace::MinContent, false);
3393        let max = cached(AvailableSpace::MaxContent, false);
3394        let def = cached(AvailableSpace::Definite(50.0), false);
3395
3396        assert!(min.width_constraint_matches(AvailableSpace::MinContent));
3397        assert!(max.width_constraint_matches(AvailableSpace::MaxContent));
3398        assert!(!min.width_constraint_matches(AvailableSpace::MaxContent));
3399        assert!(!max.width_constraint_matches(AvailableSpace::MinContent));
3400        assert!(!min.width_constraint_matches(AvailableSpace::Definite(50.0)));
3401        assert!(!def.width_constraint_matches(AvailableSpace::MinContent));
3402        assert!(!def.width_constraint_matches(AvailableSpace::MaxContent));
3403    }
3404
3405    #[test]
3406    fn width_constraint_matches_is_false_for_nan_widths_rather_than_panicking() {
3407        // (NaN - NaN).abs() is NaN, and `NaN < eps` is false — so a NaN-width
3408        // cache entry never validates. Deterministic (always relayout), not a panic.
3409        let c = cached(AvailableSpace::Definite(f32::NAN), false);
3410        assert!(!c.width_constraint_matches(AvailableSpace::Definite(f32::NAN)));
3411        assert!(!c.width_constraint_matches(AvailableSpace::Definite(0.0)));
3412
3413        let good = cached(AvailableSpace::Definite(10.0), false);
3414        assert!(!good.width_constraint_matches(AvailableSpace::Definite(f32::NAN)));
3415    }
3416
3417    #[test]
3418    fn width_constraint_matches_is_false_for_an_infinite_width_against_itself() {
3419        // inf - inf == NaN, so an infinite cached width never matches — the cache
3420        // simply always misses. Surprising, but safe and deterministic.
3421        let c = cached(AvailableSpace::Definite(f32::INFINITY), false);
3422        assert!(!c.width_constraint_matches(AvailableSpace::Definite(f32::INFINITY)));
3423        assert!(!c.is_valid_for(AvailableSpace::Definite(f32::INFINITY), false));
3424        assert!(c.should_replace_with(AvailableSpace::Definite(f32::INFINITY), false));
3425    }
3426
3427    #[test]
3428    fn width_constraint_matches_handles_huge_finite_widths() {
3429        let c = cached(AvailableSpace::Definite(f32::MAX), false);
3430        assert!(c.width_constraint_matches(AvailableSpace::Definite(f32::MAX)));
3431        assert!(!c.width_constraint_matches(AvailableSpace::Definite(f32::MIN)));
3432    }
3433
3434    #[test]
3435    fn is_valid_for_rejects_a_no_float_cache_when_the_request_gains_floats() {
3436        // A cached layout with no floats must be invalidated once the new request
3437        // carries floats, so the text re-wraps around them (#19); every other float
3438        // combination reduces to width_constraint_matches. This mirrors
3439        // should_replace_with()'s gain-float branch (they must stay consistent).
3440        let widths = [
3441            AvailableSpace::Definite(0.0),
3442            AvailableSpace::Definite(100.0),
3443            AvailableSpace::MinContent,
3444            AvailableSpace::MaxContent,
3445        ];
3446        for cached_floats in [false, true] {
3447            for cached_w in widths {
3448                let c = cached(cached_w, cached_floats);
3449                for new_w in widths {
3450                    let width_ok = c.width_constraint_matches(new_w);
3451                    // No-float request: pure width match (a float-aware cache is
3452                    // still kept for a no-float request, gated on width).
3453                    assert_eq!(c.is_valid_for(new_w, false), width_ok);
3454                    // Float request: a no-float cache is always rejected so the text
3455                    // re-wraps; a float-aware cache stays gated on width.
3456                    let expected_with_floats = if cached_floats { width_ok } else { false };
3457                    assert_eq!(c.is_valid_for(new_w, true), expected_with_floats);
3458                    // is_valid_for and should_replace_with must stay opposites on the
3459                    // gain-float axis.
3460                    if !cached_floats {
3461                        assert!(c.should_replace_with(new_w, true));
3462                    }
3463                }
3464            }
3465        }
3466    }
3467
3468    #[test]
3469    fn is_valid_for_returns_the_expected_true_and_false() {
3470        let c = cached(AvailableSpace::Definite(300.0), false);
3471        assert!(c.is_valid_for(AvailableSpace::Definite(300.0), false));
3472        assert!(!c.is_valid_for(AvailableSpace::Definite(299.0), false));
3473    }
3474
3475    #[test]
3476    fn should_replace_with_always_replaces_when_float_info_is_gained() {
3477        // Even at an identical width: a float-aware layout strictly dominates.
3478        let c = cached(AvailableSpace::Definite(300.0), false);
3479        assert!(c.should_replace_with(AvailableSpace::Definite(300.0), true));
3480        assert!(c.should_replace_with(AvailableSpace::MinContent, true));
3481    }
3482
3483    #[test]
3484    fn should_replace_with_keeps_a_float_aware_layout_at_a_matching_width() {
3485        let c = cached(AvailableSpace::Definite(300.0), true);
3486        assert!(
3487            !c.should_replace_with(AvailableSpace::Definite(300.0), false),
3488            "a non-float layout must not overwrite a float-aware one at the same width"
3489        );
3490        assert!(
3491            c.should_replace_with(AvailableSpace::Definite(100.0), false),
3492            "…but a width change still forces a replace"
3493        );
3494    }
3495
3496    #[test]
3497    fn should_replace_with_is_the_negation_of_is_valid_for_when_floats_are_unchanged() {
3498        let widths = [
3499            AvailableSpace::Definite(0.0),
3500            AvailableSpace::Definite(42.0),
3501            AvailableSpace::MinContent,
3502            AvailableSpace::MaxContent,
3503        ];
3504        for floats in [false, true] {
3505            for cached_w in widths {
3506                let c = cached(cached_w, floats);
3507                for new_w in widths {
3508                    assert_eq!(
3509                        c.should_replace_with(new_w, floats),
3510                        !c.is_valid_for(new_w, floats),
3511                        "cached={cached_w:?} new={new_w:?} floats={floats}"
3512                    );
3513                }
3514            }
3515        }
3516    }
3517
3518    // ==================================================================
3519    // CachedInlineLayout — getters
3520    // ==================================================================
3521
3522    #[test]
3523    fn get_layout_and_clone_layout_hand_back_the_very_same_arc() {
3524        let arc = layout_of(vec![tab_item(1.0, 2.0, 0.0, 0)]);
3525        let c = CachedInlineLayout::new(Arc::clone(&arc), AvailableSpace::MaxContent, false);
3526        assert!(Arc::ptr_eq(c.get_layout(), &arc));
3527
3528        let cloned = c.clone_layout();
3529        assert!(Arc::ptr_eq(&cloned, &arc), "clone_layout must not deep-copy");
3530        assert_eq!(
3531            Arc::strong_count(&arc),
3532            3,
3533            "the original + the cache's + the clone"
3534        );
3535        assert_eq!(c.get_layout().items.len(), 1);
3536    }
3537
3538    #[test]
3539    fn get_layout_works_on_an_empty_extreme_instance() {
3540        let c = cached(AvailableSpace::Definite(f32::NAN), true);
3541        assert!(c.get_layout().items.is_empty());
3542        assert!(c.clone_layout().items.is_empty());
3543    }
3544
3545    // ==================================================================
3546    // LayoutNode::split / LayoutTree::get_full_node — round-trip
3547    // ==================================================================
3548
3549    #[test]
3550    fn get_full_node_then_split_round_trips_through_the_soa_arrays() {
3551        let sd = mixed_dom();
3552        let tree = build_tree(&sd);
3553        assert!(tree.nodes.len() >= 2);
3554
3555        for i in 0..tree.nodes.len() {
3556            let full = tree.get_full_node(i).expect("in-range node");
3557            let (h, w, c) = full.split();
3558
3559            let hot = tree.get(i).unwrap();
3560            assert_eq!(h.dom_node_id, hot.dom_node_id, "node {i}");
3561            assert_eq!(h.parent, hot.parent, "node {i}");
3562            assert_eq!(h.used_size, hot.used_size, "node {i}");
3563            assert_eq!(h.formatting_context, hot.formatting_context, "node {i}");
3564            // box_props survive a pack → unpack → pack round-trip bit-for-bit.
3565            assert_eq!(h.box_props.margin, hot.box_props.margin, "node {i}");
3566            assert_eq!(h.box_props.padding, hot.box_props.padding, "node {i}");
3567            assert_eq!(h.box_props.border, hot.box_props.border, "node {i}");
3568
3569            let warm = tree.warm(i).unwrap();
3570            assert_eq!(w.pseudo_element, warm.pseudo_element, "node {i}");
3571            assert_eq!(w.baseline, warm.baseline, "node {i}");
3572            assert_eq!(
3573                w.computed_style.display, warm.computed_style.display,
3574                "node {i}"
3575            );
3576
3577            let cold = tree.cold(i).unwrap();
3578            assert_eq!(c.anonymous_type, cold.anonymous_type, "node {i}");
3579            assert_eq!(c.dirty_flag, cold.dirty_flag, "node {i}");
3580            assert_eq!(c.subtree_hash, cold.subtree_hash, "node {i}");
3581            assert_eq!(c.ifc_id, cold.ifc_id, "node {i}");
3582        }
3583    }
3584
3585    #[test]
3586    fn get_full_node_restores_the_children_from_the_arena() {
3587        let sd = mixed_dom();
3588        let tree = build_tree(&sd);
3589        for i in 0..tree.nodes.len() {
3590            let full = tree.get_full_node(i).unwrap();
3591            assert_eq!(full.children, tree.children(i).to_vec(), "node {i}");
3592        }
3593    }
3594
3595    #[test]
3596    fn get_full_node_is_none_out_of_range() {
3597        let tree = build_tree(&mixed_dom());
3598        assert!(tree.get_full_node(tree.nodes.len()).is_none());
3599        assert!(tree.get_full_node(usize::MAX).is_none());
3600    }
3601
3602    // ==================================================================
3603    // LayoutTree — index-taking accessors (numeric / min-max / overflow)
3604    // ==================================================================
3605
3606    #[test]
3607    fn tree_accessors_return_none_for_every_out_of_range_index() {
3608        let mut tree = build_tree(&mixed_dom());
3609        let n = tree.nodes.len();
3610        for idx in [n, n + 1, usize::MAX, usize::MAX - 1, usize::MAX / 2] {
3611            assert!(tree.get(idx).is_none(), "get({idx})");
3612            assert!(tree.warm(idx).is_none(), "warm({idx})");
3613            assert!(tree.cold(idx).is_none(), "cold({idx})");
3614            assert!(tree.get_mut(idx).is_none(), "get_mut({idx})");
3615            assert!(tree.warm_mut(idx).is_none(), "warm_mut({idx})");
3616            assert!(tree.cold_mut(idx).is_none(), "cold_mut({idx})");
3617            assert!(tree.get_inline_layout_for_node(idx).is_none());
3618        }
3619    }
3620
3621    #[test]
3622    fn tree_accessors_all_resolve_at_index_zero() {
3623        let mut tree = build_tree(&mixed_dom());
3624        assert!(tree.get(0).is_some());
3625        assert!(tree.warm(0).is_some());
3626        assert!(tree.cold(0).is_some());
3627        assert!(tree.get_mut(0).is_some());
3628        assert!(tree.warm_mut(0).is_some());
3629        assert!(tree.cold_mut(0).is_some());
3630        assert_eq!(tree.get(0).unwrap().parent, None, "index 0 is the root");
3631    }
3632
3633    #[test]
3634    fn children_of_an_out_of_range_index_is_an_empty_slice() {
3635        let tree = build_tree(&mixed_dom());
3636        assert!(tree.children(tree.nodes.len()).is_empty());
3637        assert!(tree.children(usize::MAX).is_empty());
3638        assert!(tree.children(usize::MAX - 1).is_empty());
3639    }
3640
3641    #[test]
3642    fn children_arena_slices_agree_with_the_parent_pointers() {
3643        let tree = build_tree(&mixed_dom());
3644        let n = tree.nodes.len();
3645        let mut seen: Vec<usize> = Vec::new();
3646        for i in 0..n {
3647            for &child in tree.children(i) {
3648                assert!(child < n, "child {child} of {i} is out of range");
3649                assert_eq!(
3650                    tree.get(child).unwrap().parent,
3651                    Some(i),
3652                    "child {child} does not point back at parent {i}"
3653                );
3654                seen.push(child);
3655            }
3656        }
3657        seen.sort_unstable();
3658        seen.dedup();
3659        assert_eq!(seen.len(), n - 1, "every node but the root is someone's child");
3660        assert!(!seen.contains(&tree.root), "the root is nobody's child");
3661    }
3662
3663    #[test]
3664    fn children_offsets_stay_inside_the_arena() {
3665        let tree = build_tree(&mixed_dom());
3666        assert_eq!(tree.children_offsets.len(), tree.nodes.len());
3667        let total: usize = tree
3668            .children_offsets
3669            .iter()
3670            .map(|&(_, len)| len as usize)
3671            .sum();
3672        assert_eq!(total, tree.children_arena.len());
3673        for &(start, len) in &tree.children_offsets {
3674            assert!((start as usize) + (len as usize) <= tree.children_arena.len());
3675        }
3676    }
3677
3678    #[test]
3679    fn get_content_size_is_default_for_an_out_of_range_index() {
3680        let tree = build_tree(&mixed_dom());
3681        assert_eq!(tree.get_content_size(usize::MAX), LogicalSize::default());
3682        assert_eq!(tree.get_content_size(tree.nodes.len()), LogicalSize::default());
3683    }
3684
3685    #[test]
3686    fn get_content_size_prefers_the_explicit_overflow_content_size() {
3687        let mut tree = raw_tree(vec![hot(None)], &[vec![]]);
3688        tree.nodes[0].used_size = Some(LogicalSize::new(10.0, 10.0));
3689        tree.warm[0].overflow_content_size = Some(LogicalSize::new(999.0, 888.0));
3690        assert_eq!(tree.get_content_size(0), LogicalSize::new(999.0, 888.0));
3691    }
3692
3693    #[test]
3694    fn get_content_size_grows_the_used_size_to_cover_the_inline_items() {
3695        let mut tree = raw_tree(vec![hot(None)], &[vec![]]);
3696        tree.nodes[0].used_size = Some(LogicalSize::new(10.0, 10.0));
3697        tree.warm[0].inline_layout_result = Some(CachedInlineLayout::new(
3698            layout_of(vec![tab_item(30.0, 40.0, 25.0, 0)]),
3699            AvailableSpace::MaxContent,
3700            false,
3701        ));
3702        // item spans x ∈ [25, 55], y ∈ [0, 40]  →  content must cover 55 × 40.
3703        let cs = tree.get_content_size(0);
3704        assert_eq!(cs.width, 55.0);
3705        assert_eq!(cs.height, 40.0);
3706    }
3707
3708    #[test]
3709    fn get_content_size_never_shrinks_below_the_used_size() {
3710        let mut tree = raw_tree(vec![hot(None)], &[vec![]]);
3711        tree.nodes[0].used_size = Some(LogicalSize::new(500.0, 500.0));
3712        tree.warm[0].inline_layout_result = Some(CachedInlineLayout::new(
3713            layout_of(vec![tab_item(1.0, 1.0, 0.0, 0)]),
3714            AvailableSpace::MaxContent,
3715            false,
3716        ));
3717        assert_eq!(tree.get_content_size(0), LogicalSize::new(500.0, 500.0));
3718    }
3719
3720    #[test]
3721    fn get_content_size_of_a_node_with_no_used_size_is_zero() {
3722        let tree = raw_tree(vec![hot(None)], &[vec![]]);
3723        assert_eq!(tree.get_content_size(0), LogicalSize::default());
3724    }
3725
3726    // ==================================================================
3727    // LayoutTree — IFC navigation
3728    // ==================================================================
3729
3730    #[test]
3731    fn get_ifc_root_layout_index_returns_the_input_unchanged_when_out_of_range() {
3732        let tree = build_tree(&mixed_dom());
3733        // Documented contract: no membership ⇒ identity. That must hold for
3734        // garbage indices too, and it must not panic.
3735        assert_eq!(tree.get_ifc_root_layout_index(usize::MAX), usize::MAX);
3736        assert_eq!(tree.get_ifc_root_layout_index(0), 0);
3737    }
3738
3739    #[test]
3740    fn get_ifc_root_layout_index_follows_membership_only_for_non_ifc_roots() {
3741        let mut tree = raw_tree(vec![hot(None), hot(Some(0))], &[vec![1], vec![]]);
3742        tree.warm[0].inline_layout_result = Some(CachedInlineLayout::new(
3743            empty_layout(),
3744            AvailableSpace::MaxContent,
3745            false,
3746        ));
3747        tree.warm[1].ifc_membership = Some(IfcMembership {
3748            ifc_id: IfcId(0),
3749            ifc_root_layout_index: 0,
3750            run_index: 0,
3751        });
3752
3753        assert_eq!(tree.get_ifc_root_layout_index(1), 0, "a text node anchors to its IFC root");
3754        assert_eq!(
3755            tree.get_ifc_root_layout_index(0),
3756            0,
3757            "the IFC root itself is its own anchor"
3758        );
3759
3760        // A node that owns an inline_layout_result must NOT be redirected, even
3761        // if it also carries a (stale) membership.
3762        tree.warm[0].ifc_membership = Some(IfcMembership {
3763            ifc_id: IfcId(9),
3764            ifc_root_layout_index: 1,
3765            run_index: 0,
3766        });
3767        assert_eq!(tree.get_ifc_root_layout_index(0), 0);
3768    }
3769
3770    #[test]
3771    fn get_inline_layout_for_node_walks_membership_then_gives_up_cleanly() {
3772        let mut tree = raw_tree(vec![hot(None), hot(Some(0)), hot(Some(0))], &[vec![1, 2]]);
3773        let arc = empty_layout();
3774        tree.warm[0].inline_layout_result = Some(CachedInlineLayout::new(
3775            Arc::clone(&arc),
3776            AvailableSpace::MaxContent,
3777            false,
3778        ));
3779        tree.warm[1].ifc_membership = Some(IfcMembership {
3780            ifc_id: IfcId(0),
3781            ifc_root_layout_index: 0,
3782            run_index: 0,
3783        });
3784        // Node 2 has neither its own layout nor a membership.
3785        assert!(Arc::ptr_eq(tree.get_inline_layout_for_node(0).unwrap(), &arc));
3786        assert!(Arc::ptr_eq(tree.get_inline_layout_for_node(1).unwrap(), &arc));
3787        assert!(tree.get_inline_layout_for_node(2).is_none());
3788    }
3789
3790    #[test]
3791    fn get_inline_layout_for_node_is_none_when_membership_dangles() {
3792        // A membership pointing at a bogus root index must return None, not panic.
3793        let mut tree = raw_tree(vec![hot(None)], &[vec![]]);
3794        tree.warm[0].ifc_membership = Some(IfcMembership {
3795            ifc_id: IfcId(3),
3796            ifc_root_layout_index: usize::MAX,
3797            run_index: 0,
3798        });
3799        assert!(tree.get_inline_layout_for_node(0).is_none());
3800
3801        // …and when the referenced root exists but has no cached layout.
3802        let mut tree = raw_tree(vec![hot(None), hot(Some(0))], &[vec![1], vec![]]);
3803        tree.warm[1].ifc_membership = Some(IfcMembership {
3804            ifc_id: IfcId(3),
3805            ifc_root_layout_index: 0,
3806            run_index: 0,
3807        });
3808        assert!(tree.get_inline_layout_for_node(1).is_none());
3809    }
3810
3811    // ==================================================================
3812    // LayoutTree — dirty flags
3813    // ==================================================================
3814
3815    /// `0 → 1 → 2` chain plus a sibling `3` under `1`.
3816    fn dirty_tree() -> LayoutTree {
3817        raw_tree(
3818            vec![hot(None), hot(Some(0)), hot(Some(1)), hot(Some(1))],
3819            &[vec![1], vec![2, 3], vec![], vec![]],
3820        )
3821    }
3822
3823    #[test]
3824    fn mark_dirty_walks_up_to_the_root() {
3825        let mut tree = dirty_tree();
3826        tree.mark_dirty(2, DirtyFlag::Layout);
3827        assert_eq!(tree.cold(2).unwrap().dirty_flag, DirtyFlag::Layout);
3828        assert_eq!(tree.cold(1).unwrap().dirty_flag, DirtyFlag::Layout);
3829        assert_eq!(tree.cold(0).unwrap().dirty_flag, DirtyFlag::Layout);
3830        assert_eq!(
3831            tree.cold(3).unwrap().dirty_flag,
3832            DirtyFlag::None,
3833            "the sibling is untouched"
3834        );
3835    }
3836
3837    #[test]
3838    fn mark_dirty_with_flag_none_is_a_no_op() {
3839        let mut tree = dirty_tree();
3840        tree.mark_dirty(2, DirtyFlag::None);
3841        assert!(tree.cold.iter().all(|c| c.dirty_flag == DirtyFlag::None));
3842    }
3843
3844    #[test]
3845    fn mark_dirty_never_downgrades_an_existing_flag() {
3846        let mut tree = dirty_tree();
3847        tree.mark_dirty(2, DirtyFlag::Layout);
3848        tree.mark_dirty(2, DirtyFlag::Paint);
3849        assert_eq!(
3850            tree.cold(2).unwrap().dirty_flag,
3851            DirtyFlag::Layout,
3852            "Layout > Paint — a Paint request must not weaken it"
3853        );
3854    }
3855
3856    #[test]
3857    fn mark_dirty_upgrades_paint_to_layout_and_keeps_propagating() {
3858        let mut tree = dirty_tree();
3859        tree.mark_dirty(2, DirtyFlag::Paint);
3860        assert_eq!(tree.cold(0).unwrap().dirty_flag, DirtyFlag::Paint);
3861        tree.mark_dirty(2, DirtyFlag::Layout);
3862        assert_eq!(tree.cold(2).unwrap().dirty_flag, DirtyFlag::Layout);
3863        assert_eq!(tree.cold(0).unwrap().dirty_flag, DirtyFlag::Layout);
3864    }
3865
3866    #[test]
3867    fn mark_dirty_stops_early_when_an_ancestor_is_already_at_least_as_dirty() {
3868        let mut tree = dirty_tree();
3869        tree.mark_dirty(3, DirtyFlag::Layout); // marks 3, 1, 0
3870        tree.mark_dirty(2, DirtyFlag::Layout); // marks 2, then stops at 1
3871        assert_eq!(tree.cold(2).unwrap().dirty_flag, DirtyFlag::Layout);
3872        assert_eq!(tree.cold(1).unwrap().dirty_flag, DirtyFlag::Layout);
3873    }
3874
3875    #[test]
3876    fn mark_dirty_out_of_range_is_a_silent_no_op() {
3877        let mut tree = dirty_tree();
3878        tree.mark_dirty(usize::MAX, DirtyFlag::Layout);
3879        tree.mark_dirty(tree.nodes.len(), DirtyFlag::Layout);
3880        assert!(tree.cold.iter().all(|c| c.dirty_flag == DirtyFlag::None));
3881    }
3882
3883    #[test]
3884    fn mark_dirty_terminates_on_a_cyclic_parent_chain() {
3885        // Not reachable through the builder, but the `>= flag` early-out is the
3886        // only thing standing between a corrupted parent pointer and a hang.
3887        let mut tree = raw_tree(vec![hot(Some(1)), hot(Some(0))], &[vec![], vec![]]);
3888        tree.mark_dirty(0, DirtyFlag::Layout);
3889        assert_eq!(tree.cold(0).unwrap().dirty_flag, DirtyFlag::Layout);
3890        assert_eq!(tree.cold(1).unwrap().dirty_flag, DirtyFlag::Layout);
3891    }
3892
3893    #[test]
3894    fn mark_dirty_terminates_when_a_node_is_its_own_parent() {
3895        let mut tree = raw_tree(vec![hot(Some(0))], &[vec![]]);
3896        tree.mark_dirty(0, DirtyFlag::Layout);
3897        assert_eq!(tree.cold(0).unwrap().dirty_flag, DirtyFlag::Layout);
3898    }
3899
3900    #[test]
3901    fn mark_subtree_dirty_marks_descendants_but_not_ancestors_or_siblings() {
3902        let mut tree = dirty_tree();
3903        tree.mark_subtree_dirty(1, DirtyFlag::Layout);
3904        assert_eq!(tree.cold(1).unwrap().dirty_flag, DirtyFlag::Layout);
3905        assert_eq!(tree.cold(2).unwrap().dirty_flag, DirtyFlag::Layout);
3906        assert_eq!(tree.cold(3).unwrap().dirty_flag, DirtyFlag::Layout);
3907        assert_eq!(
3908            tree.cold(0).unwrap().dirty_flag,
3909            DirtyFlag::None,
3910            "mark_subtree_dirty walks DOWN only"
3911        );
3912    }
3913
3914    #[test]
3915    fn mark_subtree_dirty_with_none_or_a_bad_index_is_a_no_op() {
3916        let mut tree = dirty_tree();
3917        tree.mark_subtree_dirty(0, DirtyFlag::None);
3918        tree.mark_subtree_dirty(usize::MAX, DirtyFlag::Layout);
3919        assert!(tree.cold.iter().all(|c| c.dirty_flag == DirtyFlag::None));
3920    }
3921
3922    #[test]
3923    fn mark_subtree_dirty_does_not_downgrade() {
3924        let mut tree = dirty_tree();
3925        tree.mark_subtree_dirty(0, DirtyFlag::Layout);
3926        tree.mark_subtree_dirty(0, DirtyFlag::Paint);
3927        assert!(tree.cold.iter().all(|c| c.dirty_flag == DirtyFlag::Layout));
3928    }
3929
3930    #[test]
3931    fn clear_all_dirty_flags_resets_every_node() {
3932        let mut tree = dirty_tree();
3933        tree.mark_subtree_dirty(0, DirtyFlag::Layout);
3934        assert!(tree.cold.iter().any(|c| c.dirty_flag != DirtyFlag::None));
3935        tree.clear_all_dirty_flags();
3936        assert!(tree.cold.iter().all(|c| c.dirty_flag == DirtyFlag::None));
3937    }
3938
3939    #[test]
3940    fn clear_all_dirty_flags_on_an_empty_tree_does_not_panic() {
3941        let mut tree = raw_tree(Vec::new(), &[]);
3942        tree.clear_all_dirty_flags();
3943        assert!(tree.cold.is_empty());
3944    }
3945
3946    // ==================================================================
3947    // LayoutTree — memory report (getters / numeric)
3948    // ==================================================================
3949
3950    #[test]
3951    fn memory_report_total_is_the_sum_of_its_parts() {
3952        let tree = build_tree(&mixed_dom());
3953        let r = tree.memory_report();
3954        assert_eq!(r.node_count, tree.nodes.len());
3955        assert_eq!(
3956            r.total_bytes(),
3957            r.hot_bytes
3958                + r.warm_bytes
3959                + r.warm_inline_layout_bytes
3960                + r.warm_taffy_cache_bytes
3961                + r.cold_bytes
3962                + r.dom_to_layout_bytes
3963                + r.children_arena_bytes
3964                + r.children_offsets_bytes
3965        );
3966        assert!(r.hot_bytes >= r.node_count * size_of::<LayoutNodeHot>());
3967        assert!(r.total_bytes() > 0, "a non-empty tree retains something");
3968    }
3969
3970    #[test]
3971    fn memory_report_of_an_empty_tree_is_all_zero() {
3972        let tree = raw_tree(Vec::new(), &[]);
3973        let r = tree.memory_report();
3974        assert_eq!(r.node_count, 0);
3975        assert_eq!(r.total_bytes(), 0);
3976    }
3977
3978    #[test]
3979    fn memory_report_total_bytes_default_is_zero() {
3980        assert_eq!(LayoutTreeMemoryReport::default().total_bytes(), 0);
3981    }
3982
3983    #[test]
3984    fn memory_report_total_bytes_at_the_usize_boundary_does_not_overflow() {
3985        // Eight fields, each usize::MAX / 8 → exactly usize::MAX - 7. One notch
3986        // further and `total_bytes`'s plain `+` chain would overflow-panic in debug.
3987        let eighth = usize::MAX / 8;
3988        let r = LayoutTreeMemoryReport {
3989            node_count: 0,
3990            hot_bytes: eighth,
3991            warm_bytes: eighth,
3992            warm_inline_layout_bytes: eighth,
3993            warm_taffy_cache_bytes: eighth,
3994            cold_bytes: eighth,
3995            dom_to_layout_bytes: eighth,
3996            children_arena_bytes: eighth,
3997            children_offsets_bytes: eighth,
3998        };
3999        assert_eq!(r.total_bytes(), eighth * 8);
4000        assert_eq!(r.total_bytes(), usize::MAX - 7);
4001    }
4002
4003    #[test]
4004    fn memory_report_counts_a_cached_inline_layout() {
4005        let mut tree = raw_tree(vec![hot(None)], &[vec![]]);
4006        let bare = tree.memory_report().warm_inline_layout_bytes;
4007        assert_eq!(bare, 0);
4008
4009        tree.warm[0].inline_layout_result = Some(CachedInlineLayout::new(
4010            layout_of(vec![tab_item(1.0, 1.0, 0.0, 0)]),
4011            AvailableSpace::MaxContent,
4012            false,
4013        ));
4014        assert!(
4015            tree.memory_report().warm_inline_layout_bytes >= size_of::<UnifiedLayout>(),
4016            "the UnifiedLayout header must at least be counted"
4017        );
4018    }
4019
4020    #[test]
4021    fn root_node_returns_the_hot_node_at_the_root_index() {
4022        let sd = mixed_dom();
4023        let tree = build_tree(&sd);
4024        let root = tree.root_node();
4025        assert_eq!(root.parent, None);
4026        assert_eq!(root.dom_node_id, sd.root.into_crate_internal());
4027    }
4028
4029    // ==================================================================
4030    // LayoutTree::resolve_box_props (numeric / NaN-inf)
4031    // ==================================================================
4032
4033    #[test]
4034    fn resolve_box_props_out_of_range_is_a_no_op() {
4035        let mut tree = build_tree(&mixed_dom());
4036        tree.resolve_box_props(usize::MAX, VIEWPORT, VIEWPORT, 16.0, 16.0);
4037        tree.resolve_box_props(tree.nodes.len(), VIEWPORT, VIEWPORT, 16.0, 16.0);
4038    }
4039
4040    #[test]
4041    fn resolve_box_props_keeps_the_stored_props_finite_for_nan_and_inf_inputs() {
4042        let sd = styled(
4043            Dom::create_body().with_child(div_class("m")),
4044            ".m { margin: 50%; padding: 10em; border: 1px solid black; }",
4045        );
4046        let mut tree = build_tree(&sd);
4047
4048        for (cb, vp, efs, rfs) in [
4049            (
4050                LogicalSize::new(f32::NAN, f32::NAN),
4051                LogicalSize::new(f32::NAN, f32::NAN),
4052                f32::NAN,
4053                f32::NAN,
4054            ),
4055            (
4056                LogicalSize::new(f32::INFINITY, f32::INFINITY),
4057                LogicalSize::new(f32::INFINITY, f32::INFINITY),
4058                f32::INFINITY,
4059                f32::INFINITY,
4060            ),
4061            (
4062                LogicalSize::new(f32::NEG_INFINITY, 0.0),
4063                LogicalSize::new(0.0, f32::NEG_INFINITY),
4064                f32::NEG_INFINITY,
4065                0.0,
4066            ),
4067            (
4068                LogicalSize::new(f32::MAX, f32::MAX),
4069                LogicalSize::new(f32::MAX, f32::MAX),
4070                f32::MAX,
4071                f32::MAX,
4072            ),
4073            (
4074                LogicalSize::new(0.0, 0.0),
4075                LogicalSize::new(0.0, 0.0),
4076                0.0,
4077                0.0,
4078            ),
4079        ] {
4080            tree.resolve_box_props(1, cb, vp, efs, rfs);
4081            let bp = tree.get(1).unwrap().box_props.unpack();
4082            for v in [
4083                bp.margin.top,
4084                bp.margin.right,
4085                bp.margin.bottom,
4086                bp.margin.left,
4087                bp.padding.top,
4088                bp.padding.left,
4089                bp.border.top,
4090                bp.border.left,
4091            ] {
4092                assert!(
4093                    v.is_finite(),
4094                    "the i16×10 packing must launder NaN/inf into a finite value, got {v}"
4095                );
4096                assert!(
4097                    (-3277.0..=3277.0).contains(&v),
4098                    "packed edges are clamped to ±3276.8px, got {v}"
4099                );
4100            }
4101        }
4102    }
4103
4104    #[test]
4105    fn resolve_box_props_resolves_percentages_against_the_containing_block() {
4106        let sd = styled(
4107            Dom::create_body().with_child(div_class("m")),
4108            ".m { margin-left: 50%; }",
4109        );
4110        let mut tree = build_tree(&sd);
4111        tree.resolve_box_props(1, LogicalSize::new(200.0, 100.0), VIEWPORT, 16.0, 16.0);
4112        let bp = tree.get(1).unwrap().box_props.unpack();
4113        assert!(
4114            (bp.margin.left - 100.0).abs() < 0.2,
4115            "50% of a 200px containing block ≈ 100px, got {}",
4116            bp.margin.left
4117        );
4118    }
4119
4120    // ==================================================================
4121    // Anonymous box generation via the real builder
4122    // ==================================================================
4123
4124    #[test]
4125    fn a_whitespace_only_inline_run_generates_no_anonymous_box() {
4126        let sd = mixed_dom();
4127        let tree = build_tree(&sd);
4128        let ws = text_node(&sd, " \n\t");
4129        assert!(
4130            !tree.dom_to_layout.contains_key(&ws),
4131            "CSS 2.1 §9.2.2.1: collapsible whitespace generates no box"
4132        );
4133        assert!(
4134            tree.nodes.iter().all(|n| n.dom_node_id != Some(ws)),
4135            "…and no layout node references it"
4136        );
4137    }
4138
4139    #[test]
4140    fn a_real_inline_run_next_to_a_block_sibling_gets_exactly_one_anonymous_wrapper() {
4141        let sd = mixed_dom();
4142        let tree = build_tree(&sd);
4143        let wrappers: Vec<usize> = (0..tree.nodes.len())
4144            .filter(|&i| {
4145                tree.cold(i).unwrap().anonymous_type == Some(AnonymousBoxType::InlineWrapper)
4146            })
4147            .collect();
4148        assert_eq!(
4149            wrappers.len(),
4150            1,
4151            "only the trailing `tail` run needs wrapping"
4152        );
4153
4154        let w = wrappers[0];
4155        assert_eq!(tree.get(w).unwrap().dom_node_id, None, "anon boxes have no DOM node");
4156        assert_eq!(tree.cold(w).unwrap().dirty_flag, DirtyFlag::Layout);
4157        let tail = text_node(&sd, "tail");
4158        let kids = tree.children(w);
4159        assert_eq!(kids.len(), 1);
4160        assert_eq!(tree.get(kids[0]).unwrap().dom_node_id, Some(tail));
4161    }
4162
4163    #[test]
4164    fn an_all_inline_block_container_gets_no_anonymous_wrapper() {
4165        let sd = mixed_dom();
4166        let tree = build_tree(&sd);
4167        // `.block` (DOM 1) holds only inline children — the all-inline fast path
4168        // must hand them straight to the parent, with no wrapper in between.
4169        let block_idx = (0..tree.nodes.len())
4170            .find(|&i| tree.get(i).unwrap().dom_node_id == Some(NodeId::new(1)))
4171            .expect("the .block layout node");
4172        let kids = tree.children(block_idx);
4173        assert_eq!(kids.len(), 2, "the text run and the inline div, unwrapped");
4174        assert!(
4175            kids.iter()
4176                .all(|&c| tree.cold(c).unwrap().anonymous_type.is_none()),
4177            "an all-inline block container needs no anonymous wrapper"
4178        );
4179        assert_eq!(
4180            tree.get(block_idx).unwrap().formatting_context,
4181            FormattingContext::Inline,
4182            "it establishes an IFC instead"
4183        );
4184    }
4185
4186    #[test]
4187    fn the_marker_pseudo_element_is_inserted_as_the_first_child_of_a_list_item() {
4188        let sd = styled(
4189            Dom::create_body().with_child(div_class("li").with_child(Dom::create_text("item"))),
4190            ".li { display: list-item; }",
4191        );
4192        let tree = build_tree(&sd);
4193
4194        let marker = (0..tree.nodes.len())
4195            .find(|&i| tree.warm(i).unwrap().pseudo_element == Some(PseudoElement::Marker))
4196            .expect("display:list-item must generate a ::marker");
4197        let li = tree.get(marker).unwrap().parent.expect("marker has a parent");
4198        assert_eq!(
4199            tree.children(li)[0],
4200            marker,
4201            "CSS Lists 3 §3.1: ::marker is the FIRST child"
4202        );
4203        assert_eq!(
4204            tree.get(marker).unwrap().dom_node_id,
4205            tree.get(li).unwrap().dom_node_id,
4206            "the marker shares the list-item's DOM node for counter/style resolution"
4207        );
4208        assert_eq!(tree.get(marker).unwrap().formatting_context, FormattingContext::Inline);
4209        assert!(
4210            tree.dom_to_layout[&tree.get(li).unwrap().dom_node_id.unwrap()].contains(&marker),
4211            "the marker is registered in dom_to_layout for counter resolution"
4212        );
4213    }
4214
4215    #[test]
4216    fn display_none_children_never_reach_the_layout_tree() {
4217        let sd = styled(
4218            Dom::create_body()
4219                .with_child(div_class("gone"))
4220                .with_child(div_class("here")),
4221            ".gone { display: none; } .here { display: block; }",
4222        );
4223        let tree = build_tree(&sd);
4224        assert_eq!(
4225            tree.children(tree.root).len(),
4226            1,
4227            "display:none generates no box"
4228        );
4229    }
4230
4231    #[test]
4232    fn display_contents_promotes_its_children_to_the_grandparent() {
4233        let sd = styled(
4234            Dom::create_body().with_child(div_class("c").with_child(div_class("kid"))),
4235            ".c { display: contents; } .kid { display: block; }",
4236        );
4237        let tree = build_tree(&sd);
4238        // The `.c` box is removed from its parent's child list; `.kid` is hoisted.
4239        let root_kids = tree.children(tree.root);
4240        assert!(
4241            root_kids
4242                .iter()
4243                .any(|&i| tree.warm(i).unwrap().computed_style.display == LayoutDisplay::Block),
4244            "the promoted child must be a direct child of the root"
4245        );
4246    }
4247
4248    #[test]
4249    fn a_table_with_a_bare_cell_gets_an_anonymous_row() {
4250        let sd = styled(
4251            Dom::create_body().with_child(div_class("t").with_child(div_class("cell"))),
4252            ".t { display: table; } .cell { display: table-cell; }",
4253        );
4254        let tree = build_tree(&sd);
4255        assert!(
4256            (0..tree.nodes.len()).any(|i| {
4257                tree.cold(i).unwrap().anonymous_type == Some(AnonymousBoxType::TableRow)
4258            }),
4259            "CSS 2.2 §17.2.1 stage 2: a non-proper table child is wrapped in an anonymous row"
4260        );
4261    }
4262
4263    #[test]
4264    fn whitespace_between_table_rows_is_dropped_not_wrapped() {
4265        let sd = styled(
4266            Dom::create_body().with_child(
4267                div_class("t")
4268                    .with_child(Dom::create_text("   "))
4269                    .with_child(div_class("row")),
4270            ),
4271            ".t { display: table; } .row { display: table-row; }",
4272        );
4273        let tree = build_tree(&sd);
4274        let ws = text_node(&sd, "   ");
4275        assert!(
4276            !tree.dom_to_layout.contains_key(&ws),
4277            "stage 1: irrelevant (whitespace) boxes are removed"
4278        );
4279        assert!(
4280            !(0..tree.nodes.len())
4281                .any(|i| tree.cold(i).unwrap().anonymous_type == Some(AnonymousBoxType::TableRow)),
4282            "and no anonymous row is generated for it"
4283        );
4284    }
4285
4286    #[test]
4287    fn table_column_children_are_suppressed_entirely() {
4288        let sd = styled(
4289            Dom::create_body().with_child(div_class("col").with_child(div_class("kid"))),
4290            ".col { display: table-column; } .kid { display: block; }",
4291        );
4292        let tree = build_tree(&sd);
4293        // CSS 2.2 §17.2.1: all children of a table-column are display:none.
4294        let col = tree.children(tree.root)[0];
4295        assert!(tree.children(col).is_empty());
4296    }
4297
4298    // ==================================================================
4299    // LayoutTreeBuilder (constructor / numeric / boundary)
4300    // ==================================================================
4301
4302    #[test]
4303    fn builder_new_starts_completely_empty() {
4304        let b = LayoutTreeBuilder::new(VIEWPORT);
4305        assert!(b.get(0).is_none());
4306        assert!(b.get(usize::MAX).is_none());
4307        assert!(b.nodes.is_empty());
4308        assert!(b.dom_to_layout.is_empty());
4309        assert_eq!(b.viewport_size, VIEWPORT);
4310    }
4311
4312    #[test]
4313    fn builder_new_accepts_degenerate_viewports() {
4314        for vp in [
4315            LogicalSize::new(0.0, 0.0),
4316            LogicalSize::new(-1.0, -1.0),
4317            LogicalSize::new(f32::MAX, f32::MAX),
4318            LogicalSize::new(f32::NAN, f32::INFINITY),
4319        ] {
4320            let b = LayoutTreeBuilder::new(vp);
4321            assert!(b.nodes.is_empty());
4322        }
4323    }
4324
4325    #[test]
4326    fn builder_get_and_get_mut_are_none_out_of_range() {
4327        let sd = mixed_dom();
4328        let mut b = LayoutTreeBuilder::new(VIEWPORT);
4329        let mut msgs = None;
4330        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4331        assert_eq!(root, 0);
4332        assert!(b.get(0).is_some());
4333        assert!(b.get_mut(0).is_some());
4334        for idx in [1, usize::MAX, usize::MAX - 1] {
4335            assert!(b.get(idx).is_none(), "get({idx})");
4336            assert!(b.get_mut(idx).is_none(), "get_mut({idx})");
4337        }
4338    }
4339
4340    #[test]
4341    fn create_anonymous_node_wires_up_parent_children_and_cold_defaults() {
4342        let sd = mixed_dom();
4343        let mut b = LayoutTreeBuilder::new(VIEWPORT);
4344        let mut msgs = None;
4345        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4346        let root_fc = b.get(root).unwrap().formatting_context;
4347
4348        let anon = b.create_anonymous_node(root, AnonymousBoxType::TableCell, FormattingContext::TableCell);
4349        assert_eq!(anon, 1, "anon nodes are appended");
4350
4351        let n = b.get(anon).unwrap();
4352        assert_eq!(n.dom_node_id, None, "anonymous ⇒ no DOM node");
4353        assert_eq!(n.anonymous_type, Some(AnonymousBoxType::TableCell));
4354        assert_eq!(n.formatting_context, FormattingContext::TableCell);
4355        assert_eq!(n.parent, Some(root));
4356        assert_eq!(n.parent_formatting_context, Some(root_fc));
4357        assert_eq!(n.dirty_flag, DirtyFlag::Layout, "a fresh box needs layout");
4358        assert!(n.children.is_empty());
4359        assert_eq!(n.subtree_hash, SubtreeHash(0));
4360        assert!(n.ifc_id.is_none());
4361        assert_eq!(b.get(root).unwrap().children, vec![anon]);
4362        assert!(
4363            b.dom_to_layout.values().all(|v| !v.contains(&anon)),
4364            "anon boxes are never registered in dom_to_layout"
4365        );
4366    }
4367
4368    #[test]
4369    fn create_anonymous_node_appends_in_call_order() {
4370        let sd = mixed_dom();
4371        let mut b = LayoutTreeBuilder::new(VIEWPORT);
4372        let mut msgs = None;
4373        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4374        let a = b.create_anonymous_node(root, AnonymousBoxType::TableRow, FormattingContext::TableRow);
4375        let c = b.create_anonymous_node(root, AnonymousBoxType::TableCell, FormattingContext::TableCell);
4376        assert_eq!((a, c), (1, 2));
4377        assert_eq!(b.get(root).unwrap().children, vec![a, c]);
4378    }
4379
4380    #[test]
4381    fn create_node_from_dom_registers_the_dom_mapping_and_the_parent_link() {
4382        let sd = mixed_dom();
4383        let mut b = LayoutTreeBuilder::new(VIEWPORT);
4384        let mut msgs = None;
4385        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4386        let child = b.create_node_from_dom(&sd, NodeId::new(1), Some(root), &mut msgs);
4387
4388        assert_eq!(b.get(child).unwrap().dom_node_id, Some(NodeId::new(1)));
4389        assert_eq!(b.get(child).unwrap().parent, Some(root));
4390        assert_eq!(b.get(root).unwrap().children, vec![child]);
4391        assert_eq!(b.dom_to_layout[&NodeId::new(1)], vec![child]);
4392        assert_eq!(b.get(child).unwrap().dirty_flag, DirtyFlag::Layout);
4393    }
4394
4395    #[test]
4396    fn create_node_from_dom_turns_the_roots_visible_overflow_into_auto() {
4397        // CSS Overflow 3 §3.3 — only for the root (parent == None).
4398        let sd = styled(Dom::create_body().with_child(div_class("d")), ".d { display: block; }");
4399        let mut b = LayoutTreeBuilder::new(VIEWPORT);
4400        let mut msgs = None;
4401        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4402        let child = b.create_node_from_dom(&sd, NodeId::new(1), Some(root), &mut msgs);
4403
4404        let root_style = &b.get(root).unwrap().computed_style;
4405        assert_ne!(root_style.overflow_x, LayoutOverflow::Visible);
4406        assert_ne!(root_style.overflow_y, LayoutOverflow::Visible);
4407
4408        let child_style = &b.get(child).unwrap().computed_style;
4409        assert_eq!(
4410            child_style.overflow_x,
4411            LayoutOverflow::Visible,
4412            "the rule applies to the viewport only, not to every node"
4413        );
4414    }
4415
4416    #[test]
4417    fn clone_node_from_old_resets_children_and_dirty_state() {
4418        let sd = mixed_dom();
4419        let tree = build_tree(&sd);
4420        let old_root = tree.get_full_node(0).unwrap();
4421        let old_child = tree.get_full_node(1).unwrap();
4422        assert!(!old_root.children.is_empty(), "the source root has children");
4423
4424        let mut b = LayoutTreeBuilder::new(VIEWPORT);
4425        let root = b.clone_node_from_old(&old_root, None);
4426        let child = b.clone_node_from_old(&old_child, Some(root));
4427        assert_eq!((root, child), (0, 1));
4428
4429        assert!(
4430            b.get(root).unwrap().children == vec![child],
4431            "the clone's children come only from later clone calls"
4432        );
4433        assert!(b.get(child).unwrap().children.is_empty());
4434        assert_eq!(b.get(child).unwrap().parent, Some(root));
4435        assert_eq!(b.get(child).unwrap().dirty_flag, DirtyFlag::None);
4436        let root_fc = b.get(root).unwrap().formatting_context;
4437        assert_eq!(b.get(child).unwrap().parent_formatting_context, Some(root_fc));
4438    }
4439
4440    #[test]
4441    fn clone_node_from_old_skips_dom_registration_for_anonymous_nodes() {
4442        let sd = mixed_dom();
4443        let tree = build_tree(&sd);
4444        let anon = (0..tree.nodes.len())
4445            .find(|&i| tree.cold(i).unwrap().anonymous_type.is_some())
4446            .expect("mixed_dom generates one anonymous wrapper");
4447        let old = tree.get_full_node(anon).unwrap();
4448        assert_eq!(old.dom_node_id, None);
4449
4450        let mut b = LayoutTreeBuilder::new(VIEWPORT);
4451        let idx = b.clone_node_from_old(&old, None);
4452        assert_eq!(idx, 0);
4453        assert!(
4454            b.dom_to_layout.is_empty(),
4455            "a node with no dom_node_id must not create a mapping entry"
4456        );
4457    }
4458
4459    #[test]
4460    fn build_flattens_children_into_the_arena_losslessly() {
4461        let sd = mixed_dom();
4462        let mut builder = LayoutTreeBuilder::new(VIEWPORT);
4463        let mut msgs = None;
4464        let root_id = sd.root.into_crate_internal().unwrap_or(NodeId::ZERO);
4465        let root = builder.process_node(&sd, root_id, None, &mut msgs).unwrap();
4466        let expected: Vec<Vec<usize>> = builder.nodes.iter().map(|n| n.children.clone()).collect();
4467
4468        let tree = builder.build(root);
4469        assert_eq!(tree.nodes.len(), expected.len());
4470        assert_eq!(tree.warm.len(), expected.len());
4471        assert_eq!(tree.cold.len(), expected.len());
4472        for (i, want) in expected.iter().enumerate() {
4473            assert_eq!(tree.children(i), want.as_slice(), "node {i}");
4474        }
4475    }
4476
4477    #[test]
4478    fn build_on_an_empty_builder_yields_an_empty_tree() {
4479        let tree = LayoutTreeBuilder::new(VIEWPORT).build(0);
4480        assert!(tree.nodes.is_empty());
4481        assert!(tree.children_arena.is_empty());
4482        assert!(tree.children_offsets.is_empty());
4483        assert!(tree.subtree_needs_intrinsic.is_empty());
4484        assert!(tree.get(0).is_none());
4485        assert!(tree.children(0).is_empty());
4486        assert_eq!(tree.get_content_size(0), LogicalSize::default());
4487        assert_eq!(tree.memory_report().node_count, 0);
4488    }
4489
4490    #[test]
4491    fn build_with_an_out_of_range_root_index_does_not_panic() {
4492        let sd = mixed_dom();
4493        let mut builder = LayoutTreeBuilder::new(VIEWPORT);
4494        let mut msgs = None;
4495        builder.process_node(&sd, NodeId::ZERO, None, &mut msgs).unwrap();
4496
4497        let tree = builder.build(usize::MAX);
4498        assert_eq!(tree.root, usize::MAX, "build() stores the index verbatim");
4499        assert!(tree.get(tree.root).is_none());
4500        assert!(tree.children(tree.root).is_empty());
4501        assert_eq!(tree.get_ifc_root_layout_index(tree.root), usize::MAX);
4502    }
4503
4504    #[test]
4505    fn blockify_node_display_blockifies_an_inline_flex_item() {
4506        let sd = styled(
4507            Dom::create_body().with_child(div_class("f").with_child(div_class("i"))),
4508            ".f { display: flex; } .i { display: inline; }",
4509        );
4510        let mut b = LayoutTreeBuilder::new(VIEWPORT);
4511        let mut msgs = None;
4512        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4513        let flex = b.create_node_from_dom(&sd, NodeId::new(1), Some(root), &mut msgs);
4514        let item = b.create_node_from_dom(&sd, NodeId::new(2), Some(flex), &mut msgs);
4515
4516        assert_eq!(b.get(flex).unwrap().formatting_context, FormattingContext::Flex);
4517        assert_eq!(b.get(item).unwrap().computed_style.display, LayoutDisplay::Inline);
4518
4519        b.blockify_node_display(&sd, NodeId::new(2), item, Some(flex));
4520
4521        assert_eq!(
4522            b.get(item).unwrap().computed_style.display,
4523            LayoutDisplay::Block,
4524            "CSS Display 3 §2.7: a flex item's inline display blockifies"
4525        );
4526        assert!(matches!(
4527            b.get(item).unwrap().formatting_context,
4528            FormattingContext::Block { .. }
4529        ));
4530    }
4531
4532    #[test]
4533    fn blockify_node_display_leaves_a_plain_block_child_alone() {
4534        let sd = styled(
4535            Dom::create_body().with_child(div_class("p").with_child(div_class("b"))),
4536            ".p { display: block; } .b { display: block; }",
4537        );
4538        let mut b = LayoutTreeBuilder::new(VIEWPORT);
4539        let mut msgs = None;
4540        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4541        let p = b.create_node_from_dom(&sd, NodeId::new(1), Some(root), &mut msgs);
4542        let child = b.create_node_from_dom(&sd, NodeId::new(2), Some(p), &mut msgs);
4543        let before = b.get(child).unwrap().formatting_context;
4544
4545        b.blockify_node_display(&sd, NodeId::new(2), child, Some(p));
4546        assert_eq!(b.get(child).unwrap().computed_style.display, LayoutDisplay::Block);
4547        assert_eq!(b.get(child).unwrap().formatting_context, before);
4548    }
4549
4550    #[test]
4551    fn blockify_node_display_with_a_bogus_node_index_is_a_no_op() {
4552        let sd = mixed_dom();
4553        let mut b = LayoutTreeBuilder::new(VIEWPORT);
4554        let mut msgs = None;
4555        let root = b.create_node_from_dom(&sd, NodeId::ZERO, None, &mut msgs);
4556        // A valid DOM id with a garbage layout index must not panic: both the
4557        // read and the write go through `get`/`get_mut`.
4558        b.blockify_node_display(&sd, NodeId::ZERO, usize::MAX, None);
4559        b.blockify_node_display(&sd, NodeId::ZERO, 999, Some(usize::MAX));
4560        assert_eq!(b.nodes.len(), 1);
4561        assert!(b.get(root).is_some());
4562    }
4563
4564    // ==================================================================
4565    // blockify_flex_item_if_table_internal (numeric / slice bounds)
4566    // ==================================================================
4567
4568    #[test]
4569    fn blockify_flex_item_rewrites_every_table_internal_context() {
4570        let tree = build_tree(&mixed_dom());
4571        let mut nodes = vec![tree.get_full_node(0).unwrap()];
4572
4573        for fc in [
4574            FormattingContext::TableCell,
4575            FormattingContext::TableRow,
4576            FormattingContext::TableRowGroup,
4577            FormattingContext::TableColumnGroup,
4578            FormattingContext::TableCaption,
4579            FormattingContext::Table,
4580        ] {
4581            nodes[0].formatting_context = fc;
4582            blockify_flex_item_if_table_internal(&mut nodes, 0);
4583            assert_eq!(
4584                nodes[0].formatting_context,
4585                FormattingContext::Block {
4586                    establishes_new_context: true
4587                },
4588                "{fc:?} is table-internal and must blockify"
4589            );
4590        }
4591    }
4592
4593    #[test]
4594    fn blockify_flex_item_leaves_non_table_contexts_untouched() {
4595        let tree = build_tree(&mixed_dom());
4596        let mut nodes = vec![tree.get_full_node(0).unwrap()];
4597
4598        for fc in [
4599            FormattingContext::Inline,
4600            FormattingContext::InlineBlock,
4601            FormattingContext::Flex,
4602            FormattingContext::Grid,
4603            FormattingContext::None,
4604            FormattingContext::Contents,
4605            FormattingContext::Block {
4606                establishes_new_context: false,
4607            },
4608        ] {
4609            nodes[0].formatting_context = fc;
4610            blockify_flex_item_if_table_internal(&mut nodes, 0);
4611            assert_eq!(nodes[0].formatting_context, fc, "{fc:?} must be left alone");
4612        }
4613    }
4614
4615    #[test]
4616    fn blockify_flex_item_out_of_range_or_empty_is_a_no_op() {
4617        let tree = build_tree(&mixed_dom());
4618        let mut nodes = vec![tree.get_full_node(0).unwrap()];
4619        nodes[0].formatting_context = FormattingContext::TableCell;
4620
4621        blockify_flex_item_if_table_internal(&mut nodes, 1);
4622        blockify_flex_item_if_table_internal(&mut nodes, usize::MAX);
4623        blockify_flex_item_if_table_internal(&mut [], 0);
4624        blockify_flex_item_if_table_internal(&mut [], usize::MAX);
4625        assert_eq!(nodes[0].formatting_context, FormattingContext::TableCell);
4626    }
4627
4628    #[test]
4629    fn table_cell_flex_items_do_not_produce_anonymous_table_boxes() {
4630        // CSS Flexbox §3: two `display:table-cell` flex items become two
4631        // independent block flex items, NOT one anonymous table row.
4632        let sd = styled(
4633            Dom::create_body().with_child(
4634                div_class("f")
4635                    .with_child(div_class("c"))
4636                    .with_child(div_class("c")),
4637            ),
4638            ".f { display: flex; } .c { display: table-cell; }",
4639        );
4640        let tree = build_tree(&sd);
4641        assert!(
4642            (0..tree.nodes.len()).all(|i| tree.cold(i).unwrap().anonymous_type.is_none()),
4643            "no anonymous table boxes for blockified flex items"
4644        );
4645        let flex = tree.children(tree.root)[0];
4646        for &c in tree.children(flex) {
4647            assert!(matches!(
4648                tree.get(c).unwrap().formatting_context,
4649                FormattingContext::Block { .. }
4650            ));
4651        }
4652    }
4653
4654    // ==================================================================
4655    // Shrink-to-fit bitmap
4656    // ==================================================================
4657
4658    #[test]
4659    fn is_shrink_to_fit_context_is_true_for_the_intrinsic_reading_contexts() {
4660        let sd = mixed_dom();
4661        for fc in [
4662            FormattingContext::Flex,
4663            FormattingContext::Grid,
4664            FormattingContext::Table,
4665            FormattingContext::InlineBlock,
4666        ] {
4667            assert!(
4668                is_shrink_to_fit_context(&sd, None, fc),
4669                "{fc:?} sizes from children's intrinsics"
4670            );
4671        }
4672    }
4673
4674    #[test]
4675    fn is_shrink_to_fit_context_is_false_for_a_plain_block_with_no_dom_node() {
4676        let sd = mixed_dom();
4677        for fc in [
4678            FormattingContext::Block {
4679                establishes_new_context: false,
4680            },
4681            FormattingContext::Block {
4682                establishes_new_context: true,
4683            },
4684            FormattingContext::Inline,
4685            FormattingContext::None,
4686            FormattingContext::TableRow,
4687        ] {
4688            assert!(
4689                !is_shrink_to_fit_context(&sd, None, fc),
4690                "{fc:?} with no DOM node cannot be float/abspos ⇒ not STF"
4691            );
4692        }
4693    }
4694
4695    #[test]
4696    fn is_shrink_to_fit_context_catches_floats_and_abspos() {
4697        let sd = styled(
4698            Dom::create_body()
4699                .with_child(div_class("fl"))
4700                .with_child(div_class("ab"))
4701                .with_child(div_class("fx"))
4702                .with_child(div_class("plain")),
4703            ".fl { float: left; } .ab { position: absolute; } .fx { position: fixed; } .plain { \
4704             display: block; }",
4705        );
4706        let block = FormattingContext::Block {
4707            establishes_new_context: false,
4708        };
4709        assert!(is_shrink_to_fit_context(&sd, Some(NodeId::new(1)), block), "float:left");
4710        assert!(is_shrink_to_fit_context(&sd, Some(NodeId::new(2)), block), "position:absolute");
4711        assert!(is_shrink_to_fit_context(&sd, Some(NodeId::new(3)), block), "position:fixed");
4712        assert!(
4713            !is_shrink_to_fit_context(&sd, Some(NodeId::new(4)), block),
4714            "an in-flow static block is sized top-down ⇒ not STF"
4715        );
4716    }
4717
4718    #[test]
4719    fn compute_subtree_needs_intrinsic_is_one_bit_per_node() {
4720        let sd = mixed_dom();
4721        let tree = build_tree(&sd);
4722        let bits = compute_subtree_needs_intrinsic(&sd, &tree);
4723        assert_eq!(bits.len(), tree.nodes.len());
4724        assert_eq!(tree.subtree_needs_intrinsic.len(), tree.nodes.len());
4725    }
4726
4727    #[test]
4728    fn compute_subtree_needs_intrinsic_is_all_false_for_a_pure_block_tree() {
4729        let sd = mixed_dom();
4730        let tree = build_tree(&sd);
4731        assert!(
4732            compute_subtree_needs_intrinsic(&sd, &tree)
4733                .iter()
4734                .all(|b| !b),
4735            "nothing in mixed_dom() is flex/grid/table/float/abspos"
4736        );
4737    }
4738
4739    #[test]
4740    fn compute_subtree_needs_intrinsic_propagates_a_deep_flex_up_to_the_root() {
4741        let sd = styled(
4742            Dom::create_body().with_child(
4743                div_class("a")
4744                    .with_child(div_class("b").with_child(div_class("f"))),
4745            ),
4746            ".a { display: block; } .b { display: block; } .f { display: flex; }",
4747        );
4748        let tree = build_tree(&sd);
4749        let bits = compute_subtree_needs_intrinsic(&sd, &tree);
4750        assert!(bits[tree.root], "out[i] = self || any(children) — must reach the root");
4751        assert!(bits.iter().all(|b| *b), "every node on the chain is on the flex path");
4752    }
4753
4754    #[test]
4755    fn compute_subtree_needs_intrinsic_leaves_a_flex_free_sibling_branch_false() {
4756        let sd = styled(
4757            Dom::create_body()
4758                .with_child(div_class("f"))
4759                .with_child(div_class("plain")),
4760            ".f { display: flex; } .plain { display: block; }",
4761        );
4762        let tree = build_tree(&sd);
4763        let bits = compute_subtree_needs_intrinsic(&sd, &tree);
4764        let kids = tree.children(tree.root);
4765        let flex = kids
4766            .iter()
4767            .copied()
4768            .find(|&i| tree.get(i).unwrap().formatting_context == FormattingContext::Flex)
4769            .expect("the flex child");
4770        let plain = kids.iter().copied().find(|&i| i != flex).expect("the plain child");
4771        assert!(bits[flex]);
4772        assert!(!bits[plain], "a sibling that reads no intrinsics stays false");
4773        assert!(bits[tree.root], "…but the root still sees the flex branch");
4774    }
4775
4776    #[test]
4777    fn compute_subtree_needs_intrinsic_on_an_empty_tree_is_empty() {
4778        let sd = mixed_dom();
4779        let tree = raw_tree(Vec::new(), &[]);
4780        assert!(compute_subtree_needs_intrinsic(&sd, &tree).is_empty());
4781    }
4782
4783    // ==================================================================
4784    // Level / display predicates
4785    // ==================================================================
4786
4787    #[test]
4788    fn is_block_level_matches_the_block_level_display_values() {
4789        let sd = styled(
4790            Dom::create_body()
4791                .with_child(div_class("b"))
4792                .with_child(div_class("i")),
4793            ".b { display: block; } .i { display: inline; }",
4794        );
4795        assert!(is_block_level(&sd, NodeId::new(1)));
4796        assert!(!is_block_level(&sd, NodeId::new(2)));
4797    }
4798
4799    #[test]
4800    fn is_block_level_covers_the_table_and_list_item_families() {
4801        for (css_display, want) in [
4802            ("block", true),
4803            ("flow-root", true),
4804            ("flex", true),
4805            ("grid", true),
4806            ("table", true),
4807            ("table-row", true),
4808            ("table-cell", true),
4809            ("table-caption", true),
4810            ("list-item", true),
4811            ("inline", false),
4812            ("inline-block", false),
4813            ("inline-flex", false),
4814            ("inline-grid", false),
4815            ("inline-table", false),
4816            ("none", false),
4817        ] {
4818            let sd = styled(
4819                Dom::create_body().with_child(div_class("x")),
4820                &format!(".x {{ display: {css_display}; }}"),
4821            );
4822            assert_eq!(
4823                is_block_level(&sd, NodeId::new(1)),
4824                want,
4825                "display:{css_display}"
4826            );
4827        }
4828    }
4829
4830    #[test]
4831    fn is_inline_level_is_always_true_for_text_regardless_of_display() {
4832        let sd = styled(
4833            Dom::create_body().with_child(div_class("b").with_child(Dom::create_text("t"))),
4834            ".b { display: block; }",
4835        );
4836        let t = text_node(&sd, "t");
4837        assert!(is_inline_level(&sd, t), "text nodes are inline-level by definition");
4838        assert!(!is_inline_level(&sd, NodeId::new(1)), "the block div is not");
4839    }
4840
4841    #[test]
4842    fn is_inline_level_matches_the_inline_display_family() {
4843        for (css_display, want) in [
4844            ("inline", true),
4845            ("inline-block", true),
4846            ("inline-table", true),
4847            ("inline-flex", true),
4848            ("inline-grid", true),
4849            ("block", false),
4850            ("flex", false),
4851            ("table", false),
4852            ("list-item", false),
4853        ] {
4854            let sd = styled(
4855                Dom::create_body().with_child(div_class("x")),
4856                &format!(".x {{ display: {css_display}; }}"),
4857            );
4858            assert_eq!(
4859                is_inline_level(&sd, NodeId::new(1)),
4860                want,
4861                "display:{css_display}"
4862            );
4863        }
4864    }
4865
4866    #[test]
4867    fn block_and_inline_level_are_mutually_exclusive_for_element_nodes() {
4868        for css_display in [
4869            "block",
4870            "inline",
4871            "inline-block",
4872            "flex",
4873            "inline-flex",
4874            "grid",
4875            "table",
4876            "list-item",
4877        ] {
4878            let sd = styled(
4879                Dom::create_body().with_child(div_class("x")),
4880                &format!(".x {{ display: {css_display}; }}"),
4881            );
4882            let id = NodeId::new(1);
4883            assert!(
4884                !(is_block_level(&sd, id) && is_inline_level(&sd, id)),
4885                "display:{css_display} cannot be both block- and inline-level"
4886            );
4887        }
4888    }
4889
4890    #[test]
4891    fn has_only_inline_children_is_false_for_a_childless_node() {
4892        let sd = styled(Dom::create_body().with_child(div_class("e")), ".e { display: block; }");
4893        assert!(
4894            !has_only_inline_children(&sd, NodeId::new(1)),
4895            "no children ⇒ no IFC (it's empty, not inline)"
4896        );
4897    }
4898
4899    #[test]
4900    fn has_only_inline_children_is_true_for_an_all_inline_run() {
4901        let sd = mixed_dom();
4902        // `.block` (DOM 1) holds a text node and an inline div.
4903        assert!(has_only_inline_children(&sd, NodeId::new(1)));
4904    }
4905
4906    #[test]
4907    fn has_only_inline_children_is_false_as_soon_as_one_block_child_appears() {
4908        let sd = mixed_dom();
4909        // `.mixed` (DOM 5) holds text + a block div + text.
4910        assert!(!has_only_inline_children(&sd, NodeId::new(5)));
4911    }
4912
4913    #[test]
4914    fn has_only_inline_children_is_false_for_an_out_of_range_node_id() {
4915        let sd = mixed_dom();
4916        let past_end = NodeId::new(sd.node_data.len() + 10);
4917        assert!(
4918            !has_only_inline_children(&sd, past_end),
4919            "the hierarchy lookup is a `.get`, so a bogus id must be false, not a panic"
4920        );
4921        assert!(!has_only_inline_children(&sd, NodeId::new(usize::MAX / 2)));
4922    }
4923
4924    // ==================================================================
4925    // is_whitespace_only_text (predicate / unicode / boundary)
4926    // ==================================================================
4927
4928    fn ws_dom(text: &str, css: &str) -> StyledDom {
4929        styled(
4930            Dom::create_body().with_child(div_class("p").with_child(Dom::create_text(text))),
4931            css,
4932        )
4933    }
4934
4935    #[test]
4936    fn is_whitespace_only_text_recognises_the_css_document_whitespace_set() {
4937        // CSS Text 3 §4.1: space, tab, CR, LF, FF.
4938        for text in [" ", "\t", "\n", "\r", "\u{000C}", " \t\r\n\u{000C} "] {
4939            let sd = ws_dom(text, "");
4940            let id = NodeId::new(2);
4941            assert!(
4942                is_whitespace_only_text(&sd, id),
4943                "{text:?} is collapsible document whitespace"
4944            );
4945        }
4946    }
4947
4948    #[test]
4949    fn is_whitespace_only_text_rejects_unicode_spaces_that_css_does_not_collapse() {
4950        // NBSP, ideographic space, en/em space, zero-width space, line separator:
4951        // none of these are in the CSS document-whitespace set.
4952        for text in [
4953            "\u{00A0}",
4954            "\u{3000}",
4955            "\u{2002}",
4956            "\u{2003}",
4957            "\u{200B}",
4958            "\u{2028}",
4959            " \u{00A0} ",
4960        ] {
4961            let sd = ws_dom(text, "");
4962            assert!(
4963                !is_whitespace_only_text(&sd, NodeId::new(2)),
4964                "{text:?} must NOT be treated as collapsible whitespace"
4965            );
4966        }
4967    }
4968
4969    #[test]
4970    fn is_whitespace_only_text_is_false_for_real_text() {
4971        for text in ["hi", " hi ", "\u{1F600}", "a\nb"] {
4972            let sd = ws_dom(text, "");
4973            assert!(!is_whitespace_only_text(&sd, NodeId::new(2)), "{text:?}");
4974        }
4975    }
4976
4977    #[test]
4978    fn is_whitespace_only_text_treats_the_empty_string_as_whitespace() {
4979        // `"".chars().all(..)` is vacuously true — an empty text node is
4980        // collapsible and generates no anonymous inline box.
4981        let sd = ws_dom("", "");
4982        assert!(is_whitespace_only_text(&sd, NodeId::new(2)));
4983    }
4984
4985    #[test]
4986    fn is_whitespace_only_text_respects_whitespace_preserving_modes() {
4987        for (ws, collapses) in [
4988            ("normal", true),
4989            ("nowrap", true),
4990            ("pre-line", true),
4991            ("pre", false),
4992            ("pre-wrap", false),
4993            ("break-spaces", false),
4994        ] {
4995            let sd = ws_dom(" \n ", &format!(".p {{ white-space: {ws}; }}"));
4996            assert_eq!(
4997                is_whitespace_only_text(&sd, NodeId::new(2)),
4998                collapses,
4999                "white-space:{ws} — preserved whitespace still generates a box"
5000            );
5001        }
5002    }
5003
5004    #[test]
5005    fn is_whitespace_only_text_is_false_for_non_text_and_bogus_nodes() {
5006        let sd = mixed_dom();
5007        assert!(!is_whitespace_only_text(&sd, NodeId::new(1)), "a div is not text");
5008        assert!(!is_whitespace_only_text(&sd, NodeId::ZERO), "the body is not text");
5009        let past_end = NodeId::new(sd.node_data.len() + 1);
5010        assert!(
5011            !is_whitespace_only_text(&sd, past_end),
5012            "an out-of-range id must return false, not panic"
5013        );
5014        assert!(!is_whitespace_only_text(&sd, NodeId::new(usize::MAX / 2)));
5015    }
5016
5017    // ==================================================================
5018    // Table-structure predicates
5019    // ==================================================================
5020
5021    #[test]
5022    fn should_skip_for_table_structure_only_fires_inside_table_parents() {
5023        let sd = ws_dom(" ", "");
5024        let ws = NodeId::new(2);
5025        for parent in [
5026            LayoutDisplay::Table,
5027            LayoutDisplay::InlineTable,
5028            LayoutDisplay::TableRowGroup,
5029            LayoutDisplay::TableHeaderGroup,
5030            LayoutDisplay::TableFooterGroup,
5031            LayoutDisplay::TableRow,
5032        ] {
5033            assert!(
5034                should_skip_for_table_structure(&sd, ws, parent),
5035                "whitespace under {parent:?} is an irrelevant box"
5036            );
5037        }
5038        for parent in [
5039            LayoutDisplay::Block,
5040            LayoutDisplay::Inline,
5041            LayoutDisplay::Flex,
5042            LayoutDisplay::TableCell,
5043            LayoutDisplay::TableCaption,
5044            LayoutDisplay::TableColumn,
5045        ] {
5046            assert!(
5047                !should_skip_for_table_structure(&sd, ws, parent),
5048                "whitespace under {parent:?} is NOT skipped by §17.2.1 stage 1"
5049            );
5050        }
5051    }
5052
5053    #[test]
5054    fn should_skip_for_table_structure_never_skips_real_content() {
5055        let sd = ws_dom("cell text", "");
5056        for parent in ALL_DISPLAYS {
5057            assert!(
5058                !should_skip_for_table_structure(&sd, NodeId::new(2), parent),
5059                "non-whitespace text must never be dropped (parent {parent:?})"
5060            );
5061        }
5062    }
5063
5064    #[test]
5065    fn is_proper_table_child_matches_exactly_the_seven_spec_values() {
5066        let proper = [
5067            LayoutDisplay::TableRowGroup,
5068            LayoutDisplay::TableHeaderGroup,
5069            LayoutDisplay::TableFooterGroup,
5070            LayoutDisplay::TableRow,
5071            LayoutDisplay::TableColumnGroup,
5072            LayoutDisplay::TableColumn,
5073            LayoutDisplay::TableCaption,
5074        ];
5075        for d in ALL_DISPLAYS {
5076            assert_eq!(
5077                is_proper_table_child(d),
5078                proper.contains(&d),
5079                "CSS 2.2 §17.2.1 proper-table-child set: {d:?}"
5080            );
5081        }
5082        assert!(
5083            !is_proper_table_child(LayoutDisplay::TableCell),
5084            "a cell is a proper child of a ROW, not of a table"
5085        );
5086    }
5087
5088    // ==================================================================
5089    // is_replaced_element
5090    // ==================================================================
5091
5092    #[test]
5093    fn is_replaced_element_covers_the_css_display_3_appendix_b_set() {
5094        for nt in [
5095            NodeType::Br,
5096            NodeType::Wbr,
5097            NodeType::Meter,
5098            NodeType::Progress,
5099            NodeType::Canvas,
5100            NodeType::Embed,
5101            NodeType::Object,
5102            NodeType::Audio,
5103            NodeType::Video,
5104            NodeType::Input,
5105            NodeType::TextArea,
5106            NodeType::Select,
5107            NodeType::VirtualView,
5108        ] {
5109            let nd = NodeData::create_node(nt.clone());
5110            assert!(is_replaced_element(&nd), "{nt:?} is a replaced element");
5111        }
5112
5113        let img = NodeData::create_image(ImageRef::null_image(
5114            1,
5115            1,
5116            RawImageFormat::R8,
5117            Vec::new(),
5118        ));
5119        assert!(is_replaced_element(&img), "an <img> is the canonical replaced element");
5120    }
5121
5122    #[test]
5123    fn is_replaced_element_is_false_for_ordinary_containers_and_text() {
5124        for nt in [
5125            NodeType::Div,
5126            NodeType::Body,
5127            NodeType::Html,
5128            NodeType::P,
5129            NodeType::Span,
5130            NodeType::Table,
5131            NodeType::Button,
5132            NodeType::Label,
5133            NodeType::Hr,
5134        ] {
5135            let nd = NodeData::create_node(nt.clone());
5136            assert!(!is_replaced_element(&nd), "{nt:?} is not replaced");
5137        }
5138        assert!(!is_replaced_element(&NodeData::create_text("hello")));
5139    }
5140
5141    #[test]
5142    fn display_contents_on_a_replaced_element_degrades_to_display_none() {
5143        // CSS Display 3 §2.5: a replaced element cannot be un-boxed.
5144        let sd = styled(
5145            Dom::create_body().with_child(
5146                Dom::create_from_data(NodeData::create_node(NodeType::Br))
5147                    .with_ids_and_classes(vec![IdOrClass::Class("c".into())].into()),
5148            ),
5149            ".c { display: contents; }",
5150        );
5151        let tree = build_tree(&sd);
5152        assert!(
5153            tree.children(tree.root).is_empty(),
5154            "the <br> must be dropped from its parent's child list"
5155        );
5156        let br = (0..tree.nodes.len())
5157            .find(|&i| tree.get(i).unwrap().dom_node_id == Some(NodeId::new(1)))
5158            .expect("the node object still exists, just unparented");
5159        assert_eq!(
5160            tree.warm(br).unwrap().computed_style.display,
5161            LayoutDisplay::None
5162        );
5163        assert_eq!(tree.get(br).unwrap().formatting_context, FormattingContext::None);
5164    }
5165
5166    // ==================================================================
5167    // get_display_type
5168    // ==================================================================
5169
5170    #[test]
5171    fn get_display_type_reads_the_computed_display() {
5172        for (css_display, want) in [
5173            ("none", LayoutDisplay::None),
5174            ("block", LayoutDisplay::Block),
5175            ("inline", LayoutDisplay::Inline),
5176            ("inline-block", LayoutDisplay::InlineBlock),
5177            ("flex", LayoutDisplay::Flex),
5178            ("grid", LayoutDisplay::Grid),
5179            ("table", LayoutDisplay::Table),
5180            ("table-row", LayoutDisplay::TableRow),
5181            ("table-cell", LayoutDisplay::TableCell),
5182            ("flow-root", LayoutDisplay::FlowRoot),
5183            ("list-item", LayoutDisplay::ListItem),
5184            ("contents", LayoutDisplay::Contents),
5185        ] {
5186            let sd = styled(
5187                Dom::create_body().with_child(div_class("x")),
5188                &format!(".x {{ display: {css_display}; }}"),
5189            );
5190            assert_eq!(
5191                get_display_type(&sd, NodeId::new(1)),
5192                want,
5193                "display:{css_display}"
5194            );
5195        }
5196    }
5197
5198    #[test]
5199    fn get_display_type_is_stable_across_repeated_calls() {
5200        let sd = mixed_dom();
5201        for i in 0..sd.node_data.len() {
5202            let id = NodeId::new(i);
5203            let a = get_display_type(&sd, id);
5204            let b = get_display_type(&sd, id);
5205            assert_eq!(a, b, "node {i} must be deterministic");
5206        }
5207    }
5208
5209    // ==================================================================
5210    // Formatting-context determination
5211    // ==================================================================
5212
5213    #[test]
5214    fn determine_formatting_context_is_inline_for_every_text_node() {
5215        let sd = mixed_dom();
5216        for needle in ["hello", "world", "tail", " \n\t"] {
5217            let id = text_node(&sd, needle);
5218            assert_eq!(
5219                determine_formatting_context(&sd, id),
5220                FormattingContext::Inline,
5221                "text node {needle:?}"
5222            );
5223        }
5224    }
5225
5226    #[test]
5227    fn determine_formatting_context_for_display_ignores_display_on_text_nodes() {
5228        // The text early-out fires before the display match — a text node is
5229        // Inline even if you hand it `display: grid`.
5230        let sd = mixed_dom();
5231        let t = text_node(&sd, "hello");
5232        for d in ALL_DISPLAYS {
5233            assert_eq!(
5234                determine_formatting_context_for_display(&sd, t, d),
5235                FormattingContext::Inline,
5236                "text + display:{d:?}"
5237            );
5238        }
5239    }
5240
5241    #[test]
5242    fn determine_formatting_context_for_display_maps_each_display_value() {
5243        let sd = styled(Dom::create_body().with_child(div_class("x")), ".x { display: block; }");
5244        let id = NodeId::new(1);
5245        for (d, want) in [
5246            (LayoutDisplay::Inline, FormattingContext::Inline),
5247            (
5248                LayoutDisplay::FlowRoot,
5249                FormattingContext::Block {
5250                    establishes_new_context: true,
5251                },
5252            ),
5253            (LayoutDisplay::InlineBlock, FormattingContext::InlineBlock),
5254            (LayoutDisplay::Table, FormattingContext::Table),
5255            (LayoutDisplay::InlineTable, FormattingContext::Table),
5256            (LayoutDisplay::TableRowGroup, FormattingContext::TableRowGroup),
5257            (LayoutDisplay::TableHeaderGroup, FormattingContext::TableRowGroup),
5258            (LayoutDisplay::TableFooterGroup, FormattingContext::TableRowGroup),
5259            (LayoutDisplay::TableRow, FormattingContext::TableRow),
5260            (LayoutDisplay::TableCell, FormattingContext::TableCell),
5261            (LayoutDisplay::TableColumnGroup, FormattingContext::TableColumnGroup),
5262            (LayoutDisplay::TableCaption, FormattingContext::TableCaption),
5263            (LayoutDisplay::TableColumn, FormattingContext::None),
5264            (LayoutDisplay::None, FormattingContext::None),
5265            (LayoutDisplay::Flex, FormattingContext::Flex),
5266            (LayoutDisplay::InlineFlex, FormattingContext::Flex),
5267            (LayoutDisplay::Grid, FormattingContext::Grid),
5268            (LayoutDisplay::InlineGrid, FormattingContext::Grid),
5269            (LayoutDisplay::Contents, FormattingContext::Contents),
5270            (
5271                LayoutDisplay::RunIn,
5272                FormattingContext::Block {
5273                    establishes_new_context: true,
5274                },
5275            ),
5276            (
5277                LayoutDisplay::Marker,
5278                FormattingContext::Block {
5279                    establishes_new_context: true,
5280                },
5281            ),
5282        ] {
5283            assert_eq!(
5284                determine_formatting_context_for_display(&sd, id, d),
5285                want,
5286                "display:{d:?}"
5287            );
5288        }
5289    }
5290
5291    #[test]
5292    fn determine_formatting_context_for_display_never_panics_on_any_display_value() {
5293        let sd = styled(Dom::create_body().with_child(div_class("x")), ".x { display: block; }");
5294        for d in ALL_DISPLAYS {
5295            let _ = determine_formatting_context_for_display(&sd, NodeId::new(1), d);
5296            let _ = determine_formatting_context_for_display(&sd, NodeId::ZERO, d);
5297        }
5298    }
5299
5300    #[test]
5301    fn a_block_with_only_inline_children_establishes_an_ifc() {
5302        let sd = mixed_dom();
5303        assert_eq!(
5304            determine_formatting_context(&sd, NodeId::new(1)),
5305            FormattingContext::Inline,
5306            "CSS 2.2 §9.4.2: a block container with no block-level boxes establishes an IFC"
5307        );
5308    }
5309
5310    #[test]
5311    fn a_block_with_a_block_child_stays_a_bfc() {
5312        let sd = mixed_dom();
5313        assert!(matches!(
5314            determine_formatting_context(&sd, NodeId::new(5)),
5315            FormattingContext::Block { .. }
5316        ));
5317    }
5318
5319    #[test]
5320    fn establishes_new_bfc_for_the_unconditional_display_values() {
5321        for css_display in ["inline-block", "table-cell", "table-caption", "flow-root"] {
5322            let sd = styled(
5323                Dom::create_body().with_child(div_class("x")),
5324                &format!(".x {{ display: {css_display}; }}"),
5325            );
5326            assert!(
5327                establishes_new_block_formatting_context(&sd, NodeId::new(1)),
5328                "display:{css_display} always establishes a BFC"
5329            );
5330        }
5331    }
5332
5333    #[test]
5334    fn establishes_new_bfc_for_non_visible_overflow_floats_and_abspos() {
5335        for css in [
5336            ".x { display: block; overflow-x: hidden; }",
5337            ".x { display: block; overflow-y: scroll; }",
5338            ".x { display: block; overflow: auto; }",
5339            ".x { display: block; float: left; }",
5340            ".x { display: block; float: right; }",
5341            ".x { display: block; position: absolute; }",
5342            ".x { display: block; position: fixed; }",
5343        ] {
5344            let sd = styled(Dom::create_body().with_child(div_class("x")), css);
5345            assert!(
5346                establishes_new_block_formatting_context(&sd, NodeId::new(1)),
5347                "{css} must establish a BFC"
5348            );
5349        }
5350    }
5351
5352    #[test]
5353    fn establishes_new_bfc_is_false_for_a_plain_in_flow_block() {
5354        let sd = styled(
5355            Dom::create_body().with_child(div_class("x")),
5356            ".x { display: block; }",
5357        );
5358        assert!(
5359            !establishes_new_block_formatting_context(&sd, NodeId::new(1)),
5360            "a static, visible-overflow, unfloated block does not open a BFC"
5361        );
5362    }
5363
5364    #[test]
5365    fn establishes_new_bfc_for_the_root_and_for_replaced_elements() {
5366        let sd = styled(
5367            Dom::create_body().with_child(Dom::create_from_data(NodeData::create_node(NodeType::Br))),
5368            "",
5369        );
5370        assert!(
5371            establishes_new_block_formatting_context(&sd, NodeId::ZERO),
5372            "the root element always establishes a BFC"
5373        );
5374        assert!(
5375            establishes_new_block_formatting_context(&sd, NodeId::new(1)),
5376            "replaced elements always establish an independent formatting context"
5377        );
5378    }
5379
5380    // ==================================================================
5381    // compute_layout_style
5382    // ==================================================================
5383
5384    #[test]
5385    fn compute_layout_style_captures_every_property_it_advertises() {
5386        let sd = styled(
5387            Dom::create_body().with_child(div_class("x")),
5388            ".x { display: flex; position: absolute; overflow-x: hidden; overflow-y: scroll; \
5389             width: 50px; height: 60px; min-width: 10px; min-height: 11px; max-width: 99px; \
5390             max-height: 98px; text-align: center; }",
5391        );
5392        let s = compute_layout_style(&sd, NodeId::new(1));
5393        assert_eq!(s.display, LayoutDisplay::Flex);
5394        assert_eq!(s.position, LayoutPosition::Absolute);
5395        assert_eq!(s.overflow_x, LayoutOverflow::Hidden);
5396        assert_eq!(s.overflow_y, LayoutOverflow::Scroll);
5397        assert_eq!(s.text_align, StyleTextAlign::Center);
5398        assert!(s.width.is_some());
5399        assert!(s.height.is_some());
5400        assert!(s.min_width.is_some());
5401        assert!(s.min_height.is_some());
5402        assert!(s.max_width.is_some());
5403        assert!(s.max_height.is_some());
5404    }
5405
5406    #[test]
5407    fn compute_layout_style_leaves_auto_sizes_as_none() {
5408        let sd = styled(
5409            Dom::create_body().with_child(div_class("x")),
5410            ".x { display: block; }",
5411        );
5412        let s = compute_layout_style(&sd, NodeId::new(1));
5413        assert!(s.width.is_none(), "auto width must be None, not 0px");
5414        assert!(s.height.is_none());
5415        assert!(s.max_width.is_none());
5416        assert!(s.max_height.is_none());
5417        assert_eq!(s.float, LayoutFloat::None);
5418        assert_eq!(s.position, LayoutPosition::Static);
5419    }
5420
5421    #[test]
5422    fn compute_layout_style_reads_float_left_and_right() {
5423        for (css, want) in [("left", LayoutFloat::Left), ("right", LayoutFloat::Right)] {
5424            let sd = styled(
5425                Dom::create_body().with_child(div_class("x")),
5426                &format!(".x {{ float: {css}; }}"),
5427            );
5428            assert_eq!(compute_layout_style(&sd, NodeId::new(1)).float, want);
5429        }
5430    }
5431
5432    #[test]
5433    fn compute_layout_style_never_panics_on_any_node_of_a_real_dom() {
5434        let sd = mixed_dom();
5435        for i in 0..sd.node_data.len() {
5436            let _ = compute_layout_style(&sd, NodeId::new(i));
5437        }
5438    }
5439
5440    // ==================================================================
5441    // Font-size helpers
5442    // ==================================================================
5443
5444    #[test]
5445    fn font_size_helpers_fall_back_to_the_default_when_nothing_is_specified() {
5446        let sd = styled(Dom::create_body().with_child(div_class("x")), "");
5447        assert_eq!(get_root_font_size(&sd), DEFAULT_FONT_SIZE);
5448        assert_eq!(
5449            get_parent_font_size(&sd, NodeId::ZERO),
5450            DEFAULT_FONT_SIZE,
5451            "the root has no parent ⇒ documented DEFAULT_FONT_SIZE fallback"
5452        );
5453        assert_eq!(get_element_font_size(&sd, NodeId::new(1)), DEFAULT_FONT_SIZE);
5454    }
5455
5456    #[test]
5457    fn get_element_and_parent_font_size_track_the_cascade() {
5458        let sd = styled(
5459            Dom::create_body().with_child(div_class("big").with_child(div_class("small"))),
5460            ".big { font-size: 32px; } .small { font-size: 8px; }",
5461        );
5462        assert_eq!(get_element_font_size(&sd, NodeId::new(1)), 32.0);
5463        assert_eq!(get_element_font_size(&sd, NodeId::new(2)), 8.0);
5464        assert_eq!(
5465            get_parent_font_size(&sd, NodeId::new(2)),
5466            32.0,
5467            "the parent's size, not the element's own"
5468        );
5469    }
5470
5471    #[test]
5472    fn get_root_font_size_reads_node_zero() {
5473        let root = Dom::create_body()
5474            .with_ids_and_classes(vec![IdOrClass::Class("root".into())].into())
5475            .with_child(div_class("x"));
5476        let sd = styled(root, ".root { font-size: 20px; }");
5477        assert_eq!(get_root_font_size(&sd), 20.0, "get_root_font_size hard-codes NodeId(0)");
5478        assert_eq!(get_root_font_size(&sd), get_element_font_size(&sd, NodeId::ZERO));
5479    }
5480
5481    #[test]
5482    fn font_size_helpers_return_a_finite_positive_size_for_every_node() {
5483        let sd = mixed_dom();
5484        for i in 0..sd.node_data.len() {
5485            let id = NodeId::new(i);
5486            for size in [get_element_font_size(&sd, id), get_parent_font_size(&sd, id)] {
5487                assert!(size.is_finite(), "node {i}: {size}");
5488                assert!(size > 0.0, "node {i}: a zero/negative font-size breaks em math");
5489            }
5490        }
5491    }
5492
5493    // ==================================================================
5494    // create_resolution_context (numeric / NaN-inf passthrough)
5495    // ==================================================================
5496
5497    #[test]
5498    fn create_resolution_context_zeroes_an_unknown_containing_block() {
5499        // css-sizing-3 §5.2.1: % margins/padding resolve against 0 when the
5500        // containing block isn't known yet (cycle breaking).
5501        let sd = mixed_dom();
5502        let ctx = create_resolution_context(&sd, NodeId::new(1), None, VIEWPORT);
5503        assert_eq!(ctx.containing_block_size.width, 0.0);
5504        assert_eq!(ctx.containing_block_size.height, 0.0);
5505        assert!(ctx.element_size.is_none(), "not laid out yet");
5506        assert_eq!(ctx.viewport_size.width, VIEWPORT.width);
5507        assert_eq!(ctx.viewport_size.height, VIEWPORT.height);
5508    }
5509
5510    #[test]
5511    fn create_resolution_context_passes_a_known_containing_block_through() {
5512        let sd = mixed_dom();
5513        let cb = PhysicalSize::new(321.0, 123.0);
5514        let ctx = create_resolution_context(&sd, NodeId::new(1), Some(cb), VIEWPORT);
5515        assert_eq!(ctx.containing_block_size.width, 321.0);
5516        assert_eq!(ctx.containing_block_size.height, 123.0);
5517    }
5518
5519    #[test]
5520    fn create_resolution_context_survives_a_degenerate_viewport() {
5521        let sd = mixed_dom();
5522        for vp in [
5523            LogicalSize::new(0.0, 0.0),
5524            LogicalSize::new(-100.0, -100.0),
5525            LogicalSize::new(f32::MAX, f32::MAX),
5526            LogicalSize::new(f32::INFINITY, f32::NEG_INFINITY),
5527            LogicalSize::new(f32::NAN, f32::NAN),
5528        ] {
5529            let ctx = create_resolution_context(&sd, NodeId::new(1), None, vp);
5530            // Viewport is passed through verbatim; the font sizes must stay sane.
5531            assert!(ctx.element_font_size.is_finite());
5532            assert!(ctx.parent_font_size.is_finite());
5533            assert!(ctx.root_font_size.is_finite());
5534        }
5535    }
5536
5537    #[test]
5538    fn create_resolution_context_survives_a_degenerate_containing_block() {
5539        let sd = mixed_dom();
5540        for cb in [
5541            PhysicalSize::new(0.0, 0.0),
5542            PhysicalSize::new(-1.0, -1.0),
5543            PhysicalSize::new(f32::NAN, f32::INFINITY),
5544            PhysicalSize::new(f32::MAX, f32::MIN),
5545        ] {
5546            let ctx = create_resolution_context(&sd, NodeId::new(1), Some(cb), VIEWPORT);
5547            assert!(ctx.root_font_size.is_finite());
5548        }
5549    }
5550
5551    // ==================================================================
5552    // collect_box_props (numeric / saturation / spec zeroing)
5553    // ==================================================================
5554
5555    fn collect_for(css: &str, node: usize, viewport: LogicalSize) -> CollectedBoxProps {
5556        let sd = styled(Dom::create_body().with_child(div_class("x")), css);
5557        let mut msgs = None;
5558        collect_box_props(&sd, NodeId::new(node), &mut msgs, viewport)
5559    }
5560
5561    #[test]
5562    fn collect_box_props_resolves_plain_pixel_edges() {
5563        let c = collect_for(
5564            ".x { margin: 10px; padding: 5px; border: 2px solid black; }",
5565            1,
5566            VIEWPORT,
5567        );
5568        assert_eq!(c.resolved.margin.top, 10.0);
5569        assert_eq!(c.resolved.margin.left, 10.0);
5570        assert_eq!(c.resolved.padding.right, 5.0);
5571        assert_eq!(c.resolved.border.bottom, 2.0);
5572    }
5573
5574    #[test]
5575    fn collect_box_props_zeroes_a_border_whose_style_is_none() {
5576        // CSS 2.2 §8.5.1: computed border-width is 0 when border-style is none/hidden.
5577        let c = collect_for(".x { border-width: 9px; border-style: none; }", 1, VIEWPORT);
5578        assert_eq!(c.resolved.border.top, 0.0);
5579        assert_eq!(c.resolved.border.left, 0.0);
5580
5581        let c = collect_for(".x { border-width: 9px; border-style: hidden; }", 1, VIEWPORT);
5582        assert_eq!(c.resolved.border.right, 0.0);
5583    }
5584
5585    #[test]
5586    fn collect_box_props_strips_margins_and_padding_from_internal_table_boxes() {
5587        // CSS 2.2 §17.5: internal table elements have no margins; rows/groups/
5588        // columns additionally have no padding.
5589        for display in [
5590            "table-row",
5591            "table-row-group",
5592            "table-header-group",
5593            "table-footer-group",
5594            "table-column",
5595            "table-column-group",
5596        ] {
5597            let c = collect_for(
5598                &format!(".x {{ display: {display}; margin: 10px; padding: 7px; }}"),
5599                1,
5600                VIEWPORT,
5601            );
5602            assert_eq!(c.resolved.margin.top, 0.0, "display:{display} margin");
5603            assert_eq!(c.resolved.padding.top, 0.0, "display:{display} padding");
5604        }
5605
5606        // A cell keeps its padding but loses its margin.
5607        let c = collect_for(".x { display: table-cell; margin: 10px; padding: 7px; }", 1, VIEWPORT);
5608        assert_eq!(c.resolved.margin.left, 0.0, "cells have no margins");
5609        assert_eq!(c.resolved.padding.left, 7.0, "…but they do have padding");
5610    }
5611
5612    #[test]
5613    fn collect_box_props_zeroes_vertical_margins_on_a_non_replaced_inline() {
5614        let c = collect_for(".x { display: inline; margin: 10px; }", 1, VIEWPORT);
5615        assert_eq!(c.resolved.margin.top, 0.0);
5616        assert_eq!(c.resolved.margin.bottom, 0.0);
5617        assert_eq!(
5618            c.resolved.margin.left, 10.0,
5619            "horizontal margins still apply to inline boxes"
5620        );
5621        assert_eq!(c.resolved.margin.right, 10.0);
5622    }
5623
5624    #[test]
5625    fn collect_box_props_does_not_clamp_huge_lengths_before_packing() {
5626        // collect_box_props returns f32; the ±3276.8px saturation happens later,
5627        // in PackedBoxProps. Assert the split so a regression in either is visible.
5628        let c = collect_for(".x { margin: 99999px; }", 1, VIEWPORT);
5629        assert_eq!(c.resolved.margin.top, 99_999.0);
5630
5631        let packed = PackedBoxProps::pack(&c.resolved);
5632        assert_eq!(packed.margin[0], i16::MAX, "the packing saturates, it does not wrap");
5633    }
5634
5635    #[test]
5636    fn collect_box_props_survives_a_degenerate_viewport() {
5637        for vp in [
5638            LogicalSize::new(0.0, 0.0),
5639            LogicalSize::new(-800.0, -600.0),
5640            LogicalSize::new(f32::MAX, f32::MAX),
5641            LogicalSize::new(f32::INFINITY, f32::INFINITY),
5642            LogicalSize::new(f32::NAN, f32::NAN),
5643        ] {
5644            // vh/vw units make the viewport actually load-bearing here.
5645            let c = collect_for(".x { margin: 10vh; padding: 5vw; }", 1, vp);
5646            let packed = PackedBoxProps::pack(&c.resolved);
5647            for v in packed.margin.iter().chain(packed.padding.iter()) {
5648                assert!(
5649                    (i16::MIN..=i16::MAX).contains(v),
5650                    "packing must stay in range for viewport {vp:?}"
5651                );
5652            }
5653        }
5654    }
5655
5656    #[test]
5657    fn collect_box_props_fills_debug_messages_when_asked() {
5658        let sd = styled(
5659            Dom::create_body().with_child(div_class("x")),
5660            ".x { margin: 3px; }",
5661        );
5662        let mut msgs: Option<Vec<LayoutDebugMessage>> = Some(Vec::new());
5663        let _ = collect_box_props(&sd, NodeId::new(1), &mut msgs, VIEWPORT);
5664        assert!(
5665            !msgs.expect("still Some").is_empty(),
5666            "a Some(vec) sink must actually receive the [BOX] trace"
5667        );
5668
5669        // …and a None sink must be left alone (no allocation, no panic).
5670        let mut none_sink: Option<Vec<LayoutDebugMessage>> = None;
5671        let _ = collect_box_props(&sd, NodeId::new(1), &mut none_sink, VIEWPORT);
5672        assert!(none_sink.is_none());
5673    }
5674
5675    #[test]
5676    fn collect_box_props_unresolved_and_resolved_agree_after_a_re_resolve() {
5677        let c = collect_for(".x { margin: 4px; padding: 6px; }", 1, VIEWPORT);
5678        let params = crate::solver3::geometry::ResolutionParams {
5679            containing_block: VIEWPORT,
5680            viewport_size: VIEWPORT,
5681            element_font_size: DEFAULT_FONT_SIZE,
5682            root_font_size: DEFAULT_FONT_SIZE,
5683        };
5684        let again = c.unresolved.resolve(&params);
5685        assert_eq!(again.margin.top, c.resolved.margin.top);
5686        assert_eq!(again.padding.left, c.resolved.padding.left);
5687        assert_eq!(again.border.top, c.resolved.border.top);
5688    }
5689
5690    #[test]
5691    fn edge_sizes_default_is_all_zero() {
5692        let e = EdgeSizes::default();
5693        assert_eq!((e.top, e.right, e.bottom, e.left), (0.0, 0.0, 0.0, 0.0));
5694    }
5695
5696    // ==================================================================
5697    // Whole-pipeline invariants
5698    // ==================================================================
5699
5700    #[test]
5701    fn a_freshly_built_tree_satisfies_every_structural_invariant() {
5702        for sd in [
5703            mixed_dom(),
5704            styled(Dom::create_body(), ""),
5705            styled(
5706                Dom::create_body().with_child(div_class("f").with_child(div_class("c"))),
5707                ".f { display: flex; } .c { display: table-cell; }",
5708            ),
5709            styled(
5710                Dom::create_body().with_child(div_class("t").with_child(div_class("c"))),
5711                ".t { display: table; } .c { display: table-cell; }",
5712            ),
5713            styled(
5714                Dom::create_body().with_child(div_class("li").with_child(Dom::create_text("x"))),
5715                ".li { display: list-item; }",
5716            ),
5717        ] {
5718            let tree = build_tree(&sd);
5719            let n = tree.nodes.len();
5720            assert!(n >= 1);
5721            assert_eq!(tree.warm.len(), n);
5722            assert_eq!(tree.cold.len(), n);
5723            assert_eq!(tree.children_offsets.len(), n);
5724            assert_eq!(tree.subtree_needs_intrinsic.len(), n);
5725            assert!(tree.root < n);
5726            assert_eq!(tree.get(tree.root).unwrap().parent, None);
5727
5728            for i in 0..n {
5729                if let Some(p) = tree.get(i).unwrap().parent {
5730                    assert!(p < n, "node {i}'s parent {p} is out of range");
5731                }
5732                for &c in tree.children(i) {
5733                    assert!(c < n, "node {i}'s child {c} is out of range");
5734                    assert_ne!(c, i, "no node may be its own child");
5735                }
5736            }
5737            for (dom_id, indices) in &tree.dom_to_layout {
5738                for &i in indices {
5739                    assert!(i < n, "dom_to_layout[{dom_id:?}] points at {i}, out of range");
5740                    assert_eq!(tree.get(i).unwrap().dom_node_id, Some(*dom_id));
5741                }
5742            }
5743        }
5744    }
5745
5746    #[test]
5747    fn building_a_body_only_dom_yields_exactly_one_node() {
5748        let sd = styled(Dom::create_body(), "");
5749        let tree = build_tree(&sd);
5750        assert_eq!(tree.nodes.len(), 1);
5751        assert_eq!(tree.root, 0);
5752        assert!(tree.children(0).is_empty());
5753        assert!(tree.children_arena.is_empty());
5754        assert_eq!(tree.children_offsets, vec![(0, 0)]);
5755        assert_eq!(tree.memory_report().node_count, 1);
5756    }
5757
5758    #[test]
5759    fn the_root_box_always_establishes_a_new_block_formatting_context() {
5760        let tree = build_tree(&mixed_dom());
5761        match tree.get(tree.root).unwrap().formatting_context {
5762            FormattingContext::Block {
5763                establishes_new_context,
5764            } => assert!(establishes_new_context, "process_node forces this for the root"),
5765            other => panic!("the root should be a Block FC, got {other:?}"),
5766        }
5767    }
5768
5769    #[test]
5770    fn a_deeply_nested_dom_builds_without_blowing_the_stack() {
5771        // process_node recurses once per level; 200 is well inside a test thread's
5772        // stack but deep enough to catch an accidental per-level allocation blowup.
5773        let mut dom = div_class("d");
5774        for _ in 0..200 {
5775            dom = div_class("d").with_child(dom);
5776        }
5777        let sd = styled(Dom::create_body().with_child(dom), ".d { display: block; }");
5778        let tree = build_tree(&sd);
5779        assert_eq!(tree.nodes.len(), 202, "body + 201 divs");
5780        // The chain must be a straight line: every node but the last has 1 child.
5781        let mut i = tree.root;
5782        let mut depth = 0;
5783        while let Some(&next) = tree.children(i).first() {
5784            i = next;
5785            depth += 1;
5786            assert!(depth <= 202, "the parent/child links formed a cycle");
5787        }
5788        assert_eq!(depth, 201);
5789    }
5790}