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