1
//! Layout tree construction from a styled DOM, including anonymous box generation
2
use std::{
3
    collections::BTreeMap,
4
    hash::{Hash, Hasher},
5
    sync::{
6
        atomic::{AtomicU32, Ordering},
7
        Arc,
8
    },
9
};
10

            
11
use azul_core::diff::NodeDataFingerprint;
12

            
13
use crate::text3::cache::UnifiedConstraints;
14

            
15
/// Global counter for IFC IDs. Resets to 0 when layout() callback is invoked.
16
static IFC_ID_COUNTER: AtomicU32 = AtomicU32::new(0);
17

            
18
/// Unique identifier for an Inline Formatting Context (IFC).
19
///
20
/// An IFC represents a region where inline content (text, inline-blocks, images)
21
/// is laid out together. One IFC can contain content from multiple DOM nodes
22
/// (e.g., `<p>Hello <span>world</span>!</p>` is one IFC with 3 text runs).
23
///
24
/// The ID is generated using a global atomic counter that resets at the start
25
/// of each layout pass. This ensures:
26
/// - IDs are unique within a layout pass
27
/// - The same logical IFC gets the same ID across frames (for selection stability)
28
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
29
pub struct IfcId(pub u32);
30

            
31
impl IfcId {
32
    /// Generate a new unique IFC ID.
33
20755
    pub fn unique() -> Self {
34
20755
        Self(IFC_ID_COUNTER.fetch_add(1, Ordering::Relaxed))
35
20755
    }
36

            
37
    /// Reset the IFC ID counter. Called at the start of each layout pass.
38
2765
    pub fn reset_counter() {
39
2765
        IFC_ID_COUNTER.store(0, Ordering::Relaxed);
40
2765
    }
41
}
42

            
43
/// Tracks a layout node's membership in an Inline Formatting Context.
44
///
45
/// Text nodes don't store their own `inline_layout_result` - instead, they
46
/// participate in their parent's IFC. This struct provides the link from
47
/// a text node back to its IFC's layout data.
48
///
49
/// # Architecture
50
///
51
/// ```text
52
/// DOM:  <p>Hello <span>world</span>!</p>
53
///
54
/// Layout Tree:
55
/// ├── LayoutNode (p) - IFC root
56
/// │   └── inline_layout_result: Some(UnifiedLayout)
57
/// │   └── ifc_id: IfcId(5)
58
/// │
59
/// ├── LayoutNode (::text "Hello ")
60
/// │   └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 0 })
61
/// │
62
/// ├── LayoutNode (span)
63
/// │   └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 1 })
64
/// │   └── LayoutNode (::text "world")
65
/// │       └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 1 })
66
/// │
67
/// └── LayoutNode (::text "!")
68
///     └── ifc_membership: Some(IfcMembership { ifc_id: 5, run_index: 2 })
69
/// ```
70
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71
pub struct IfcMembership {
72
    /// The IFC ID this node's content was laid out in.
73
    pub ifc_id: IfcId,
74
    /// The index of the IFC root LayoutNode in the layout tree.
75
    /// Used to quickly find the node with `inline_layout_result`.
76
    pub ifc_root_layout_index: usize,
77
    /// Which run index within the IFC corresponds to this node's text.
78
    /// Maps to `ContentIndex::run_index` in the shaped items.
79
    pub run_index: u32,
80
}
81

            
82
use azul_core::{
83
    dom::{FormattingContext, NodeData, NodeId, NodeType},
84
    geom::{LogicalPosition, LogicalRect, LogicalSize},
85
    styled_dom::StyledDom,
86
};
87
use azul_css::{
88
    corety::LayoutDebugMessage,
89
    css::CssPropertyValue,
90
    format_rust_code::GetHash,
91
    props::{
92
        basic::{
93
            pixel::DEFAULT_FONT_SIZE, PhysicalSize, PixelValue, PropertyContext, ResolutionContext,
94
        },
95
        layout::{
96
            LayoutDisplay, LayoutFloat, LayoutHeight, LayoutMaxHeight, LayoutMaxWidth,
97
            LayoutMinHeight, LayoutMinWidth, LayoutOverflow, LayoutPosition, LayoutWidth,
98
            LayoutWritingMode,
99
        },
100
        property::{CssProperty, CssPropertyType},
101
        style::{StyleTextAlign, StyleWhiteSpace},
102
    },
103
};
104
use taffy::{Cache as TaffyCache, Layout, LayoutInput, LayoutOutput};
105

            
106
#[cfg(feature = "text_layout")]
107
use crate::text3;
108
use crate::{
109
    debug_log,
110
    font::parsed::ParsedFont,
111
    font_traits::{FontLoaderTrait, ParsedFontTrait, UnifiedLayout},
112
    solver3::{
113
        geometry::{BoxProps, IntrinsicSizes, PositionedRectangle},
114
        getters::{
115
            get_css_height, get_css_max_height, get_css_max_width, get_css_min_height,
116
            get_css_min_width, get_css_width, get_direction_property as get_direction,
117
            get_display_property, get_float, get_overflow_x,
118
            get_overflow_y, get_position, get_text_align,
119
            get_text_orientation_property as get_text_orientation,
120
            get_white_space_property, get_writing_mode, MultiValue,
121
        },
122
        scrollbar::ScrollbarRequirements,
123
        LayoutContext, Result,
124
    },
125
    text3::cache::AvailableSpace,
126
};
127

            
128
/// Represents the invalidation state of a layout node.
129
///
130
/// The states are ordered by severity, allowing for easy "upgrading" of the dirty state.
131
/// A node marked for `Layout` does not also need to be marked for `Paint`.
132
///
133
/// Because this enum derives `PartialOrd` and `Ord`, you can directly compare variants:
134
///
135
/// - `DirtyFlag::Layout > DirtyFlag::Paint` is `true`
136
/// - `DirtyFlag::Paint >= DirtyFlag::None` is `true`
137
/// - `DirtyFlag::Paint < DirtyFlag::Layout` is `true`
138
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
139
pub enum DirtyFlag {
140
    /// The node's layout is valid and no repaint is needed. This is the "clean" state.
141
    #[default]
142
    None,
143
    /// The node's geometry is valid, but its appearance (e.g., color) has changed.
144
    /// Requires a display list update only.
145
    Paint,
146
    /// The node's geometry (size or position) is invalid.
147
    /// Requires a full layout pass and a display list update.
148
    Layout,
149
}
150

            
151
/// A hash that represents the content and style of a node PLUS all of its descendants.
152
/// If two SubtreeHashes are equal, their entire subtrees are considered identical for layout
153
/// purposes.
154
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Hash)]
155
pub struct SubtreeHash(pub u64);
156

            
157
/// Per-item metrics cached from the last IFC layout.
158
///
159
/// These metrics enable incremental IFC relayout (Phase 2 optimization):
160
/// when a single inline item changes, we can check whether its advance width
161
/// changed and potentially skip full line-breaking for unaffected lines.
162
///
163
/// Index in `CachedInlineLayout::item_metrics` matches the item order in
164
/// `UnifiedLayout::items`.
165
#[derive(Debug, Clone)]
166
pub struct InlineItemMetrics {
167
    /// The DOM NodeId of the source node for this item (for dirty checking).
168
    /// `None` for generated content (list markers, hyphens, etc.)
169
    pub source_node_id: Option<NodeId>,
170
    /// Advance width of this item (glyph run width, inline-block width, etc.)
171
    pub advance_width: f32,
172
    /// Advance height contribution from this item to its line box.
173
    pub line_height_contribution: f32,
174
    /// Whether this item can participate in line breaking.
175
    /// `false` for items inside `white-space: nowrap` or `white-space: pre`.
176
    pub can_break: bool,
177
    /// Which line this item was placed on (0-indexed).
178
    pub line_index: u32,
179
    /// X offset within its line.
180
    pub x_offset: f32,
181
}
182

            
183
/// Cached inline layout result with the constraints used to compute it.
184
///
185
/// This structure solves a fundamental architectural problem: inline layouts
186
/// (text wrapping, inline-block positioning) depend on the available width.
187
/// Different layout phases may compute the layout with different widths:
188
///
189
/// 1. **Min-content measurement**: width = MinContent (effectively 0)
190
/// 2. **Max-content measurement**: width = MaxContent (effectively infinite)
191
/// 3. **Final layout**: width = Definite(actual_column_width)
192
///
193
/// Without tracking which constraints were used, a cached result from phase 1
194
/// would incorrectly be reused in phase 3, causing text to wrap at the wrong
195
/// positions (the root cause of table cell width bugs).
196
///
197
/// By storing the constraints alongside the result, we can:
198
/// - Invalidate the cache when constraints change
199
/// - Keep multiple cached results for different constraint types if needed
200
/// - Ensure the final render always uses a layout computed with correct widths
201
#[derive(Debug, Clone)]
202
pub struct CachedInlineLayout {
203
    /// The computed inline layout
204
    pub layout: Arc<UnifiedLayout>,
205
    /// The available width constraint used to compute this layout.
206
    /// This is the key for cache validity checking.
207
    /// +spec:writing-modes:1dcba2 - "available width" (CSS2.1) = auto size in inline axis
208
    pub available_width: AvailableSpace,
209
    /// Whether this layout was computed with float exclusions.
210
    /// Float-aware layouts should not be overwritten by non-float layouts.
211
    pub has_floats: bool,
212
    /// The full constraints used to compute this layout.
213
    /// Used for quick relayout after text edits without rebuilding from CSS.
214
    pub constraints: Option<UnifiedConstraints>,
215
    /// Per-item metrics for incremental IFC relayout (Phase 2).
216
    ///
217
    /// Each entry corresponds to one `PositionedItem` in `layout.items`.
218
    /// These metrics enable the IFC relayout decision tree:
219
    /// - Check if a dirty node's advance_width changed → skip repositioning if not
220
    /// - Use `can_break` + `line_index` for the nowrap fast path
221
    /// - Use `x_offset` for shifting subsequent items without full line-breaking
222
    pub item_metrics: Vec<InlineItemMetrics>,
223
    /// Cached line break boundaries for incremental relayout.
224
    /// Enables checking if a width change fits on the same line without
225
    /// re-running the full line-breaking algorithm.
226
    pub line_breaks: Option<crate::text3::cache::CachedLineBreaks>,
227
}
228

            
229
impl CachedInlineLayout {
230
    /// Creates a new cached inline layout.
231
105
    pub fn new(
232
105
        layout: Arc<UnifiedLayout>,
233
105
        available_width: AvailableSpace,
234
105
        has_floats: bool,
235
105
    ) -> Self {
236
105
        let item_metrics = Self::extract_item_metrics(&layout);
237
105
        Self {
238
105
            layout,
239
105
            available_width,
240
105
            has_floats,
241
105
            constraints: None,
242
105
            item_metrics,
243
105
            line_breaks: None,
244
105
        }
245
105
    }
246

            
247
    /// Creates a new cached inline layout with full constraints.
248
6265
    pub fn new_with_constraints(
249
6265
        layout: Arc<UnifiedLayout>,
250
6265
        available_width: AvailableSpace,
251
6265
        has_floats: bool,
252
6265
        constraints: UnifiedConstraints,
253
6265
    ) -> Self {
254
6265
        let item_metrics = Self::extract_item_metrics(&layout);
255
6265
        let available_width_px = match available_width {
256
6195
            AvailableSpace::Definite(w) => w,
257
70
            _ => f32::MAX,
258
        };
259
6265
        let line_breaks = Some(crate::text3::cache::extract_line_breaks(
260
6265
            &layout.items, available_width_px,
261
6265
        ));
262
6265
        Self {
263
6265
            layout,
264
6265
            available_width,
265
6265
            has_floats,
266
6265
            constraints: Some(constraints),
267
6265
            item_metrics,
268
6265
            line_breaks,
269
6265
        }
270
6265
    }
271

            
272
    /// Extracts per-item metrics from a computed `UnifiedLayout`.
273
    ///
274
    /// This is called automatically by the constructors. The metrics
275
    /// enable incremental IFC relayout in Phase 2c/2d by providing
276
    /// cached advance widths, line assignments, and break information
277
    /// for each positioned item.
278
6370
    fn extract_item_metrics(layout: &UnifiedLayout) -> Vec<InlineItemMetrics> {
279
        use crate::text3::cache::{ShapedItem, get_item_vertical_metrics_approx};
280

            
281
61915
        layout.items.iter().map(|positioned_item| {
282
61915
            let bounds = positioned_item.item.bounds();
283
61915
            let (ascent, descent) = get_item_vertical_metrics_approx(&positioned_item.item);
284

            
285
61915
            let source_node_id = match &positioned_item.item {
286
61495
                ShapedItem::Cluster(c) => c.source_node_id,
287
                // Objects (inline-blocks, images) and other generated items
288
                // don't expose source_node_id directly on ShapedItem.
289
                // Phase 2c will refine this via the ContentIndex mapping.
290
                ShapedItem::Object { .. }
291
                | ShapedItem::CombinedBlock { .. }
292
                | ShapedItem::Tab { .. }
293
420
                | ShapedItem::Break { .. } => None,
294
            };
295

            
296
            // For Phase 2a, default can_break = true for all items.
297
            // Phase 2c will refine this by checking the white-space property
298
            // on the IFC root's style or the item's own style context.
299
            // (Note: text3::StyleProperties doesn't carry white-space;
300
            //  that's resolved at the IFC/BFC boundary level.)
301
61915
            let can_break = !matches!(&positioned_item.item, ShapedItem::Break { .. });
302

            
303
61915
            InlineItemMetrics {
304
61915
                source_node_id,
305
61915
                advance_width: bounds.width,
306
61915
                line_height_contribution: ascent + descent,
307
61915
                can_break,
308
61915
                line_index: positioned_item.line_index as u32,
309
61915
                x_offset: positioned_item.position.x,
310
61915
            }
311
61915
        }).collect()
312
6370
    }
313

            
314
    /// Checks if this cached layout is valid for the given constraints.
315
    ///
316
    /// A cached layout is valid if:
317
    /// 1. The available width matches (definite widths must be equal, or both are the same
318
    ///    indefinite type)
319
    /// 2. OR the new request doesn't have floats but the cached one does (keep float-aware layout)
320
    ///
321
    /// The second condition preserves float-aware layouts, which are more "correct" than
322
    /// non-float layouts and shouldn't be overwritten.
323
    pub fn is_valid_for(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
324
        // If we have a float-aware layout and the new request doesn't have floats,
325
        // keep the float-aware layout (it's more accurate)
326
        if self.has_floats && !new_has_floats {
327
            // But only if the width constraint type matches
328
            return self.width_constraint_matches(new_width);
329
        }
330

            
331
        // Otherwise, require exact width match
332
        self.width_constraint_matches(new_width)
333
    }
334

            
335
    /// Tolerance for comparing definite layout widths (in logical pixels).
336
    /// Sub-pixel differences below this threshold are treated as identical
337
    /// to avoid unnecessary relayout from floating-point rounding.
338
    const LAYOUT_WIDTH_EPSILON: f32 = 0.1;
339

            
340
    /// Checks if the width constraint matches.
341
    fn width_constraint_matches(&self, new_width: AvailableSpace) -> bool {
342
        match (self.available_width, new_width) {
343
            // Definite widths must match within a small epsilon
344
            (AvailableSpace::Definite(old), AvailableSpace::Definite(new)) => {
345
                (old - new).abs() < Self::LAYOUT_WIDTH_EPSILON
346
            }
347
            // MinContent matches MinContent
348
            (AvailableSpace::MinContent, AvailableSpace::MinContent) => true,
349
            // MaxContent matches MaxContent
350
            (AvailableSpace::MaxContent, AvailableSpace::MaxContent) => true,
351
            // Different constraint types don't match
352
            _ => false,
353
        }
354
    }
355

            
356
    /// Determines if this cached layout should be replaced by a new layout.
357
    ///
358
    /// Returns true if the new layout should replace this one.
359
    pub fn should_replace_with(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
360
        // Always replace if we gain float information
361
        if new_has_floats && !self.has_floats {
362
            return true;
363
        }
364

            
365
        // Replace if width constraint changed
366
        !self.width_constraint_matches(new_width)
367
    }
368

            
369
    /// Returns a reference to the inner UnifiedLayout.
370
    ///
371
    /// This is a convenience method for code that only needs the layout data
372
    /// and doesn't care about the caching metadata.
373
    #[inline]
374
10045
    pub fn get_layout(&self) -> &Arc<UnifiedLayout> {
375
10045
        &self.layout
376
10045
    }
377

            
378
    /// Returns a clone of the inner Arc<UnifiedLayout>.
379
    ///
380
    /// This is useful for APIs that need to return an owned reference
381
    /// to the layout without exposing the caching metadata.
382
    #[inline]
383
    pub fn clone_layout(&self) -> Arc<UnifiedLayout> {
384
        self.layout.clone()
385
    }
386
}
387

            
388
/// A layout tree node representing the CSS box model.
389
///
390
/// ## Memory Layout Optimization (`#[repr(C)]`)
391
///
392
/// Fields are ordered by access frequency (hottest first) to maximize CPU
393
/// cache line utilization during tree traversal. With `#[repr(C)]`, the
394
/// compiler preserves this ordering. The 6 hottest fields (~140 bytes)
395
/// occupy the first 2-3 cache lines (64 bytes each), which are loaded
396
/// first by the hardware prefetcher.
397
///
398
/// | Tier   | Fields                                  | ~Bytes | Accesses |
399
/// |--------|-----------------------------------------|--------|----------|
400
/// | HOT    | box_props, dom_node_id, children,       |  ~140  |  410+    |
401
/// |        | used_size, formatting_context, parent    |        |          |
402
/// | WARM   | intrinsic_sizes..computed_style          |  ~220  |  ~80     |
403
/// | COLD   | dirty_flag..is_anonymous                 |  ~190  |  ~20     |
404
///
405
/// Note: An absolute position is a final paint-time value and shouldn't be
406
/// cached on the node itself, as it can change even if the node's
407
/// layout is clean (e.g., if a sibling changes size). We will calculate
408
/// it in a separate map.
409
#[derive(Debug, Clone)]
410
#[repr(C)]
411
pub struct LayoutNode {
412
    // ── HOT tier: accessed on every node in every layout pass ────────────
413
    // These fields should fit in the first 2-3 cache lines (~128-192 bytes).
414

            
415
    /// The resolved box model properties (margin, border, padding)
416
    /// in logical pixels. Cached after first resolution.
417
    /// (148 accesses — hottest field)
418
    pub box_props: BoxProps,
419
    /// Reference back to the original DOM node (None for anonymous boxes)
420
    /// (111 accesses)
421
    pub dom_node_id: Option<NodeId>,
422
    /// Children indices in the layout tree
423
    /// (53 accesses)
424
    pub children: Vec<usize>,
425
    /// The size used during the last layout pass.
426
    /// (43 accesses)
427
    pub used_size: Option<LogicalSize>,
428
    /// The formatting context this node establishes or participates in.
429
    /// (30 accesses)
430
    pub formatting_context: FormattingContext,
431
    /// Parent index (None for root)
432
    /// (25 accesses)
433
    pub parent: Option<usize>,
434

            
435
    // ── WARM tier: frequently accessed but not on every node ─────────────
436

            
437
    /// Cached intrinsic sizes (min-content, max-content, etc.)
438
    /// (16 accesses — sizing pass only)
439
    pub intrinsic_sizes: Option<IntrinsicSizes>,
440
    // +spec:display-property:af3a89 - alignment baseline for inline-level boxes
441
    /// The baseline of this box, if applicable, measured from its content-box top edge.
442
    /// (14 accesses — IFC/table alignment)
443
    pub baseline: Option<f32>,
444
    /// Cached inline layout result with the constraints used to compute it.
445
    ///
446
    /// This field stores both the computed layout AND the constraints (available width,
447
    /// float state) under which it was computed. This is essential for correctness:
448
    /// 
449
    /// - Table cells are measured multiple times with different widths
450
    /// - Min-content/max-content intrinsic sizing uses special constraint values
451
    /// - The final layout must use the actual available width, not a measurement width
452
    ///
453
    /// By tracking the constraints, we avoid the bug where a min-content measurement
454
    /// (with width=0) would be incorrectly reused for final rendering.
455
    /// (13 accesses — IFC roots / table cells)
456
    pub inline_layout_result: Option<CachedInlineLayout>,
457
    /// Cached scrollbar information (calculated during layout)
458
    /// Used to determine if scrollbars appeared/disappeared requiring reflow
459
    /// (12 accesses — scrollable containers only)
460
    pub scrollbar_info: Option<ScrollbarRequirements>,
461
    /// The position of this node *relative to its parent's content box*.
462
    /// (9 accesses — positioning pass)
463
    pub relative_position: Option<LogicalPosition>,
464
    /// The actual content size (children overflow size) for scrollable containers.
465
    /// This is the size of all content that might need to be scrolled, which can
466
    /// be larger than `used_size` when content overflows the container.
467
    /// (7 accesses — scrollable containers)
468
    pub overflow_content_size: Option<LogicalSize>,
469
    /// Cache for Taffy layout computations for this node.
470
    /// (6 accesses — Taffy bridge)
471
    pub taffy_cache: TaffyCache,
472
    /// Pre-computed CSS properties needed during layout.
473
    /// Computed once during layout tree build to avoid repeated style lookups.
474
    /// (5 accesses — cache.rs only)
475
    pub computed_style: ComputedLayoutStyle,
476
    /// Pseudo-element type (::marker, ::before, ::after) if this node is a pseudo-element
477
    /// (5 accesses — pseudo-elements only)
478
    pub pseudo_element: Option<PseudoElement>,
479
    /// Escaped top margin (CSS 2.1 margin collapsing)
480
    /// If this BFC's first child's top margin "escaped" the BFC, this contains
481
    /// the collapsed margin that should be applied by the parent.
482
    /// (4 accesses — BFC margin collapsing)
483
    pub escaped_top_margin: Option<f32>,
484
    /// Escaped bottom margin (CSS 2.1 margin collapsing)  
485
    /// If this BFC's last child's bottom margin "escaped" the BFC, this contains
486
    /// the collapsed margin that should be applied by the parent.
487
    /// (4 accesses)
488
    pub escaped_bottom_margin: Option<f32>,
489
    /// Parent's formatting context (needed to determine if stretch applies)
490
    /// (4 accesses — flex/grid children)
491
    pub parent_formatting_context: Option<FormattingContext>,
492
    /// If this node participates in an IFC (is inline content like text),
493
    /// stores the reference back to the IFC root and the run index.
494
    /// This allows text nodes to find their layout data in the parent's IFC.
495
    /// (3 accesses — text nodes only)
496
    pub ifc_membership: Option<IfcMembership>,
497
    /// The layout tree index of this node's containing block.
498
    /// - For abs-pos elements: nearest positioned (non-static) ancestor
499
    /// - For fixed elements: root / None (viewport)
500
    /// - For normal-flow: parent (None = implicit)
501
    /// Used for clip exemption: abs-pos elements whose containing block
502
    /// is above an overflow clipper should not be clipped.
503
    pub containing_block_index: Option<usize>,
504

            
505
    // ── COLD tier: construction / reconciliation / debugging only ────────
506

            
507
    /// Type of anonymous box (if applicable)
508
    /// (2 accesses)
509
    pub anonymous_type: Option<AnonymousBoxType>,
510
    /// Multi-field fingerprint of this node's data (style, text, etc.)
511
    /// for granular change detection during reconciliation.
512
    /// (2 accesses — reconciliation only)
513
    pub node_data_fingerprint: NodeDataFingerprint,
514
    /// A hash of this node's data and all of its descendants. Used for
515
    /// fast reconciliation.
516
    /// (9 accesses — all in cache.rs reconciliation)
517
    pub subtree_hash: SubtreeHash,
518
    /// Dirty flags to track what needs recalculation.
519
    /// (7 accesses — reconciliation setup)
520
    pub dirty_flag: DirtyFlag,
521
    /// Unresolved box model properties (raw CSS values).
522
    /// These are resolved lazily during layout when containing block is known.
523
    /// (1 access — initial resolution only)
524
    pub unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps,
525
    /// If this node is an IFC root, stores the IFC ID.
526
    /// Used to identify which IFC this node's `inline_layout_result` belongs to.
527
    /// (1 access — IFC creation only)
528
    pub ifc_id: Option<IfcId>,
529
}
530

            
531
/// Pre-computed CSS properties needed during layout.
532
/// 
533
/// This struct stores resolved CSS values that are frequently accessed during
534
/// layout calculations. By computing these once during layout tree construction,
535
/// we avoid O(n * m) style lookups where n = nodes and m = layout passes.
536
///
537
/// All values are resolved to their final form (no 'inherit', 'initial', etc.)
538
#[derive(Debug, Clone, Default)]
539
pub struct ComputedLayoutStyle {
540
    /// CSS `display` property
541
    pub display: LayoutDisplay,
542
    /// CSS `position` property
543
    pub position: LayoutPosition,
544
    /// CSS `float` property
545
    pub float: LayoutFloat,
546
    /// CSS `overflow-x` property
547
    pub overflow_x: LayoutOverflow,
548
    /// CSS `overflow-y` property
549
    pub overflow_y: LayoutOverflow,
550
    /// CSS `writing-mode` property
551
    pub writing_mode: azul_css::props::layout::LayoutWritingMode,
552
    /// CSS `direction` property (ltr/rtl)
553
    pub direction: azul_css::props::style::StyleDirection,
554
    /// CSS `text-orientation` property (for vertical writing modes)
555
    pub text_orientation: azul_css::props::style::effects::StyleTextOrientation,
556
    /// CSS `width` property (None = auto)
557
    pub width: Option<azul_css::props::layout::LayoutWidth>,
558
    /// CSS `height` property (None = auto)
559
    pub height: Option<azul_css::props::layout::LayoutHeight>,
560
    /// CSS `min-width` property
561
    pub min_width: Option<azul_css::props::layout::LayoutMinWidth>,
562
    /// CSS `min-height` property
563
    pub min_height: Option<azul_css::props::layout::LayoutMinHeight>,
564
    /// CSS `max-width` property
565
    pub max_width: Option<azul_css::props::layout::LayoutMaxWidth>,
566
    /// CSS `max-height` property
567
    pub max_height: Option<azul_css::props::layout::LayoutMaxHeight>,
568
    /// CSS `text-align` property
569
    pub text_align: azul_css::props::style::StyleTextAlign,
570
}
571

            
572
// Note: LayoutNode methods that cross hot/warm/cold boundaries have been
573
// moved to LayoutTree methods (resolve_box_props, get_content_size).
574

            
575
/// CSS pseudo-elements that can be generated
576
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
577
pub enum PseudoElement {
578
    /// ::marker pseudo-element for list items
579
    Marker,
580
    /// ::before pseudo-element
581
    Before,
582
    /// ::after pseudo-element
583
    After,
584
}
585

            
586
// +spec:display-property:b7f4bf - anonymous inline/block boxes are both called "anonymous boxes"
587
/// Types of anonymous boxes that can be generated
588
// +spec:display-property:ae4f16 - anonymous boxes are treated as descendants alongside pseudo-elements
589
#[derive(Debug, Clone, Copy, PartialEq)]
590
pub enum AnonymousBoxType {
591
    /// Anonymous block box wrapping inline content
592
    InlineWrapper,
593
    /// Anonymous box for a list item marker (bullet or number)
594
    /// DEPRECATED: Use PseudoElement::Marker instead
595
    ListItemMarker,
596
    /// Anonymous table wrapper
597
    TableWrapper,
598
    /// Anonymous table row group (tbody)
599
    TableRowGroup,
600
    /// Anonymous table row
601
    TableRow,
602
    /// Anonymous table cell
603
    TableCell,
604
}
605

            
606
// =============================================================================
607
// SoA (struct-of-arrays) layout node split for cache performance
608
// =============================================================================
609

            
610
/// Hot layout node fields — accessed on every node in every layout pass.
611
///
612
/// Stored in a separate `Vec` for cache locality. At ~100 bytes per node,
613
/// 1000 nodes fit in ~100 KB (L2 cache), vs ~550 KB with the monolithic struct.
614
#[derive(Debug, Clone)]
615
pub struct LayoutNodeHot {
616
    /// The resolved box model properties (margin, border, padding)
617
    /// Stored in packed i16×10 encoding to reduce cache footprint.
618
    /// Use `box_props.unpack()` to get f32 `ResolvedBoxProps` for computation.
619
    pub box_props: crate::solver3::geometry::PackedBoxProps,
620
    /// Reference back to the original DOM node (None for anonymous boxes)
621
    pub dom_node_id: Option<NodeId>,
622
    /// The size used during the last layout pass.
623
    pub used_size: Option<LogicalSize>,
624
    /// The formatting context this node establishes or participates in.
625
    pub formatting_context: FormattingContext,
626
    /// Parent index (None for root)
627
    pub parent: Option<usize>,
628
}
629

            
630
/// Warm layout node fields — accessed frequently but not on every node.
631
///
632
/// Stored in a separate `Vec`. These fields are accessed during specific
633
/// layout phases (sizing, IFC, table alignment) but not during the main
634
/// constraint-solving loop.
635
#[derive(Debug, Clone, Default)]
636
pub struct LayoutNodeWarm {
637
    /// Cached intrinsic sizes (min-content, max-content, etc.)
638
    pub intrinsic_sizes: Option<IntrinsicSizes>,
639
    /// The baseline of this box, measured from its content-box top edge.
640
    pub baseline: Option<f32>,
641
    /// Cached inline layout result with the constraints used to compute it.
642
    pub inline_layout_result: Option<CachedInlineLayout>,
643
    /// Cached scrollbar information
644
    pub scrollbar_info: Option<ScrollbarRequirements>,
645
    /// The position relative to parent's content box.
646
    pub relative_position: Option<LogicalPosition>,
647
    /// The actual content size for scrollable containers.
648
    pub overflow_content_size: Option<LogicalSize>,
649
    /// Cache for Taffy layout computations.
650
    pub taffy_cache: TaffyCache,
651
    /// Pre-computed CSS properties needed during layout.
652
    pub computed_style: ComputedLayoutStyle,
653
    /// Pseudo-element type if this node is a pseudo-element
654
    pub pseudo_element: Option<PseudoElement>,
655
    /// Escaped top margin (CSS 2.1 margin collapsing)
656
    pub escaped_top_margin: Option<f32>,
657
    /// Escaped bottom margin (CSS 2.1 margin collapsing)
658
    pub escaped_bottom_margin: Option<f32>,
659
    /// Parent's formatting context
660
    pub parent_formatting_context: Option<FormattingContext>,
661
    /// IFC membership for text nodes
662
    pub ifc_membership: Option<IfcMembership>,
663
    /// Containing block index for clip exemption
664
    pub containing_block_index: Option<usize>,
665
}
666

            
667
/// Cold layout node fields — construction / reconciliation / debugging only.
668
///
669
/// Stored in a separate `Vec`. These fields are rarely accessed during layout;
670
/// mostly used during tree construction, reconciliation, and dirty tracking.
671
#[derive(Debug, Clone)]
672
pub struct LayoutNodeCold {
673
    /// Type of anonymous box (if applicable)
674
    pub anonymous_type: Option<AnonymousBoxType>,
675
    /// Multi-field fingerprint for granular change detection.
676
    pub node_data_fingerprint: NodeDataFingerprint,
677
    /// Hash of this node's data + all descendants.
678
    pub subtree_hash: SubtreeHash,
679
    /// Dirty flags for recalculation tracking.
680
    pub dirty_flag: DirtyFlag,
681
    /// Unresolved box model properties (raw CSS values).
682
    pub unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps,
683
    /// IFC ID if this node is an IFC root.
684
    pub ifc_id: Option<IfcId>,
685
}
686

            
687
impl Default for LayoutNodeCold {
688
    fn default() -> Self {
689
        Self {
690
            anonymous_type: None,
691
            node_data_fingerprint: NodeDataFingerprint::default(),
692
            subtree_hash: SubtreeHash::default(),
693
            dirty_flag: DirtyFlag::default(),
694
            unresolved_box_props: Default::default(),
695
            ifc_id: None,
696
        }
697
    }
698
}
699

            
700
impl LayoutNode {
701
    /// Split this full layout node into hot/warm/cold components.
702
    /// Used during `LayoutTreeBuilder::build()` to create the SoA layout.
703
25515
    pub fn split(self) -> (LayoutNodeHot, LayoutNodeWarm, LayoutNodeCold) {
704
25515
        (
705
25515
            LayoutNodeHot {
706
25515
                box_props: crate::solver3::geometry::PackedBoxProps::pack(&self.box_props),
707
25515
                dom_node_id: self.dom_node_id,
708
25515
                used_size: self.used_size,
709
25515
                formatting_context: self.formatting_context,
710
25515
                parent: self.parent,
711
25515
            },
712
25515
            LayoutNodeWarm {
713
25515
                intrinsic_sizes: self.intrinsic_sizes,
714
25515
                baseline: self.baseline,
715
25515
                inline_layout_result: self.inline_layout_result,
716
25515
                scrollbar_info: self.scrollbar_info,
717
25515
                relative_position: self.relative_position,
718
25515
                overflow_content_size: self.overflow_content_size,
719
25515
                taffy_cache: self.taffy_cache,
720
25515
                computed_style: self.computed_style,
721
25515
                pseudo_element: self.pseudo_element,
722
25515
                escaped_top_margin: self.escaped_top_margin,
723
25515
                escaped_bottom_margin: self.escaped_bottom_margin,
724
25515
                parent_formatting_context: self.parent_formatting_context,
725
25515
                ifc_membership: self.ifc_membership,
726
25515
                containing_block_index: self.containing_block_index,
727
25515
            },
728
25515
            LayoutNodeCold {
729
25515
                anonymous_type: self.anonymous_type,
730
25515
                node_data_fingerprint: self.node_data_fingerprint,
731
25515
                subtree_hash: self.subtree_hash,
732
25515
                dirty_flag: self.dirty_flag,
733
25515
                unresolved_box_props: self.unresolved_box_props,
734
25515
                ifc_id: self.ifc_id,
735
25515
            },
736
25515
        )
737
25515
    }
738
}
739

            
740
/// The complete layout tree structure.
741
///
742
/// Uses a struct-of-arrays (SoA) layout for cache performance:
743
/// - `nodes` (hot): accessed on every node in every layout pass
744
/// - `warm`: accessed during specific layout phases
745
/// - `cold`: construction / reconciliation only
746
#[derive(Debug, Clone)]
747
pub struct LayoutTree {
748
    /// Hot layout data — box props, parent, used_size, formatting context
749
    pub nodes: Vec<LayoutNodeHot>,
750
    /// Warm layout data — intrinsic sizes, baseline, inline layout, etc.
751
    pub warm: Vec<LayoutNodeWarm>,
752
    /// Cold layout data — dirty flags, fingerprints, reconciliation data
753
    pub cold: Vec<LayoutNodeCold>,
754
    /// Root node index
755
    pub root: usize,
756
    /// Mapping from DOM node IDs to layout node indices
757
    // BTreeMap (not HashMap): std HashMap's RandomState hasher needs an RNG seed
758
    // that isn't available in the remill-lifted wasm (no getrandom), so inserts
759
    // silently no-op there — dom_to_layout came back empty (node mapping lost,
760
    // get_node_size/position returned None → 0-rects). BTreeMap is deterministic,
761
    // matches the rest of azul-core, and lifts reliably (M12.7).
762
    pub dom_to_layout: BTreeMap<NodeId, Vec<usize>>,
763
    /// Flat arena holding all children indices contiguously.
764
    pub children_arena: Vec<usize>,
765
    /// Per-node (start, len) into `children_arena`. Indexed by node index.
766
    pub children_offsets: Vec<(u32, u32)>,
767
    /// Per-node bit: this node or any descendant establishes a shrink-to-fit
768
    /// (STF) context whose sizing algorithm reads children's intrinsic sizes
769
    /// (flex/grid/table/inline-block containers, floats, or abspos elements).
770
    ///
771
    /// If `subtree_needs_intrinsic[i]` is false AND no ancestor of `i` is STF
772
    /// either, the intrinsic sizing pass can skip the entire subtree — nothing
773
    /// will ever read those values. This is the static-DOM optimization from
774
    /// §58 Win #3 (the "safely re-enabled Fix C").
775
    ///
776
    /// Computed once at tree build time in `generate_layout_tree`. An empty
777
    /// vec means "assume every subtree needs intrinsics" (safe fallback for
778
    /// code paths that construct `LayoutTree` without going through the
779
    /// builder — currently none, but preserves the invariant for tests).
780
    pub subtree_needs_intrinsic: Vec<bool>,
781
}
782

            
783
/// Approximate per-field heap-byte breakdown of a [`LayoutTree`].
784
#[derive(Debug, Clone, Default)]
785
pub struct LayoutTreeMemoryReport {
786
    pub node_count: usize,
787
    pub hot_bytes: usize,
788
    pub warm_bytes: usize,
789
    pub warm_inline_layout_bytes: usize,
790
    pub warm_taffy_cache_bytes: usize,
791
    pub cold_bytes: usize,
792
    pub dom_to_layout_bytes: usize,
793
    pub children_arena_bytes: usize,
794
    pub children_offsets_bytes: usize,
795
}
796

            
797
impl LayoutTreeMemoryReport {
798
    pub fn total_bytes(&self) -> usize {
799
        self.hot_bytes
800
            + self.warm_bytes
801
            + self.warm_inline_layout_bytes
802
            + self.warm_taffy_cache_bytes
803
            + self.cold_bytes
804
            + self.dom_to_layout_bytes
805
            + self.children_arena_bytes
806
            + self.children_offsets_bytes
807
    }
808
}
809

            
810
impl LayoutTree {
811
    /// Approximate heap bytes retained by this LayoutTree.
812
    pub fn memory_report(&self) -> LayoutTreeMemoryReport {
813
        let mut report = LayoutTreeMemoryReport {
814
            node_count: self.nodes.len(),
815
            hot_bytes: self.nodes.capacity() * core::mem::size_of::<LayoutNodeHot>(),
816
            warm_bytes: self.warm.capacity() * core::mem::size_of::<LayoutNodeWarm>(),
817
            cold_bytes: self.cold.capacity() * core::mem::size_of::<LayoutNodeCold>(),
818
            children_arena_bytes: self.children_arena.capacity() * core::mem::size_of::<usize>(),
819
            children_offsets_bytes: self.children_offsets.capacity() * core::mem::size_of::<(u32, u32)>(),
820
            dom_to_layout_bytes: 0,
821
            warm_inline_layout_bytes: 0,
822
            warm_taffy_cache_bytes: 0,
823
        };
824
        // HashMap<NodeId, Vec<usize>> — approximate: (key + Vec-header) per entry
825
        // plus heap for each inner Vec.
826
        let entries = self.dom_to_layout.len();
827
        report.dom_to_layout_bytes = entries * (core::mem::size_of::<NodeId>() + core::mem::size_of::<Vec<usize>>());
828
        for v in self.dom_to_layout.values() {
829
            report.dom_to_layout_bytes += v.capacity() * core::mem::size_of::<usize>();
830
        }
831
        // Inline layout data lives behind Arc — count Arc heap-shares once
832
        // per node that has a cached layout. Counted conservatively.
833
        for w in &self.warm {
834
            if let Some(cached) = &w.inline_layout_result {
835
                // Arc<UnifiedLayout> — count the UnifiedLayout header + its items.
836
                report.warm_inline_layout_bytes += core::mem::size_of::<crate::text3::cache::UnifiedLayout>();
837
                report.warm_inline_layout_bytes += cached.layout.items.capacity()
838
                    * core::mem::size_of::<crate::text3::cache::PositionedItem>();
839
                report.warm_inline_layout_bytes += cached.item_metrics.capacity()
840
                    * core::mem::size_of::<InlineItemMetrics>();
841
                // Glyph bytes inside ShapedItem::Cluster — unbounded but bounded
842
                // per entry. Approximate by counting clusters × 32 bytes/glyph.
843
                for item in cached.layout.items.iter() {
844
                    if let crate::text3::cache::ShapedItem::Cluster(c) = &item.item {
845
                        report.warm_inline_layout_bytes += c.glyphs.capacity()
846
                            * core::mem::size_of::<crate::text3::cache::ShapedGlyph>();
847
                        report.warm_inline_layout_bytes += c.text.capacity();
848
                    }
849
                }
850
            }
851
            // Taffy cache — each slot is an Option, ~50 B empty
852
            report.warm_taffy_cache_bytes += core::mem::size_of::<TaffyCache>();
853
        }
854
        report
855
    }
856

            
857
    /// Returns the children of node `index` as a contiguous slice from the arena.
858
    #[inline]
859
365050
    pub fn children(&self, index: usize) -> &[usize] {
860
365050
        if let Some(&(start, len)) = self.children_offsets.get(index) {
861
365050
            &self.children_arena[(start as usize)..((start as usize) + (len as usize))]
862
        } else {
863
            &[]
864
        }
865
365050
    }
866

            
867
    /// Get hot layout data for a node (box_props, dom_node_id, used_size, etc.)
868
    #[inline]
869
1345925
    pub fn get(&self, index: usize) -> Option<&LayoutNodeHot> {
870
1345925
        self.nodes.get(index)
871
1345925
    }
872

            
873
    /// Get mutable hot layout data for a node.
874
    #[inline]
875
56700
    pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNodeHot> {
876
56700
        self.nodes.get_mut(index)
877
56700
    }
878

            
879
    /// Get warm layout data for a node (intrinsic_sizes, baseline, inline_layout, etc.)
880
    #[inline]
881
330365
    pub fn warm(&self, index: usize) -> Option<&LayoutNodeWarm> {
882
330365
        self.warm.get(index)
883
330365
    }
884

            
885
    /// Get mutable warm layout data for a node.
886
    #[inline]
887
148610
    pub fn warm_mut(&mut self, index: usize) -> Option<&mut LayoutNodeWarm> {
888
148610
        self.warm.get_mut(index)
889
148610
    }
890

            
891
    /// Get cold layout data for a node (dirty_flag, subtree_hash, fingerprint, etc.)
892
    #[inline]
893
36050
    pub fn cold(&self, index: usize) -> Option<&LayoutNodeCold> {
894
36050
        self.cold.get(index)
895
36050
    }
896

            
897
    /// Get mutable cold layout data for a node.
898
    #[inline]
899
20755
    pub fn cold_mut(&mut self, index: usize) -> Option<&mut LayoutNodeCold> {
900
20755
        self.cold.get_mut(index)
901
20755
    }
902

            
903
    pub fn root_node(&self) -> &LayoutNodeHot {
904
        &self.nodes[self.root]
905
    }
906

            
907
    /// Reconstruct a full `LayoutNode` from the split hot/warm/cold arrays.
908
    ///
909
    /// Used when passing node data to `LayoutTreeBuilder::clone_node_from_old()`.
910
70
    pub fn get_full_node(&self, index: usize) -> Option<LayoutNode> {
911
70
        let hot = self.nodes.get(index)?;
912
70
        let warm = self.warm.get(index).cloned().unwrap_or_default();
913
70
        let cold = self.cold.get(index).cloned().unwrap_or_default();
914
70
        let children = self.children(index).to_vec();
915
70
        Some(LayoutNode {
916
70
            box_props: hot.box_props.unpack(),
917
70
            dom_node_id: hot.dom_node_id,
918
70
            children,
919
70
            used_size: hot.used_size,
920
70
            formatting_context: hot.formatting_context.clone(),
921
70
            parent: hot.parent,
922
70
            intrinsic_sizes: warm.intrinsic_sizes,
923
70
            baseline: warm.baseline,
924
70
            inline_layout_result: warm.inline_layout_result,
925
70
            scrollbar_info: warm.scrollbar_info,
926
70
            relative_position: warm.relative_position,
927
70
            overflow_content_size: warm.overflow_content_size,
928
70
            taffy_cache: warm.taffy_cache,
929
70
            computed_style: warm.computed_style,
930
70
            pseudo_element: warm.pseudo_element,
931
70
            escaped_top_margin: warm.escaped_top_margin,
932
70
            escaped_bottom_margin: warm.escaped_bottom_margin,
933
70
            parent_formatting_context: warm.parent_formatting_context,
934
70
            ifc_membership: warm.ifc_membership,
935
70
            containing_block_index: warm.containing_block_index,
936
70
            anonymous_type: cold.anonymous_type,
937
70
            node_data_fingerprint: cold.node_data_fingerprint,
938
70
            subtree_hash: cold.subtree_hash,
939
70
            dirty_flag: cold.dirty_flag,
940
70
            unresolved_box_props: cold.unresolved_box_props,
941
70
            ifc_id: cold.ifc_id,
942
70
        })
943
70
    }
944

            
945
    /// Re-resolve box properties for a node with the actual containing block size.
946
    pub fn resolve_box_props(
947
        &mut self,
948
        node_index: usize,
949
        containing_block: LogicalSize,
950
        viewport_size: LogicalSize,
951
        element_font_size: f32,
952
        root_font_size: f32,
953
    ) {
954
        let params = crate::solver3::geometry::ResolutionParams {
955
            containing_block,
956
            viewport_size,
957
            element_font_size,
958
            root_font_size,
959
        };
960
        if let (Some(hot), Some(cold)) = (self.nodes.get_mut(node_index), self.cold.get(node_index)) {
961
            hot.box_props = crate::solver3::geometry::PackedBoxProps::pack(&cold.unresolved_box_props.resolve(&params));
962
        }
963
    }
964

            
965
    /// Marks a node and its ancestors as dirty with the given flag.
966
    pub fn mark_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
967
        if flag == DirtyFlag::None {
968
            return;
969
        }
970

            
971
        let mut current_index = Some(start_index);
972
        while let Some(index) = current_index {
973
            let cold = match self.cold.get_mut(index) {
974
                Some(c) => c,
975
                None => break,
976
            };
977
            if cold.dirty_flag >= flag {
978
                break;
979
            }
980
            cold.dirty_flag = flag;
981
            current_index = self.nodes.get(index).and_then(|n| n.parent);
982
        }
983
    }
984

            
985
    /// Marks a node and its entire subtree of descendants with the given dirty flag.
986
    pub fn mark_subtree_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
987
        if flag == DirtyFlag::None {
988
            return;
989
        }
990

            
991
        let mut stack = vec![start_index];
992
        while let Some(index) = stack.pop() {
993
            let children = self.children(index).to_vec();
994
            if let Some(cold) = self.cold.get_mut(index) {
995
                if cold.dirty_flag < flag {
996
                    cold.dirty_flag = flag;
997
                }
998
                stack.extend_from_slice(&children);
999
            }
        }
    }
    /// Resets the dirty flags of all nodes in the tree to `None` after layout is complete.
    pub fn clear_all_dirty_flags(&mut self) {
        for cold in &mut self.cold {
            cold.dirty_flag = DirtyFlag::None;
        }
    }
    /// Get inline layout for a node, navigating through IFC membership if needed.
27860
    pub fn get_inline_layout_for_node(&self, layout_index: usize) -> Option<&std::sync::Arc<UnifiedLayout>> {
27860
        let warm = self.warm.get(layout_index)?;
        // First, check if this node has its own inline_layout_result (it's an IFC root)
27860
        if let Some(cached) = &warm.inline_layout_result {
5880
            return Some(cached.get_layout());
21980
        }
        // For text nodes, check if they have ifc_membership pointing to the IFC root
21980
        if let Some(ifc_membership) = &warm.ifc_membership {
4165
            let ifc_root_warm = self.warm.get(ifc_membership.ifc_root_layout_index)?;
4165
            if let Some(cached) = &ifc_root_warm.inline_layout_result {
4165
                return Some(cached.get_layout());
            }
17815
        }
17815
        None
27860
    }
    /// Get the content size of a node (for scrollbar calculations).
36750
    pub fn get_content_size(&self, index: usize) -> LogicalSize {
36750
        let warm = match self.warm.get(index) {
36750
            Some(w) => w,
            None => return LogicalSize::default(),
        };
36750
        if let Some(content_size) = warm.overflow_content_size {
29505
            return content_size;
7245
        }
7245
        let hot = match self.nodes.get(index) {
7245
            Some(h) => h,
            None => return LogicalSize::default(),
        };
7245
        let mut content_size = hot.used_size.unwrap_or_default();
7245
        if let Some(ref cached_layout) = warm.inline_layout_result {
            let text_layout = &cached_layout.layout;
            let mut max_x: f32 = 0.0;
            let mut max_y: f32 = 0.0;
            for positioned_item in &text_layout.items {
                let item_bounds = positioned_item.item.bounds();
                max_x = max_x.max(positioned_item.position.x + item_bounds.width);
                max_y = max_y.max(positioned_item.position.y + item_bounds.height);
            }
            content_size.width = content_size.width.max(max_x);
            content_size.height = content_size.height.max(max_y);
7245
        }
7245
        content_size
36750
    }
}
/// Generate layout tree from styled DOM with proper anonymous box generation
55
pub fn generate_layout_tree<T: ParsedFontTrait>(
55
    ctx: &mut LayoutContext<'_, T>,
55
) -> Result<LayoutTree> {
55
    let mut builder = LayoutTreeBuilder::new(ctx.viewport_size);
55
    let root_id = ctx
55
        .styled_dom
55
        .root
55
        .into_crate_internal()
55
        .unwrap_or(NodeId::ZERO);
55
    let root_index =
55
        builder.process_node(ctx.styled_dom, root_id, None, &mut ctx.debug_messages)?;
55
    let mut layout_tree = builder.build(root_index);
    // Pre-compute the STF (shrink-to-fit) subtree bitmap. This is static-DOM
    // information: whether a subtree establishes any shrink-to-fit context
    // depends only on the DOM structure + formatting context, both of which
    // are frozen from here until the next layout-tree rebuild. The intrinsic
    // sizing pass reads this to skip subtrees whose intrinsics are never
    // consumed (§58 Win #3).
55
    layout_tree.subtree_needs_intrinsic = compute_subtree_needs_intrinsic(ctx.styled_dom, &layout_tree);
55
    debug_log!(
55
        ctx,
55
        "Generated layout tree with {} nodes (incl. anonymous)",
55
        layout_tree.nodes.len()
    );
55
    Ok(layout_tree)
55
}
/// Returns true if `(dom_node_id, fc)` establishes a formatting context whose
/// sizing algorithm reads children's intrinsic sizes. Covers:
/// - flex containers (flex item sizing uses child min/max-content),
/// - grid containers (grid-track sizing likewise),
/// - tables and table cells,
/// - inline-block (its own width may be shrink-to-fit),
/// - floats and abspos elements (their `auto` width resolves to shrink-to-fit).
///
/// A `FormattingContext::Block` with a definite CSS width is NOT shrink-to-fit —
/// its inner layout gets the width top-down, so descendant intrinsics don't
/// feed back up. That's the path Fix C short-circuits.
28665
pub(crate) fn is_shrink_to_fit_context(
28665
    styled_dom: &StyledDom,
28665
    dom_node_id: Option<NodeId>,
28665
    fc: &FormattingContext,
28665
) -> bool {
    use crate::solver3::getters::{get_float, MultiValue};
    use crate::solver3::positioning::get_position_type;
    use azul_css::props::layout::{LayoutFloat, LayoutPosition};
28665
    match fc {
        FormattingContext::Flex
        | FormattingContext::Grid
        | FormattingContext::Table
1645
        | FormattingContext::InlineBlock => return true,
27020
        _ => {}
    }
27020
    let Some(dom_id) = dom_node_id else { return false; };
26845
    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
26845
    let float_val = match get_float(styled_dom, dom_id, node_state) {
26845
        MultiValue::Exact(v) => v,
        _ => LayoutFloat::None,
    };
26845
    if float_val != LayoutFloat::None {
1820
        return true;
25025
    }
25025
    let pos = get_position_type(styled_dom, Some(dom_id));
25025
    if pos == LayoutPosition::Absolute || pos == LayoutPosition::Fixed {
        // Abspos only becomes shrink-to-fit when width is `auto`.
        // Being conservative: treat as STF whenever abspos so we still
        // compute intrinsics for the auto-width case. Misses no work.
        return true;
25025
    }
25025
    false
28665
}
/// Per-node bitmap of "this node or any descendant establishes a shrink-to-fit
/// context." Post-order walk: `out[i] = self_stf(i) || any(out[child_of_i])`.
/// Layout tree nodes are built top-down (pre-order), so iterating from the end
/// visits children before parents.
1925
fn compute_subtree_needs_intrinsic(
1925
    styled_dom: &StyledDom,
1925
    tree: &LayoutTree,
1925
) -> Vec<bool> {
1925
    let n = tree.nodes.len();
1925
    let mut out = vec![false; n];
9590
    for idx in (0..n).rev() {
9590
        let hot = &tree.nodes[idx];
9590
        let self_stf = is_shrink_to_fit_context(styled_dom, hot.dom_node_id, &hot.formatting_context);
9590
        let mut any = self_stf;
9590
        if !any {
8575
            for &child in tree.children(idx) {
6895
                if out.get(child).copied().unwrap_or(false) {
1960
                    any = true;
1960
                    break;
4935
                }
            }
1015
        }
9590
        out[idx] = any;
    }
1925
    out
1925
}
/// Incrementally builds a [`LayoutTree`] from a [`StyledDom`].
///
/// Usage: create via [`LayoutTreeBuilder::new`], call [`process_node`](Self::process_node)
/// on the root DOM node, then call [`build`](Self::build) to produce the final
/// SoA-split `LayoutTree`. During `process_node`, anonymous boxes are generated
/// as required by CSS 2.2 §9.2.1.1 (inline wrappers) and §17.2.1 (table fixup).
pub struct LayoutTreeBuilder {
    nodes: Vec<LayoutNode>,
    dom_to_layout: BTreeMap<NodeId, Vec<usize>>,
    viewport_size: LogicalSize,
}
impl LayoutTreeBuilder {
4690
    pub fn new(viewport_size: LogicalSize) -> Self {
4690
        Self {
4690
            nodes: Vec::new(),
4690
            dom_to_layout: BTreeMap::new(),
4690
            viewport_size,
4690
        }
4690
    }
42070
    pub fn get(&self, index: usize) -> Option<&LayoutNode> {
42070
        self.nodes.get(index)
42070
    }
15890
    pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNode> {
15890
        self.nodes.get_mut(index)
15890
    }
    // +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
    /// Main entry point for recursively building the layout tree.
    /// This function dispatches to specialized handlers based on the node's
    /// `display` property to correctly generate anonymous boxes.
9450
    pub fn process_node(
9450
        &mut self,
9450
        styled_dom: &StyledDom,
9450
        dom_id: NodeId,
9450
        parent_idx: Option<usize>,
9450
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
9450
    ) -> Result<usize> {
9450
        let node_data = &styled_dom.node_data.as_container()[dom_id];
9450
        let node_idx = self.create_node_from_dom(styled_dom, dom_id, parent_idx, debug_messages);
9450
        let raw_display = get_display_type(styled_dom, dom_id);
        // +spec:display-property:042f56 - replaced elements with layout-internal display use inline
        // CSS Display 3 §2.4: "When the display property of a replaced element computes to
        // one of the layout-internal values, it is handled as having a used value of inline."
9450
        let raw_display = if raw_display.is_layout_internal() && is_replaced_element(node_data) {
            LayoutDisplay::Inline
        } else {
9450
            raw_display
        };
        // +spec:display-property:0b40af - display/position/float interaction per CSS 2.2 §9.7
        // +spec:display-property:ba53ba - float!=none or position!=static causes display to blockify
        // +spec:positioning:69468c - absolute/fixed blockifies the box, float computes to none
        // +spec:table-layout:cfc60a - CSS 2.2 §9.7: display/position/float interaction
        // Blockification rules (CSS Display 3 §2.7 / §2.8):
        // 1. Root element → blockify
        // 2. position:absolute or position:fixed → float computes to 'none', blockify
        // 3. float is not 'none' → blockify
        // 4. Flex/Grid children → blockify
9450
        let node_position = self.nodes.get(node_idx).map(|n| n.computed_style.position).unwrap_or_default();
9450
        let node_float = self.nodes.get(node_idx).map(|n| n.computed_style.float).unwrap_or_default();
9450
        let is_absolute_or_fixed = matches!(node_position, LayoutPosition::Absolute | LayoutPosition::Fixed);
9450
        let is_floated = node_float != LayoutFloat::None;
9450
        let is_root = parent_idx.is_none();
        // Per CSS 2.2 §9.7: if position is absolute or fixed, float computes to 'none'
9450
        if is_absolute_or_fixed && is_floated {
            if let Some(node) = self.nodes.get_mut(node_idx) {
                node.computed_style.float = LayoutFloat::None;
            }
9450
        }
9450
        let is_flex_grid_child = parent_idx
9450
            .and_then(|p| self.nodes.get(p).map(|n| matches!(n.formatting_context, FormattingContext::Flex | FormattingContext::Grid)))
9450
            .unwrap_or(false);
9450
        let display_type = crate::solver3::getters::get_computed_display(
9450
            raw_display, is_absolute_or_fixed, is_floated, is_root, is_flex_grid_child,
        );
        // If blockification changed the display type, update the node's formatting context
9450
        if display_type != raw_display {
            if let Some(node) = self.nodes.get_mut(node_idx) {
                node.computed_style.display = display_type;
                node.formatting_context = determine_formatting_context_for_display(
                    styled_dom, dom_id, display_type,
                );
            }
9450
        }
        // Compute containing block index for abs-pos clip exemption
9450
        if is_absolute_or_fixed {
            let cb_index = if matches!(node_position, LayoutPosition::Fixed) {
                // Fixed elements: containing block is the root (viewport)
                None
            } else {
                // Absolute elements: containing block is nearest positioned ancestor
                let mut ancestor = parent_idx;
                loop {
                    match ancestor {
                        Some(idx) => {
                            let pos = self.nodes.get(idx)
                                .map(|n| n.computed_style.position)
                                .unwrap_or_default();
                            if pos.is_positioned() {
                                break Some(idx);
                            }
                            ancestor = self.nodes.get(idx).and_then(|n| n.parent);
                        }
                        None => break None, // root
                    }
                }
            };
            if let Some(node) = self.nodes.get_mut(node_idx) {
                node.containing_block_index = cb_index;
            }
9450
        }
9450
        if parent_idx.is_none() {
1925
            if let Some(node) = self.nodes.get_mut(node_idx) {
1925
                if let FormattingContext::Block { ref mut establishes_new_context } = node.formatting_context {
1925
                    *establishes_new_context = true;
1925
                }
            }
7525
        }
        // +spec:display-property:1f4039 - list-item generates ::marker pseudo-element + principal box
        // +spec:display-property:2bb592 - list-item generates ::marker pseudo-element with list-style content
        // +spec:display-property:3b507e - list-item generates ::marker pseudo-element
        // +spec:display-property:a48f00 - additional boxes (marker, table wrapper) placed w.r.t. principal box
        // +spec:display-property:998063 - list-item generates principal block box + marker box
        // If this is a list-item, inject a ::marker pseudo-element as its first child
        // +spec:display-property:a42905 - list-item generates ::marker pseudo-element with list-style content, principal box outer=block inner=flow
9450
        if display_type == LayoutDisplay::ListItem {
            self.create_marker_pseudo_element(styled_dom, dom_id, node_idx);
9450
        }
        // +spec:display-contents:376f2e - display:contents removes principal box, children render normally
        // +spec:display-contents:3c7066 - display:contents strips element from formatting tree, hoists children
        // +spec:display-contents:3f4884 - replaced elements / form controls not specially handled yet (spec note: use display:none instead)
        // +spec:display-contents:4f9129 - semantic container role preserved: children promoted but DOM structure unchanged
        // +spec:display-contents:7558e8 - display:contents is rendering-time only; DOM relationships unaffected
        // +spec:display-contents:a079e3 - display:contents generates no box; children promoted to nearest non-contents ancestor (writing-mode parent lookup skips these)
        // +spec:display-contents:e202d5 - display:contents removes principal box, children render as normal
        // +spec:display-contents:6bbdf4 - display:contents preserves semantic container role (visibility context)
        // +spec:display-property:d7a8de - display:none/contents elements generate no box; anonymous box generation ignores them
        // +spec:display-property:dc2132 - display:none and display:contents control box generation
        // display:contents - element generates no box; promote children to parent
        // +spec:display-contents:61992e - element itself generates no boxes, children promoted to parent
        // +spec:display-contents:af8feb - treated as if replaced in element tree by its contents
        // +spec:display-contents:353e71 - display:contents box generation behavior
        // +spec:display-contents:b0a76b - display:contents generates no box; children promoted to parent
        // +spec:display-property:e370af - display:contents generates no box; children promoted to parent
        //
        // +spec:display-contents:852a59 - display:contents computes to display:none for replaced elements
        // +spec:display-contents:4a524e - display:contents computes to display:none on replaced elements
        // +spec:replaced-elements:af1e68 - display:contents on replaced elements has no effect (element renders normally)
        // Per CSS Display 3 §2.5 / Appendix B: replaced elements (img, canvas, embed, object,
        // audio, iframe, video, input, textarea, select, br, wbr, meter, progress)
        // and similar cannot be "un-boxed" — display:contents becomes display:none.
9450
        if display_type == LayoutDisplay::Contents && is_replaced_element(node_data) {
            // Treat as display:none — remove node from parent and skip children
            if let Some(parent) = parent_idx {
                if let Some(p) = self.nodes.get_mut(parent) {
                    p.children.retain(|&c| c != node_idx);
                }
            }
            if let Some(node) = self.nodes.get_mut(node_idx) {
                node.computed_style.display = LayoutDisplay::None;
                node.formatting_context = FormattingContext::None;
            }
            return Ok(node_idx);
9450
        }
9450
        if display_type == LayoutDisplay::Contents {
            // Remove the node we just created — it shouldn't generate a box
            if let Some(parent) = parent_idx {
                if let Some(p) = self.nodes.get_mut(parent) {
                    p.children.retain(|&c| c != node_idx);
                }
            }
            // Process children as if they belong to the parent (or root if no parent)
            let effective_parent = parent_idx.unwrap_or(node_idx);
            for child_dom_id in dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
                self.process_node(styled_dom, child_dom_id, Some(effective_parent), debug_messages)?;
            }
            return Ok(node_idx);
9450
        }
9450
        match display_type {
            LayoutDisplay::Block
            | LayoutDisplay::InlineBlock
            | LayoutDisplay::FlowRoot
            | LayoutDisplay::ListItem => {
8050
                self.process_block_children(styled_dom, dom_id, node_idx, debug_messages)?
            }
            // +spec:table-layout:d52e09 - display:table/inline-table cause element to behave like a table element
            // +spec:table-layout:360da0 - table display values cause table formatting behavior
            LayoutDisplay::Table | LayoutDisplay::InlineTable => {
                self.process_table_children(styled_dom, dom_id, node_idx, debug_messages)?
            }
            LayoutDisplay::TableRowGroup
            | LayoutDisplay::TableHeaderGroup
            | LayoutDisplay::TableFooterGroup => {
                self.process_table_row_group_children(styled_dom, dom_id, node_idx, debug_messages)?
            }
            LayoutDisplay::TableRow => {
                self.process_table_row_children(styled_dom, dom_id, node_idx, debug_messages)?
            }
            LayoutDisplay::TableColumn => {
                // +spec:table-layout:77974f - Stage 1: all children of table-column treated as display:none
                // +spec:table-layout:c8dc69 - Stage 1: remove irrelevant boxes from table-column
                // CSS 2.2 §17.2.1: "All child boxes of a 'table-column' parent are
                // treated as if they had 'display: none'." - skip all children.
            }
            LayoutDisplay::TableColumnGroup => {
                // CSS 2.2 §17.2.1: "If a child C of a 'table-column-group' parent is not
                // a 'table-column' box, then it is treated as if it had 'display: none'."
                for child_dom_id in dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
                    let child_display = get_display_type(styled_dom, child_dom_id);
                    if child_display == LayoutDisplay::TableColumn {
                        self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
                    }
                    // Non-table-column children are suppressed (treated as display:none)
                }
            }
            // Inline, TableCell, etc., have their children processed as part of their
            // formatting context layout and don't require anonymous box generation at this stage.
            // of table-internal display values is handled via blockify_flex_item_if_table_internal
            _ => {
                // +spec:display-contents:34008d - display:none elements generate no boxes; excluded from formatting structure
                // +spec:display-property:1f38b2 - display:none creates no box at all, filter from layout tree
                // +spec:display-property:eb53f7 - display:none suppresses box generation; visibility:hidden boxes still affect layout
                // Filter out display: none children - they don't participate in layout
                // +spec:display-property:d1600a - display:none suppresses box generation; visibility:hidden boxes still affect layout
                // ALSO filter out whitespace-only text nodes for Flex/Grid/etc containers
                // to prevent them from becoming unwanted anonymous items.
1400
                let children: Vec<NodeId> = dom_id
1400
                    .az_children(&styled_dom.node_hierarchy.as_container())
                    // +spec:display-property:9f02c6 - display:none elements generate no boxes
1400
                    .filter(|&child_id| {
                        // +spec:display-property:3b507e - display:none excludes subtree from box tree
490
                        if get_display_type(styled_dom, child_id) == LayoutDisplay::None {
                            return false;
490
                        }
                        // Check for whitespace-only text
490
                        let node_data = &styled_dom.node_data.as_container()[child_id];
490
                        if let NodeType::Text(text) = node_data.get_node_type() {
                            // Skip if text is empty or just whitespace
350
                            return !text.as_str().trim().is_empty();
140
                        }
140
                        true
490
                    })
1400
                    .collect();
1400
                let is_flex_or_grid = matches!(
1400
                    display_type,
                    LayoutDisplay::Flex | LayoutDisplay::InlineFlex
                    | LayoutDisplay::Grid | LayoutDisplay::InlineGrid
                );
1645
                for child_dom_id in children {
                    // +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
                    // +spec:width-calculation:59d456 - table wrapper box is block-level, establishes BFC (CSS 2.2 §17.4)
                    // the table wrapper box becomes the flex item; align-self applies to the
                    // wrapper, flex longhands apply to the inner table box, caption contents
                    // contribute to wrapper min/max-content sizes
245
                    let child_display = get_display_type(styled_dom, child_dom_id);
245
                    if is_flex_or_grid && child_display.creates_table_context() {
                        let wrapper_idx = self.create_anonymous_node(
                            node_idx,
                            AnonymousBoxType::TableWrapper,
                            FormattingContext::Block { establishes_new_context: true },
                        );
                        self.process_node(styled_dom, child_dom_id, Some(wrapper_idx), debug_messages)?;
                    } else {
245
                        let child_idx = self.process_node(styled_dom, child_dom_id, Some(node_idx), debug_messages)?;
                        // table-internal flex items are blockified, preventing anonymous table
                        // box generation (e.g. two display:table-cell flex items become two
                        // separate display:block flex items)
245
                        if is_flex_or_grid {
140
                            blockify_flex_item_if_table_internal(&mut self.nodes, child_idx);
140
                        }
                    }
                }
            }
        }
9450
        Ok(node_idx)
9450
    }
    // +spec:display-property:5572e7 - Anonymous block boxes: wrap inline runs when block container has mixed block/inline children
    // +spec:display-property:090043 - Anonymous block box properties inherited from enclosing non-anonymous box; non-inherited props get initial values
    // +spec:display-property:7b9f7a - Block-level vs inline-level classification and anonymous block box creation
    // +spec:display-property:078fe5 - Anonymous block boxes wrapping inline content in mixed block/inline contexts
    // +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
    // +spec:display-property:1fe2be - inline box construction with anonymous text interspersed with inline elements
    // +spec:display-property:be80e3 - Anonymous inline boxes: text in block containers treated as anonymous inlines, whitespace-only runs collapsed
    /// Handles children of a block-level element, creating anonymous block
    /// wrappers for consecutive runs of inline-level children if necessary.
    // +spec:display-property:b73c50 - blockify inline content by wrapping in anonymous block containers
8050
    fn process_block_children(
8050
        &mut self,
8050
        styled_dom: &StyledDom,
8050
        parent_dom_id: NodeId,
8050
        parent_idx: usize,
8050
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
8050
    ) -> Result<()> {
        // Filter out display: none children - they don't participate in layout
8050
        let children: Vec<NodeId> = parent_dom_id
8050
            .az_children(&styled_dom.node_hierarchy.as_container())
11445
            .filter(|&child_id| get_display_type(styled_dom, child_id) != LayoutDisplay::None)
8050
            .collect();
        // Debug: log which children we found
8050
        if let Some(msgs) = debug_messages.as_mut() {
8050
            msgs.push(LayoutDebugMessage::info(format!(
8050
                "[process_block_children] DOM node {} has {} children: {:?}",
8050
                parent_dom_id.index(),
8050
                children.len(),
11445
                children.iter().map(|c| c.index()).collect::<Vec<_>>()
            )));
        }
8050
        let has_block_child = children.iter().any(|&id| is_block_level(styled_dom, id));
8050
        if let Some(msgs) = debug_messages.as_mut() {
8050
            msgs.push(LayoutDebugMessage::info(format!(
8050
                "[process_block_children] has_block_child={}, children display types: {:?}",
                has_block_child,
8050
                children
8050
                    .iter()
11445
                    .map(|c| {
11445
                        let dt = get_display_type(styled_dom, *c);
11445
                        let is_block = is_block_level(styled_dom, *c);
11445
                        format!("{}:{:?}(block={})", c.index(), dt, is_block)
11445
                    })
8050
                    .collect::<Vec<_>>()
            )));
        }
8050
        if !has_block_child {
            // All children are inline, no anonymous boxes needed.
3255
            if let Some(msgs) = debug_messages.as_mut() {
3255
                msgs.push(LayoutDebugMessage::info(format!(
3255
                    "[process_block_children] All inline, processing {} children directly",
3255
                    children.len()
3255
                )));
3255
            }
4305
            for child_id in children {
1050
                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
            }
3255
            return Ok(());
4795
        }
        // Mixed block and inline content requires anonymous wrappers.
4795
        let mut inline_run = Vec::new();
15190
        for child_id in children {
10395
            if is_block_level(styled_dom, child_id) {
                // +spec:display-contents:02a534 - contiguous text sequences with no text don't generate boxes
                // End the current inline run — but skip if all nodes are whitespace-only text.
                // +spec:display-property:7d1570 - whitespace-only text that would be collapsed does not generate anonymous inline boxes
                // +spec:white-space-processing:b32f69 - whitespace-only inline runs between blocks don't generate anonymous inline boxes
                // CSS 2.1 §9.2.2.1: "White space content that would subsequently be collapsed
                // away according to the 'white-space' property does not generate any anonymous
                // inline boxes."
6090
                if !inline_run.is_empty() {
2800
                    let all_whitespace = inline_run
2800
                        .iter()
2800
                        .all(|id| is_whitespace_only_text(styled_dom, *id));
2800
                    if all_whitespace {
2730
                        if let Some(msgs) = debug_messages.as_mut() {
2730
                            msgs.push(LayoutDebugMessage::info(format!(
2730
                                "[process_block_children] Skipping whitespace-only inline run between blocks: {:?}",
2730
                                inline_run.iter().map(|c: &NodeId| c.index()).collect::<Vec<_>>()
                            )));
                        }
2730
                        inline_run.clear();
                    } else {
70
                        if let Some(msgs) = debug_messages.as_mut() {
70
                            msgs.push(LayoutDebugMessage::info(format!(
70
                                "[process_block_children] Creating anon wrapper for inline run: {:?}",
70
                                inline_run
70
                                    .iter()
70
                                    .map(|c: &NodeId| c.index())
70
                                    .collect::<Vec<_>>()
                            )));
                        }
70
                        let anon_idx = self.create_anonymous_node(
70
                            parent_idx,
70
                            AnonymousBoxType::InlineWrapper,
70
                            FormattingContext::Block {
70
                                // Anonymous wrappers are BFC roots
70
                                establishes_new_context: true,
70
                            },
                        );
70
                        for inline_child_id in inline_run.drain(..) {
70
                            self.process_node(
70
                                styled_dom,
70
                                inline_child_id,
70
                                Some(anon_idx),
70
                                debug_messages,
                            )?;
                        }
                    }
3290
                }
                // Process the block-level child directly
6090
                if let Some(msgs) = debug_messages.as_mut() {
6090
                    msgs.push(LayoutDebugMessage::info(format!(
6090
                        "[process_block_children] Processing block child DOM {}",
6090
                        child_id.index()
6090
                    )));
6090
                }
6090
                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
4305
            } else {
4305
                inline_run.push(child_id);
4305
            }
        }
        // Process any remaining inline children at the end — skip if all whitespace
4795
        if !inline_run.is_empty() {
1505
            let all_whitespace = inline_run
1505
                .iter()
1505
                .all(|id| is_whitespace_only_text(styled_dom, *id));
1505
            if all_whitespace {
1435
                if let Some(msgs) = debug_messages.as_mut() {
1435
                    msgs.push(LayoutDebugMessage::info(format!(
1435
                        "[process_block_children] Skipping trailing whitespace-only inline run: {:?}",
1435
                        inline_run.iter().map(|c| c.index()).collect::<Vec<_>>()
                    )));
                }
            } else {
70
                if let Some(msgs) = debug_messages.as_mut() {
70
                    msgs.push(LayoutDebugMessage::info(format!(
70
                        "[process_block_children] Creating anon wrapper for remaining inline run: {:?}",
70
                        inline_run.iter().map(|c| c.index()).collect::<Vec<_>>()
                    )));
                }
70
                let anon_idx = self.create_anonymous_node(
70
                    parent_idx,
70
                    AnonymousBoxType::InlineWrapper,
70
                    FormattingContext::Block {
70
                        establishes_new_context: true, // Anonymous wrappers are BFC roots
70
                    },
                );
140
                for inline_child_id in inline_run {
70
                    self.process_node(
70
                        styled_dom,
70
                        inline_child_id,
70
                        Some(anon_idx),
70
                        debug_messages,
                    )?;
                }
            }
3290
        }
4795
        Ok(())
8050
    }
    // +spec:table-layout:6bb84e - Anonymous table object generation (stages 1-3: remove irrelevant boxes, generate missing child wrappers, generate missing parents)
    // +spec:table-layout:77974f - Stage 2: generate missing child wrappers for table/inline-table
    // +spec:table-layout:c8dc69 - Stage 2: wrap non-proper children in anonymous table-row
    /// CSS 2.2 Section 17.2.1 - Anonymous box generation for tables:
    /// "If a child C of a 'table' or 'inline-table' box is not a proper table child,
    /// then generate an anonymous 'table-row' box around C and all consecutive
    /// siblings of C that are not proper table children."
    ///
    // +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
    /// Proper table children are: table-row-group, table-header-group,
    /// table-footer-group, table-row, table-column-group, table-column, table-caption.
    fn process_table_children(
        &mut self,
        styled_dom: &StyledDom,
        parent_dom_id: NodeId,
        parent_idx: usize,
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
    ) -> Result<()> {
        let parent_display = get_display_type(styled_dom, parent_dom_id);
        let mut non_proper_children = Vec::new();
        for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
            // CSS 2.2 Section 17.2.1, Stage 1: Skip whitespace-only text nodes
            if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
                continue;
            }
            let child_display = get_display_type(styled_dom, child_id);
            if is_proper_table_child(child_display) {
                // Flush any accumulated non-proper children into an anonymous table-row
                if !non_proper_children.is_empty() {
                    let anon_row_idx = self.create_anonymous_node(
                        parent_idx,
                        AnonymousBoxType::TableRow,
                        FormattingContext::TableRow,
                    );
                    for np_id in non_proper_children.drain(..) {
                        self.process_node(styled_dom, np_id, Some(anon_row_idx), debug_messages)?;
                    }
                }
                // Process proper table child directly (row, row-group, caption, etc.)
                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
            } else {
                // Non-proper table child: accumulate for wrapping
                non_proper_children.push(child_id);
            }
        }
        // Flush any remaining accumulated non-proper children
        if !non_proper_children.is_empty() {
            let anon_row_idx = self.create_anonymous_node(
                parent_idx,
                AnonymousBoxType::TableRow,
                FormattingContext::TableRow,
            );
            for np_id in non_proper_children {
                self.process_node(styled_dom, np_id, Some(anon_row_idx), debug_messages)?;
            }
        }
        Ok(())
    }
    /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
    /// "If a child C of a row group box is not a 'table-row' box, then generate
    /// an anonymous 'table-row' box around C and all consecutive siblings of C
    /// that are not 'table-row' boxes."
    fn process_table_row_group_children(
        &mut self,
        styled_dom: &StyledDom,
        parent_dom_id: NodeId,
        parent_idx: usize,
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
    ) -> Result<()> {
        let parent_display = get_display_type(styled_dom, parent_dom_id);
        let mut non_row_children = Vec::new();
        for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
            if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
                continue;
            }
            let child_display = get_display_type(styled_dom, child_id);
            if child_display == LayoutDisplay::TableRow {
                // Flush accumulated non-row children into anonymous row
                if !non_row_children.is_empty() {
                    let anon_row_idx = self.create_anonymous_node(
                        parent_idx,
                        AnonymousBoxType::TableRow,
                        FormattingContext::TableRow,
                    );
                    for nr_id in non_row_children.drain(..) {
                        self.process_node(styled_dom, nr_id, Some(anon_row_idx), debug_messages)?;
                    }
                }
                // Process table-row child directly
                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
            } else {
                non_row_children.push(child_id);
            }
        }
        // Flush remaining
        if !non_row_children.is_empty() {
            let anon_row_idx = self.create_anonymous_node(
                parent_idx,
                AnonymousBoxType::TableRow,
                FormattingContext::TableRow,
            );
            for nr_id in non_row_children {
                self.process_node(styled_dom, nr_id, Some(anon_row_idx), debug_messages)?;
            }
        }
        Ok(())
    }
    /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
    /// "If a child C of a 'table-row' box is not a 'table-cell', then generate an
    /// anonymous 'table-cell' box around C and all consecutive siblings of C that
    /// are not 'table-cell' boxes."
    fn process_table_row_children(
        &mut self,
        styled_dom: &StyledDom,
        parent_dom_id: NodeId,
        parent_idx: usize,
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
    ) -> Result<()> {
        let parent_display = get_display_type(styled_dom, parent_dom_id);
        let mut non_cell_children = Vec::new();
        for child_id in parent_dom_id.az_children(&styled_dom.node_hierarchy.as_container()) {
            if should_skip_for_table_structure(styled_dom, child_id, parent_display) {
                continue;
            }
            let child_display = get_display_type(styled_dom, child_id);
            if child_display == LayoutDisplay::TableCell {
                // Flush accumulated non-cell children into one anonymous table-cell
                if !non_cell_children.is_empty() {
                    let anon_cell_idx = self.create_anonymous_node(
                        parent_idx,
                        AnonymousBoxType::TableCell,
                        FormattingContext::Block {
                            establishes_new_context: true,
                        },
                    );
                    for nc_id in non_cell_children.drain(..) {
                        self.process_node(styled_dom, nc_id, Some(anon_cell_idx), debug_messages)?;
                    }
                }
                // Process table-cell child directly
                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
            } else {
                // Accumulate consecutive non-cell children
                non_cell_children.push(child_id);
            }
        }
        // Flush remaining non-cell children
        if !non_cell_children.is_empty() {
            let anon_cell_idx = self.create_anonymous_node(
                parent_idx,
                AnonymousBoxType::TableCell,
                FormattingContext::Block {
                    establishes_new_context: true,
                },
            );
            for nc_id in non_cell_children {
                self.process_node(styled_dom, nc_id, Some(anon_cell_idx), debug_messages)?;
            }
        }
        Ok(())
    }
    // +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())
    /// CSS 2.2 Section 17.2.1 - Anonymous box generation:
    /// "In this process, inline-level boxes are wrapped in anonymous boxes as needed
    /// to satisfy the constraints of the table model."
    ///
    // +spec:display-property:ee83bf - Anonymous box generation: boxes not associated with elements, inheriting through box tree parentage
    /// Helper to create an anonymous node in the tree.
    /// Anonymous boxes don't have a corresponding DOM node and are used to enforce
    /// the CSS box model structure (e.g., wrapping inline content in blocks,
    /// or creating missing table structural elements).
    // +spec:display-property:6ff51a - anonymous block boxes have no styles (box_props default), so parent element properties still apply to its content
175
    pub fn create_anonymous_node(
175
        &mut self,
175
        parent: usize,
175
        anon_type: AnonymousBoxType,
175
        fc: FormattingContext,
175
    ) -> usize {
175
        let index = self.nodes.len();
        // +spec:display-property:e67146 - Anonymous boxes inherit from enclosing non-anonymous box; non-inherited props use initial values
175
        let parent_fc = self.nodes.get(parent).map(|n| n.formatting_context.clone());
175
        self.nodes.push(LayoutNode {
175
            // ── HOT ──
175
            box_props: BoxProps::default(),
175
            dom_node_id: None,
175
            children: Vec::new(),
175
            used_size: None,
175
            formatting_context: fc,
175
            parent: Some(parent),
175
            // ── WARM ──
175
            intrinsic_sizes: None,
175
            baseline: None,
175
            inline_layout_result: None,
175
            scrollbar_info: None,
175
            relative_position: None,
175
            overflow_content_size: None,
175
            taffy_cache: TaffyCache::new(),
175
            computed_style: ComputedLayoutStyle::default(),
175
            pseudo_element: None,
175
            escaped_top_margin: None,
175
            escaped_bottom_margin: None,
175
            parent_formatting_context: parent_fc,
175
            ifc_membership: None,
175
            containing_block_index: None,
175
            // ── COLD ──
175
            anonymous_type: Some(anon_type),
175
            node_data_fingerprint: NodeDataFingerprint::default(),
175
            subtree_hash: SubtreeHash(0),
175
            dirty_flag: DirtyFlag::Layout,
175
            unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
175
            ifc_id: None,
175
        });
175
        self.nodes[parent].children.push(index);
175
        index
175
    }
    /// Creates a ::marker pseudo-element as the first child of a list-item.
    ///
    /// Per CSS Lists Module Level 3, Section 3.1:
    /// "For elements with display: list-item, user agents must generate a
    /// ::marker pseudo-element as the first child of the principal box."
    ///
    /// The ::marker references the same DOM node as its parent list-item,
    /// but is marked as a pseudo-element for proper counter resolution and styling.
    pub fn create_marker_pseudo_element(
        &mut self,
        styled_dom: &StyledDom,
        list_item_dom_id: NodeId,
        list_item_idx: usize,
    ) -> usize {
        let index = self.nodes.len();
        // The marker references the same DOM node as the list-item
        // This is important for style resolution (the marker inherits from the list-item)
        let parent_fc = self
            .nodes
            .get(list_item_idx)
            .map(|n| n.formatting_context.clone());
        self.nodes.push(LayoutNode {
            // ── HOT ──
            box_props: BoxProps::default(),
            dom_node_id: Some(list_item_dom_id),
            children: Vec::new(),
            used_size: None,
            formatting_context: FormattingContext::Inline,
            parent: Some(list_item_idx),
            // ── WARM ──
            intrinsic_sizes: None,
            baseline: None,
            inline_layout_result: None,
            scrollbar_info: None,
            relative_position: None,
            overflow_content_size: None,
            taffy_cache: TaffyCache::new(),
            computed_style: ComputedLayoutStyle::default(),
            pseudo_element: Some(PseudoElement::Marker),
            escaped_top_margin: None,
            escaped_bottom_margin: None,
            parent_formatting_context: parent_fc,
            ifc_membership: None,
            containing_block_index: None,
            // ── COLD ──
            anonymous_type: None,
            node_data_fingerprint: NodeDataFingerprint::default(),
            subtree_hash: SubtreeHash(0),
            dirty_flag: DirtyFlag::Layout,
            unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
            ifc_id: None,
        });
        // Insert as FIRST child (per spec)
        self.nodes[list_item_idx].children.insert(0, index);
        // Register with DOM mapping for counter resolution
        self.dom_to_layout
            .entry(list_item_dom_id)
            .or_default()
            .push(index);
        index
    }
    // M12.7: returns `usize`, NOT `Result<usize>` — this fn has no error path
    // (always `Ok(index)`). The `Result` forced callers to use `?`, whose lifted
    // discriminant decode mis-reads the Ok as Err (the rc=5 root cause: reconcile
    // reaches this fn but returns Err before its own Ok). Dropping the Result
    // removes that mis-lifting `?`.
25270
    pub fn create_node_from_dom(
25270
        &mut self,
25270
        styled_dom: &StyledDom,
25270
        dom_id: NodeId,
25270
        parent: Option<usize>,
25270
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
25270
    ) -> usize {
25270
        let index = self.nodes.len();
        // as IT sees it). If this is 0 but build() sees 0 nodes, the push is lost
        // between here and build (builder &mut threading); if garbage, len mis-reads.
25270
        { let _ = (0xCE00_0000u32 | (index as u32 & 0xffff)); }
25270
        let parent_fc =
25270
            parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context.clone()));
        // this is reached but step A is NOT, collect_box_props diverges; if this is
        // NOT reached, the parent Option discriminant mis-lifts (None→Some garbage).
25270
        { let _ = (0xCD00_0001u32 | ((parent_fc.is_some() as u32) << 8)); }
25270
        let collected = collect_box_props(styled_dom, dom_id, debug_messages, self.viewport_size);
25270
        { let _ = (0xCA00_0001u32); }
25270
        self.nodes.push(LayoutNode {
            // ── HOT ──
25270
            box_props: collected.resolved,
25270
            dom_node_id: Some(dom_id),
25270
            children: Vec::new(),
25270
            used_size: None,
25270
            formatting_context: determine_formatting_context(styled_dom, dom_id),
25270
            parent,
            // ── WARM ──
25270
            intrinsic_sizes: None,
25270
            baseline: None,
25270
            inline_layout_result: None,
25270
            scrollbar_info: None,
25270
            relative_position: None,
25270
            overflow_content_size: None,
25270
            taffy_cache: TaffyCache::new(),
            // +spec:overflow:8f9f7e - viewport overflow propagation: visible→auto, clip→hidden
            computed_style: {
25270
                let mut style = compute_layout_style(styled_dom, dom_id);
25270
                if parent.is_none() {
                    // CSS Overflow 3 §3.3: If visible is applied to the viewport,
                    // it must be interpreted as auto. If clip is applied to the
                    // viewport, it must be interpreted as hidden.
                    use azul_css::props::layout::LayoutOverflow;
4655
                    if style.overflow_x == LayoutOverflow::Visible {
4655
                        style.overflow_x = LayoutOverflow::Auto;
4655
                    } else if style.overflow_x == LayoutOverflow::Clip {
                        style.overflow_x = LayoutOverflow::Hidden;
                    }
4655
                    if style.overflow_y == LayoutOverflow::Visible {
4655
                        style.overflow_y = LayoutOverflow::Auto;
4655
                    } else if style.overflow_y == LayoutOverflow::Clip {
                        style.overflow_y = LayoutOverflow::Hidden;
                    }
20615
                }
25270
                style
            },
25270
            pseudo_element: None,
25270
            escaped_top_margin: None,
25270
            escaped_bottom_margin: None,
25270
            parent_formatting_context: parent_fc,
25270
            ifc_membership: None,
25270
            containing_block_index: None,
            // ── COLD ──
25270
            anonymous_type: None,
25270
            node_data_fingerprint: NodeDataFingerprint::compute(
25270
                &styled_dom.node_data.as_container()[dom_id],
25270
                styled_dom.styled_nodes.as_container().get(dom_id).map(|n| &n.styled_node_state),
            ),
25270
            subtree_hash: SubtreeHash(0),
25270
            dirty_flag: DirtyFlag::Layout,
25270
            unresolved_box_props: collected.unresolved,
25270
            ifc_id: None,
        });
25270
        { let _ = (0xCB00_0001u32 | ((self.nodes.len() as u32 & 0xff) << 8)); }
25270
        if let Some(p) = parent {
20615
            self.nodes[p].children.push(index);
20615
        }
25270
        self.dom_to_layout.entry(dom_id).or_default().push(index);
25270
        { let _ = (0xCF00_0000u32 | (self.nodes.len() as u32 & 0xffff)); }
25270
        index
25270
    }
70
    pub fn clone_node_from_old(&mut self, old_node: &LayoutNode, parent: Option<usize>) -> usize {
70
        let index = self.nodes.len();
70
        let mut new_node = old_node.clone();
70
        new_node.parent = parent;
        new_node.parent_formatting_context =
70
            parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context.clone()));
70
        new_node.children = Vec::new();
70
        new_node.dirty_flag = DirtyFlag::None;
70
        self.nodes.push(new_node);
70
        if let Some(p) = parent {
35
            self.nodes[p].children.push(index);
35
        }
70
        if let Some(dom_id) = old_node.dom_node_id {
70
            self.dom_to_layout.entry(dom_id).or_default().push(index);
70
        }
70
        index
70
    }
4690
    pub fn build(self, root_idx: usize) -> LayoutTree {
4690
        let nodes = self.nodes;
4690
        let node_count = nodes.len();
        // Flatten per-node children Vecs into a single contiguous arena.
25515
        let total_children: usize = nodes.iter().map(|n| n.children.len()).sum();
4690
        let mut arena = Vec::with_capacity(total_children);
4690
        let mut offsets = Vec::with_capacity(node_count);
        // Split monolithic LayoutNodes into hot/warm/cold SoA arrays
4690
        let mut hot_nodes = Vec::with_capacity(node_count);
4690
        let mut warm_nodes = Vec::with_capacity(node_count);
4690
        let mut cold_nodes = Vec::with_capacity(node_count);
30205
        for node in nodes {
25515
            // Flatten children into arena first
25515
            let start = arena.len() as u32;
25515
            let len = node.children.len() as u32;
25515
            arena.extend_from_slice(&node.children);
25515
            offsets.push((start, len));
25515

            
25515
            // Split into hot/warm/cold
25515
            let (hot, warm, cold) = node.split();
25515
            hot_nodes.push(hot);
25515
            warm_nodes.push(warm);
25515
            cold_nodes.push(cold);
25515
        }
        // discriminant). If len>0 but calculate_intrinsic_recursive's
        // `tree.get(root).ok_or(InvalidTree)?` still errors, that `?`/null-check
        // mis-discriminates Some→None. If len==0, build's input was empty.
        // if build>0 but get_node_size sees 0, the tree.clone() (hashbrown) drops the map.
4690
        LayoutTree {
4690
            nodes: hot_nodes,
4690
            warm: warm_nodes,
4690
            cold: cold_nodes,
4690
            root: root_idx,
4690
            dom_to_layout: self.dom_to_layout,
4690
            children_arena: arena,
4690
            children_offsets: offsets,
4690
            // Populated by `generate_layout_tree` after the tree is built,
4690
            // since the computation needs styled_dom for float/position lookup.
4690
            subtree_needs_intrinsic: Vec::new(),
4690
        }
4690
    }
}
// +spec:display-property:697082 - outer display type determines principal box's role in flow layout (block vs inline)
// +spec:display-property:0d251b - Block-level elements: display 'block', 'list-item', 'table' generate block-level boxes
// +spec:display-property:9464be - block-level vs block container distinction: not all block-level boxes are block containers (e.g. replaced elements, flex containers)
63000
pub fn is_block_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
26705
    matches!(
63000
        get_display_type(styled_dom, node_id),
        LayoutDisplay::Block
            | LayoutDisplay::FlowRoot
            | LayoutDisplay::Flex
            | LayoutDisplay::Grid
            | LayoutDisplay::Table
            | LayoutDisplay::TableCaption
            | LayoutDisplay::TableRow
            | LayoutDisplay::TableRowGroup
            | LayoutDisplay::TableHeaderGroup
            | LayoutDisplay::TableFooterGroup
            | LayoutDisplay::TableCell
            | LayoutDisplay::ListItem
    )
63000
}
// +spec:display-property:23f111 - Inline-level elements: inline, inline-block, inline-table, inline-flex, inline-grid
/// Checks if a node is inline-level (including text nodes).
/// According to CSS spec, inline-level content includes:
///
/// - Elements with display: inline, inline-block, inline-table, inline-flex, inline-grid
/// - Text nodes
/// - Generated content
9240
fn is_inline_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
    // Text nodes are always inline-level
9240
    let node_data = &styled_dom.node_data.as_container()[node_id];
9240
    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
3115
        return true;
6125
    }
    // Check the display property
5810
    matches!(
6125
        get_display_type(styled_dom, node_id),
        LayoutDisplay::Inline
            | LayoutDisplay::InlineBlock
            | LayoutDisplay::InlineTable
            | LayoutDisplay::InlineFlex
            | LayoutDisplay::InlineGrid
    )
9240
}
// +spec:display-property:c2520b - Block containers with only inline-level children establish IFC; mixed content gets anonymous block wrappers
/// Checks if a block container has only inline-level children.
/// According to CSS 2.2 Section 9.4.2: "An inline formatting context is established
/// by a block container box that contains no block-level boxes."
// +spec:display-property:75d642 - block container with only inline-level content establishes IFC
// +spec:display-property:c188d6 - IFC: all inline content within a containing block flows together as continuous text
11130
fn has_only_inline_children(styled_dom: &StyledDom, node_id: NodeId) -> bool {
11130
    let hierarchy = styled_dom.node_hierarchy.as_container();
11130
    let node_hier = match hierarchy.get(node_id) {
11130
        Some(n) => n,
        None => {
            return false;
        }
    };
    // Get the first child
11130
    let mut current_child = node_hier.first_child_id(node_id);
    // If there are no children, it's not an IFC (it's empty)
11130
    if current_child.is_none() {
3500
        return false;
7630
    }
    // Check all children
11060
    while let Some(child_id) = current_child {
9240
        let is_inline = is_inline_level(styled_dom, child_id);
9240
        if !is_inline {
            // Found a block-level child
5810
            return false;
3430
        }
        // Move to next sibling
3430
        if let Some(child_hier) = hierarchy.get(child_id) {
3430
            current_child = child_hier.next_sibling_id();
3430
        } else {
            break;
        }
    }
    // All children are inline-level
1820
    true
11130
}
/// Pre-computes all CSS properties needed during layout for a single node.
/// 
/// This is called once per node during layout tree construction, avoiding
/// repeated style lookups during the actual layout pass (O(n) vs O(n²)).
25270
fn compute_layout_style(styled_dom: &StyledDom, dom_id: NodeId) -> ComputedLayoutStyle {
25270
    let styled_node_state = styled_dom
25270
        .styled_nodes
25270
        .as_container()
25270
        .get(dom_id)
25270
        .map(|n| n.styled_node_state.clone())
25270
        .unwrap_or_default();
    // Get display property
25270
    let display = match get_display_property(styled_dom, Some(dom_id)) {
25270
        MultiValue::Exact(d) => d,
        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => LayoutDisplay::Block,
    };
    // Get position property
25270
    let position = get_position(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
    // Get float property  
25270
    let float = get_float(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
    // Get overflow properties
    // +spec:overflow:48890c - overflow:hidden treated as overflow:clip on replaced elements
25270
    let is_replaced = matches!(
25270
        styled_dom.node_data.as_container()[dom_id].get_node_type(),
        NodeType::Image(_) | NodeType::VirtualView
    );
25270
    let overflow_x = {
25270
        let v = get_overflow_x(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
25270
        if is_replaced && v == LayoutOverflow::Hidden { LayoutOverflow::Clip } else { v }
    };
25270
    let overflow_y = {
25270
        let v = get_overflow_y(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
25270
        if is_replaced && v == LayoutOverflow::Hidden { LayoutOverflow::Clip } else { v }
    };
    // Get writing mode, direction, and text-orientation
    // +spec:writing-modes:2af307 - Propagate used writing-mode from <body> to <html> root
25270
    let writing_mode = {
25270
        let own_wm = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
25270
        let nd = &styled_dom.node_data.as_container()[dom_id];
25270
        if matches!(nd.node_type, NodeType::Html) {
            // If root <html>, propagate writing-mode from first <body> child
1925
            styled_dom
1925
                .node_hierarchy
1925
                .as_container()
1925
                .get(dom_id)
1925
                .and_then(|node| node.first_child_id(dom_id))
1925
                .and_then(|child_id| {
1925
                    let child_data = &styled_dom.node_data.as_container()[child_id];
1925
                    if matches!(child_data.node_type, NodeType::Body) {
1925
                        let child_state = &styled_dom
1925
                            .styled_nodes
1925
                            .as_container()[child_id]
1925
                            .styled_node_state;
1925
                        Some(get_writing_mode(styled_dom, child_id, child_state)
1925
                            .unwrap_or_default())
                    } else {
                        None
                    }
1925
                })
1925
                .unwrap_or(own_wm)
        } else {
23345
            own_wm
        }
    };
25270
    let direction = get_direction(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
25270
    let text_orientation = get_text_orientation(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
    // Get text-align
25270
    let text_align = get_text_align(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
    // Get explicit width/height (None = auto)
25270
    let width = match get_css_width(styled_dom, dom_id, &styled_node_state) {
4200
        MultiValue::Exact(w) => Some(w),
21070
        _ => None,
    };
25270
    let height = match get_css_height(styled_dom, dom_id, &styled_node_state) {
6720
        MultiValue::Exact(h) => Some(h),
18550
        _ => None,
    };
    // Get min/max constraints
25270
    let min_width = match get_css_min_width(styled_dom, dom_id, &styled_node_state) {
        MultiValue::Exact(v) => Some(v),
25270
        _ => None,
    };
25270
    let min_height = match get_css_min_height(styled_dom, dom_id, &styled_node_state) {
280
        MultiValue::Exact(v) => Some(v),
24990
        _ => None,
    };
25270
    let max_width = match get_css_max_width(styled_dom, dom_id, &styled_node_state) {
35
        MultiValue::Exact(v) => Some(v),
25235
        _ => None,
    };
25270
    let max_height = match get_css_max_height(styled_dom, dom_id, &styled_node_state) {
        MultiValue::Exact(v) => Some(v),
25270
        _ => None,
    };
25270
    ComputedLayoutStyle {
25270
        display,
25270
        position,
25270
        float,
25270
        overflow_x,
25270
        overflow_y,
25270
        writing_mode,
25270
        direction,
25270
        text_orientation,
25270
        width,
25270
        height,
25270
        min_width,
25270
        min_height,
25270
        max_width,
25270
        max_height,
25270
        text_align,
25270
    }
25270
}
// hash_node_data() removed — replaced by NodeDataFingerprint::compute()
/// Helper function to get element's computed font-size
71155
fn get_element_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
71155
    { let _ = (0xC3_000001u32); } // 2-arg wrapper entered
71155
    let node_state = styled_dom
71155
        .styled_nodes
71155
        .as_container()
71155
        .get(dom_id)
71155
        .map(|n| &n.styled_node_state)
71155
        .cloned()
71155
        .unwrap_or_default();
71155
    { let _ = (0xC3_000002u32); } // after node_state (clone); next = 3-arg call
71155
    crate::solver3::getters::get_element_font_size(styled_dom, dom_id, &node_state)
71155
}
/// Helper function to get parent's computed font-size
25270
fn get_parent_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
25270
    styled_dom
25270
        .node_hierarchy
25270
        .as_container()
25270
        .get(dom_id)
25270
        .and_then(|node| node.parent_id())
25270
        .map(|parent_id| get_element_font_size(styled_dom, parent_id))
25270
        .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE)
25270
}
/// Helper function to get root element's font-size
25270
fn get_root_font_size(styled_dom: &StyledDom) -> f32 {
    // Root is always NodeId(0) in Azul
25270
    get_element_font_size(styled_dom, NodeId::new(0))
25270
}
/// Create a ResolutionContext for a given node
25270
fn create_resolution_context(
25270
    styled_dom: &StyledDom,
25270
    dom_id: NodeId,
25270
    containing_block_size: Option<azul_css::props::basic::PhysicalSize>,
25270
    viewport_size: LogicalSize,
25270
) -> azul_css::props::basic::ResolutionContext {
25270
    { let _ = (0xC1_000001u32); } // create_resolution_context entered
25270
    let element_font_size = get_element_font_size(styled_dom, dom_id);
25270
    { let _ = (0xC1_000002u32); } // after get_element_font_size
25270
    let parent_font_size = get_parent_font_size(styled_dom, dom_id);
25270
    { let _ = (0xC1_000003u32); } // after get_parent_font_size
25270
    let root_font_size = get_root_font_size(styled_dom);
25270
    { let _ = (0xC1_000004u32); } // after get_root_font_size
25270
    ResolutionContext {
25270
        element_font_size,
25270
        parent_font_size,
25270
        root_font_size,
25270
        // +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
25270
        containing_block_size: containing_block_size.unwrap_or(PhysicalSize::new(0.0, 0.0)),
25270
        element_size: None, // Not yet laid out
25270
        viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
25270
    }
25270
}
/// Result of collecting box properties from the styled DOM.
struct CollectedBoxProps {
    unresolved: crate::solver3::geometry::UnresolvedBoxProps,
    resolved: BoxProps,
}
/// Collects box properties from the styled DOM and returns both unresolved and resolved forms.
///
/// The unresolved form stores the raw CSS values for later re-resolution when
/// the containing block size is known. The resolved form is an initial resolution
/// using viewport_size for viewport-relative units.
25270
fn collect_box_props(
25270
    styled_dom: &StyledDom,
25270
    dom_id: NodeId,
25270
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
25270
    viewport_size: LogicalSize,
25270
) -> CollectedBoxProps {
    use crate::solver3::geometry::{UnresolvedBoxProps, UnresolvedEdge, UnresolvedMargin};
    use crate::solver3::getters::*;
    // before create_node step A is the diverging call.
25270
    { let _ = (0xC0_000001u32); } // entered
25270
    let node_data = &styled_dom.node_data.as_container()[dom_id];
    // Get styled node state
25270
    let node_state = styled_dom
25270
        .styled_nodes
25270
        .as_container()
25270
        .get(dom_id)
25270
        .map(|n| &n.styled_node_state)
25270
        .cloned()
25270
        .unwrap_or_default();
25270
    { let _ = (0xC0_000002u32); } // after node_state (clone)
    // Create resolution context for this element
    // Note: containing_block_size is None here because we don't have it yet
    // This is fine for initial resolution - will be re-resolved during layout
25270
    let context = create_resolution_context(styled_dom, dom_id, None, viewport_size);
25270
    { let _ = (0xC0_000003u32); } // after create_resolution_context
    // Read margin values from styled_dom
25270
    let margin_top_mv = get_css_margin_top(styled_dom, dom_id, &node_state);
25270
    { let _ = (0xC0_000004u32); } // after get_css_margin_top
25270
    let margin_right_mv = get_css_margin_right(styled_dom, dom_id, &node_state);
25270
    let margin_bottom_mv = get_css_margin_bottom(styled_dom, dom_id, &node_state);
25270
    let margin_left_mv = get_css_margin_left(styled_dom, dom_id, &node_state);
    // Convert MultiValue to UnresolvedMargin
101080
    let to_unresolved_margin = |mv: &MultiValue<PixelValue>| -> UnresolvedMargin {
101080
        match mv {
70
            MultiValue::Auto => UnresolvedMargin::Auto,
101010
            MultiValue::Exact(pv) => UnresolvedMargin::Length(*pv),
            _ => UnresolvedMargin::Zero,
        }
101080
    };
    // Build unresolved margins
25270
    let unresolved_margin = UnresolvedEdge {
25270
        top: to_unresolved_margin(&margin_top_mv),
25270
        right: to_unresolved_margin(&margin_right_mv),
25270
        bottom: to_unresolved_margin(&margin_bottom_mv),
25270
        left: to_unresolved_margin(&margin_left_mv),
25270
    };
25270
    { let _ = (0xC0_000005u32); } // after margin block
    // Read padding values
25270
    let padding_top_mv = get_css_padding_top(styled_dom, dom_id, &node_state);
25270
    let padding_right_mv = get_css_padding_right(styled_dom, dom_id, &node_state);
25270
    let padding_bottom_mv = get_css_padding_bottom(styled_dom, dom_id, &node_state);
25270
    let padding_left_mv = get_css_padding_left(styled_dom, dom_id, &node_state);
    // Convert MultiValue to PixelValue (default to 0px)
102515
    let to_pixel_value = |mv: MultiValue<PixelValue>| -> PixelValue {
102515
        match mv {
102515
            MultiValue::Exact(pv) => pv,
            _ => PixelValue::const_px(0),
        }
102515
    };
    // Build unresolved padding
25270
    let unresolved_padding = UnresolvedEdge {
25270
        top: to_pixel_value(padding_top_mv),
25270
        right: to_pixel_value(padding_right_mv),
25270
        bottom: to_pixel_value(padding_bottom_mv),
25270
        left: to_pixel_value(padding_left_mv),
25270
    };
25270
    { let _ = (0xC0_000056u32); } // after padding getters+values, before get_display_type
    // +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
    // Non-cell internal table elements (rows, row groups, columns, column groups) do not have padding.
    // 0xC0_57<dt> the CALL returned (dt = LayoutDisplay discriminant) and the MATCH below
    // diverges; if it stays 0x56, get_display_type (the enum extraction) itself diverges.
    // M12.7 NOTE: get_display_type RETURNS a valid dt here (captured =2), but the code
    // immediately after diverges — and replacing the `match` below with a branchless
    // bitmask test did NOT help (so it's NOT the multi-way-branch codegen). So the
    // get_display_type CALL corrupts the caller frame / control flow (same class as
    // create_node's return 0→48704), specific to ENUM-returning getters (pixel getters
    // like get_css_margin_* lift fine). Remill-level. The match is kept (original).
25270
    let unresolved_padding = match get_display_type(styled_dom, dom_id) {
        LayoutDisplay::TableRow
        | LayoutDisplay::TableRowGroup
        | LayoutDisplay::TableHeaderGroup
        | LayoutDisplay::TableFooterGroup
        | LayoutDisplay::TableColumn
1190
        | LayoutDisplay::TableColumnGroup => UnresolvedEdge {
1190
            top: PixelValue::const_px(0),
1190
            right: PixelValue::const_px(0),
1190
            bottom: PixelValue::const_px(0),
1190
            left: PixelValue::const_px(0),
1190
        },
24080
        _ => unresolved_padding,
    };
25270
    { let _ = (0xC0_000006u32); } // after padding block
    // Read border values
25270
    let border_top_mv = get_css_border_top_width(styled_dom, dom_id, &node_state);
25270
    let border_right_mv = get_css_border_right_width(styled_dom, dom_id, &node_state);
25270
    let border_bottom_mv = get_css_border_bottom_width(styled_dom, dom_id, &node_state);
25270
    let border_left_mv = get_css_border_left_width(styled_dom, dom_id, &node_state);
    // +spec:box-model:17c0e0 - computed border-width is 0 if border-style is none or hidden
    // +spec:box-model:5d2b66 - border-style none/hidden means no border
    // CSS 2.2 §8.5.1: "Computed value: absolute length; '0' if the border style is 'none' or 'hidden'"
    use azul_css::props::style::border::BorderStyle;
101080
    let style_zeroes_width = |s: BorderStyle| matches!(s, BorderStyle::None | BorderStyle::Hidden);
    // Read border styles to check if widths should be zeroed.
    // FAST PATH: compact cache returns styles directly for normal state — no
    // cascade walks. Prior code here did 4 cascade walks × 586 nodes.
25270
    let (bs_top, bs_right, bs_bottom, bs_left) = {
25270
        let cache_ptr = &styled_dom.css_property_cache.ptr;
25270
        if node_state.is_normal() {
25270
            if let Some(ref cc) = cache_ptr.compact_cache {
25270
                let idx = dom_id.index();
25270
                (cc.get_border_top_style(idx), cc.get_border_right_style(idx),
25270
                 cc.get_border_bottom_style(idx), cc.get_border_left_style(idx))
            } else {
                (
                    cache_ptr.get_border_top_style(node_data, &dom_id, &node_state)
                        .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
                    cache_ptr.get_border_right_style(node_data, &dom_id, &node_state)
                        .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
                    cache_ptr.get_border_bottom_style(node_data, &dom_id, &node_state)
                        .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
                    cache_ptr.get_border_left_style(node_data, &dom_id, &node_state)
                        .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
                )
            }
        } else {
            (
                cache_ptr.get_border_top_style(node_data, &dom_id, &node_state)
                    .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
                cache_ptr.get_border_right_style(node_data, &dom_id, &node_state)
                    .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
                cache_ptr.get_border_bottom_style(node_data, &dom_id, &node_state)
                    .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
                cache_ptr.get_border_left_style(node_data, &dom_id, &node_state)
                    .and_then(|v| v.get_property()).map(|s| s.inner).unwrap_or(BorderStyle::None),
            )
        }
    };
    // Build unresolved border, zeroing width when style is none or hidden
25270
    let unresolved_border = UnresolvedEdge {
25270
        top: if style_zeroes_width(bs_top) { PixelValue::const_px(0) } else { to_pixel_value(border_top_mv) },
25270
        right: if style_zeroes_width(bs_right) { PixelValue::const_px(0) } else { to_pixel_value(border_right_mv) },
25270
        bottom: if style_zeroes_width(bs_bottom) { PixelValue::const_px(0) } else { to_pixel_value(border_bottom_mv) },
25270
        left: if style_zeroes_width(bs_left) { PixelValue::const_px(0) } else { to_pixel_value(border_left_mv) },
    };
25270
    { let _ = (0xC0_000007u32); } // after border block (incl is_normal/compact_cache fast-path)
    // +spec:box-model:8538a9 - Internal table elements do not have margins (CSS 2.2 §17.5)
    // "These boxes have content and borders and cells have padding as well.
    //  Internal table elements do not have margins."
    // +spec:box-model:b4923a - Internal table elements do not have margins (CSS 2.2 § 17.5)
    // +spec:box-model:0a9f8e - Internal table elements do not have margins (CSS 2.2 § 17.5)
25270
    let display_type = get_display_type(styled_dom, dom_id);
25270
    let unresolved_margin = match display_type {
        LayoutDisplay::TableRow
        | LayoutDisplay::TableRowGroup
        | LayoutDisplay::TableHeaderGroup
        | LayoutDisplay::TableFooterGroup
        | LayoutDisplay::TableCell
        | LayoutDisplay::TableColumn
4165
        | LayoutDisplay::TableColumnGroup => UnresolvedEdge {
4165
            top: UnresolvedMargin::Zero,
4165
            right: UnresolvedMargin::Zero,
4165
            bottom: UnresolvedMargin::Zero,
4165
            left: UnresolvedMargin::Zero,
4165
        },
        // +spec:box-model:1197a5 - height property does not apply to non-replaced inline elements; vertical margins zeroed
        // +spec:replaced-elements:f07118 - non-replaced elements have rendering dictated by CSS model
        // "These properties apply to all elements, but vertical margins will not have
        //  any effect on non-replaced inline elements."
        LayoutDisplay::Inline => {
8435
            let is_replaced = matches!(
8435
                node_data.get_node_type(),
                NodeType::Image(_) | NodeType::VirtualView
            );
8435
            if is_replaced {
                unresolved_margin
            } else {
8435
                UnresolvedEdge {
8435
                    top: UnresolvedMargin::Zero,
8435
                    bottom: UnresolvedMargin::Zero,
8435
                    ..unresolved_margin
8435
                }
            }
        },
12670
        _ => unresolved_margin,
    };
    // Build the UnresolvedBoxProps
25270
    let unresolved = UnresolvedBoxProps {
25270
        margin: unresolved_margin,
25270
        padding: unresolved_padding,
25270
        border: unresolved_border,
25270
    };
    // Create initial resolution params (with viewport as containing block for now)
25270
    let params = crate::solver3::geometry::ResolutionParams {
25270
        containing_block: viewport_size,
25270
        viewport_size,
25270
        element_font_size: context.parent_font_size,
25270
        root_font_size: context.root_font_size,
25270
    };
    // Resolve to get initial box_props
25270
    let resolved = unresolved.resolve(&params);
    // Debug ALL node box props (padding, margin, border) for cascade debugging
25270
    if let Some(msgs) = debug_messages.as_mut() {
24640
        msgs.push(LayoutDebugMessage::box_props(format!(
24640
            "[BOX] node[{}] {:?} pad=[{:.1} {:.1} {:.1} {:.1}] mar=[{:.1} {:.1} {:.1} {:.1}] bor=[{:.1} {:.1} {:.1} {:.1}]",
24640
            dom_id.index(), node_data.node_type,
24640
            resolved.padding.top, resolved.padding.right, resolved.padding.bottom, resolved.padding.left,
24640
            resolved.margin.top, resolved.margin.right, resolved.margin.bottom, resolved.margin.left,
24640
            resolved.border.top, resolved.border.right, resolved.border.bottom, resolved.border.left,
24640
        )));
24640
    }
    // Debug nodes with non-zero margins or vh units
25270
    if let Some(msgs) = debug_messages.as_mut() {
        // Check if any margin uses vh
24640
        let has_vh = match &unresolved_margin.top {
12040
            UnresolvedMargin::Length(pv) => pv.metric == azul_css::props::basic::SizeMetric::Vh,
12600
            _ => false,
        };
24640
        if has_vh || resolved.margin.top > 0.0 || resolved.margin.left > 0.0 {
2310
            msgs.push(LayoutDebugMessage::box_props(format!(
2310
                "NodeId {:?} ({:?}): unresolved_margin_top={:?}, resolved_margin_top={:.2}, viewport_size={:?}",
2310
                dom_id, node_data.node_type,
2310
                unresolved_margin.top,
2310
                resolved.margin.top,
2310
                viewport_size
2310
            )));
22330
        }
630
    }
    // Debug margin_auto detection
25270
    if let Some(msgs) = debug_messages.as_mut() {
24640
        msgs.push(LayoutDebugMessage::box_props(format!(
24640
            "NodeId {:?} ({:?}): margin_auto: left={}, right={}, top={}, bottom={} | margin_left={:?}",
24640
            dom_id, node_data.node_type,
24640
            resolved.margin_auto.left, resolved.margin_auto.right,
24640
            resolved.margin_auto.top, resolved.margin_auto.bottom,
24640
            unresolved_margin.left
24640
        )));
24640
    }
    // Debug for Body nodes
25270
    if matches!(node_data.node_type, azul_core::dom::NodeType::Body) {
2905
        if let Some(msgs) = debug_messages.as_mut() {
2905
            msgs.push(LayoutDebugMessage::box_props(format!(
2905
                "Body margin resolved: top={:.2}, right={:.2}, bottom={:.2}, left={:.2}",
2905
                resolved.margin.top, resolved.margin.right,
2905
                resolved.margin.bottom, resolved.margin.left
2905
            )));
2905
        }
22365
    }
25270
    CollectedBoxProps { unresolved, resolved }
25270
}
/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 1:
/// "Remove all irrelevant boxes. These are boxes that do not contain table-related boxes
/// and do not themselves have 'display' set to a table-related value. In this context,
/// 'irrelevant boxes' means anonymous inline boxes that contain only white space."
///
/// Checks if a DOM node is whitespace-only text (for table anonymous box generation).
/// Returns true if the node is a text node containing only whitespace characters
/// that would be collapsed away by the white-space property.
// according to the 'white-space' property does not generate any anonymous inline boxes (CSS2§9.2.2.1)
12670
pub fn is_whitespace_only_text(styled_dom: &StyledDom, node_id: NodeId) -> bool {
12670
    let binding = styled_dom.node_data.as_container();
12670
    let node_data = binding.get(node_id);
12670
    if let Some(data) = node_data {
12670
        if let NodeType::Text(text) = data.get_node_type() {
            // Check if the text contains only CSS document white space characters
            // Per CSS Text 3 §4.1: document white space = U+0020, U+0009, segment breaks
35980
            if !text.chars().all(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')) {
105
                return false;
4235
            }
            // Per CSS2§9.2.2.1: "White space content that would subsequently be
            // collapsed away according to the 'white-space' property does not
            // generate any anonymous inline boxes."
            // For white-space: pre / pre-wrap / break-spaces, whitespace is preserved
            // and should NOT be treated as collapsible.
4235
            let white_space = styled_dom
4235
                .styled_nodes
4235
                .as_container()
4235
                .get(node_id)
4235
                .map(|n| {
4235
                    match get_white_space_property(styled_dom, node_id, &n.styled_node_state) {
4235
                        MultiValue::Exact(ws) => ws,
                        _ => StyleWhiteSpace::Normal,
                    }
4235
                })
4235
                .unwrap_or(StyleWhiteSpace::Normal);
4235
            return match white_space {
                // These values collapse whitespace — whitespace-only text is collapsible
4165
                StyleWhiteSpace::Normal | StyleWhiteSpace::Nowrap | StyleWhiteSpace::PreLine => true,
                // These values preserve whitespace — whitespace-only text is NOT collapsible
70
                StyleWhiteSpace::Pre | StyleWhiteSpace::PreWrap | StyleWhiteSpace::BreakSpaces => false,
            };
8330
        }
    }
8330
    false
12670
}
/// CSS 2.2 Section 17.2.1 - Anonymous box generation, Stage 1:
/// Determines if a node should be skipped in table structure generation.
/// Whitespace-only text nodes are "irrelevant" and should not generate boxes
/// when they appear between table-related elements.
///
/// Returns true if the node should be skipped (i.e., it's whitespace-only text
/// and the parent is a table structural element).
fn should_skip_for_table_structure(
    styled_dom: &StyledDom,
    node_id: NodeId,
    parent_display: LayoutDisplay,
) -> bool {
    // CSS 2.2 Section 17.2.1: Only skip whitespace text nodes when parent is
    // a table structural element (table, row group, row)
    matches!(
        parent_display,
        LayoutDisplay::Table
            | LayoutDisplay::InlineTable
            | LayoutDisplay::TableRowGroup
            | LayoutDisplay::TableHeaderGroup
            | LayoutDisplay::TableFooterGroup
            | LayoutDisplay::TableRow
    ) && is_whitespace_only_text(styled_dom, node_id)
}
/// Returns true if the given display type is a "proper table child" of a table/inline-table box.
/// Per CSS 2.2 §17.2.1, proper table children are: table-row-group, table-header-group,
/// table-footer-group, table-row, table-column-group, table-column, table-caption.
fn is_proper_table_child(display: LayoutDisplay) -> bool {
    matches!(
        display,
        LayoutDisplay::TableRowGroup
            | LayoutDisplay::TableHeaderGroup
            | LayoutDisplay::TableFooterGroup
            | LayoutDisplay::TableRow
            | LayoutDisplay::TableColumnGroup
            | LayoutDisplay::TableColumn
            | LayoutDisplay::TableCaption
    )
}
// Determines the display type of a node based on its tag and CSS properties.
// Delegates to getters::get_display_property which uses the compact cache fast path.
// M12.7 ROOT: get_display_type (and every layout enum getter) mis-lifts to wasm via the
// remill enum-return/decode path — the geometry-chain blocker. FOUR Rust workarounds all
// FAILED to advance (none reached collect_box_props past get_display_type):
//   1. skip the get_css_property! enum compact-cache fast path  → no change
//   2. replace the LayoutDisplay `match` with a branchless bitmask → no change
//   3. #[inline(never)] (wrap the call w/ enforce_sp_preservation) → made it diverge earlier
//   4. bypass MultiValue<LayoutDisplay> by reading cc.get_display() directly → diverges earlier
// So it is NOT the match codegen, NOT the MultiValue wrapper, NOT a frame/SP issue — it is
// the lift of a fn RETURNING a small fieldless enum (LayoutDisplay) corrupting control flow
// (pixel/i16-returning getters lift fine). Needs the remill m12-q-reg-x8-sret fork's
// enum-return handling — not fixable in Rust. (Original kept.)
203280
pub fn get_display_type(styled_dom: &StyledDom, node_id: NodeId) -> LayoutDisplay {
    use crate::solver3::getters::get_display_property;
203280
    get_display_property(styled_dom, Some(node_id)).unwrap_or(LayoutDisplay::Inline)
203280
}
// +spec:display-contents:95faa5 - blockification has no effect on none/contents (other => other)
// +spec:display-property:f68848 - Automatic box type transformations: blockification of computed display values
/// Blockify a display type per CSS Display 3 §2.7.
// +spec:display-property:760c5f - blockification sets computed outer display type to block
/// +spec:display-property:d50f70 - blockification affects computed values, determining principal box type only
/// // +spec:inline-block:692e44 - blockification of inline-block per CSS2 compatibility
// +spec:display-property:c3aca2 - inline-block blockifies to block, not flow-root
// +spec:display-property:ee2d65 - blockification of inline-level display types (CSS Display 3 §2.7)
// +spec:display-property:e4a8b7 - layout-internal boxes blockified to flow (block container)
/// CSS Flexbox §3: flex items with table-internal display values
/// (table-cell, table-row, table-row-group, table-header-group, table-footer-group,
/// table-column, table-column-group, table-caption) are blockified to display:block
/// before anonymous table box generation can occur. E.g. two consecutive
/// display:table-cell flex items become two separate display:block flex items.
140
fn blockify_flex_item_if_table_internal(nodes: &mut Vec<LayoutNode>, node_idx: usize) {
140
    if let Some(node) = nodes.get_mut(node_idx) {
140
        let is_table_internal = matches!(
140
            node.formatting_context,
            FormattingContext::TableCell
                | FormattingContext::TableRow
                | FormattingContext::TableRowGroup
                | FormattingContext::TableColumnGroup
                | FormattingContext::TableCaption
                | FormattingContext::Table
        );
140
        if is_table_internal {
            node.formatting_context = FormattingContext::Block {
                establishes_new_context: true,
            };
140
        }
    }
140
}
/// Returns true if the node is a replaced element per CSS Display 3 Appendix B.
/// Replaced elements (img, canvas, embed, object, audio, video, input, textarea,
/// select, br, wbr, meter, progress, virtual views) cannot be un-boxed by
/// `display: contents` and always establish an independent formatting context.
8365
fn is_replaced_element(node_data: &NodeData) -> bool {
8330
    matches!(
8365
        node_data.get_node_type(),
        NodeType::Image(_)
        | NodeType::VirtualView
        | NodeType::Br
        | NodeType::Wbr
        | NodeType::Meter
        | NodeType::Progress
        | NodeType::Canvas
        | NodeType::Embed
        | NodeType::Object
        | NodeType::Audio
        | NodeType::Video
        | NodeType::Input
        | NodeType::TextArea
        | NodeType::Select
    )
8365
}
// +spec:display-property:285fe7 - block box establishing a BFC (block-level block container with new BFC)
/// **Corrected:** Checks for all conditions that create a new Block Formatting Context.
/// A BFC contains floats and prevents margin collapse.
9310
fn establishes_new_block_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> bool {
9310
    let display = get_display_type(styled_dom, node_id);
9310
    if matches!(
9310
        display,
        LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::TableCaption | LayoutDisplay::FlowRoot
    ) {
        return true;
9310
    }
9310
    if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
9310
        let overflow_x = get_overflow_x(styled_dom, node_id, &styled_node.styled_node_state);
9310
        if !overflow_x.is_visible_or_clip() {
35
            return true;
9275
        }
9275
        let overflow_y = get_overflow_y(styled_dom, node_id, &styled_node.styled_node_state);
9275
        if !overflow_y.is_visible_or_clip() {
            return true;
9275
        }
9275
        let position = get_position(styled_dom, node_id, &styled_node.styled_node_state);
9275
        if position.is_absolute_or_fixed() {
35
            return true;
9240
        }
9240
        let float = get_float(styled_dom, node_id, &styled_node.styled_node_state);
9240
        if !float.is_none() {
875
            return true;
8365
        }
    }
    // CSS Writing Modes 4 § 3.2: block container with different writing-mode than parent establishes BFC
8365
    if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
8365
        let hierarchy = styled_dom.node_hierarchy.as_container();
8365
        if let Some(parent_dom_id) = hierarchy[node_id].parent_id() {
5075
            let parent_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
5075
            let child_wm = get_writing_mode(styled_dom, node_id, &styled_node.styled_node_state).unwrap_or_default();
5075
            let parent_wm = get_writing_mode(styled_dom, parent_dom_id, parent_state).unwrap_or_default();
5075
            if child_wm != parent_wm {
                return true;
5075
            }
3290
        }
    }
    // +spec:replaced-elements:4f494d - replaced elements always establish an independent formatting context
8365
    let node_data = &styled_dom.node_data.as_container()[node_id];
8365
    if is_replaced_element(node_data) {
35
        return true;
8330
    }
    // The root element (<html>) also establishes a BFC.
8330
    if styled_dom.root.into_crate_internal() == Some(node_id) {
3290
        return true;
5040
    }
5040
    false
9310
}
// +spec:display-property:0d93f1 - maps display value to box generation (principal box, none, or contents)
/// Like `determine_formatting_context`, but uses an explicit (possibly blockified) display type
/// instead of reading it from the DOM. Used when blockification changes the display.
// +spec:display-property:80f43f - inner display type defines formatting context for non-replaced elements
// +spec:display-property:46e71c - Maps outer display (block/inline) and inner display (flow/flow-root/table/flex/grid) to FormattingContext
// +spec:display-property:aa582d - maps display types to formatting contexts (inline-level, block-level, atomic inline, block container)
20405
fn determine_formatting_context_for_display(
20405
    styled_dom: &StyledDom,
20405
    node_id: NodeId,
20405
    display_type: LayoutDisplay,
20405
) -> FormattingContext {
20405
    let node_data = &styled_dom.node_data.as_container()[node_id];
20405
    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
        return FormattingContext::Inline;
20405
    }
    // +spec:display-property:2a8d62 - block containers with inline-level content establish an IFC
20405
    match display_type {
        // +spec:display-property:37bcf3 - inline outer display type generates an inline box
        // +spec:display-property:30a935 - outer display without inner defaults to flow (block/inline both use flow context)
3570
        LayoutDisplay::Inline => FormattingContext::Inline,
        // +spec:block-formatting-context:97b03b - flow-root always establishes a new BFC; block/list-item may establish one based on other conditions
        // +spec:display-property:0bac26 - list-item limited to flow layout inner types (block/flow-root)
        // +spec:display-property:0beffc - block container with only inline children establishes IFC
        // +spec:display-property:7c49c1 - block container with only inline children establishes an IFC
        // +spec:display-property:90ba2a - flow-root always establishes a new BFC
        LayoutDisplay::FlowRoot => FormattingContext::Block {
            establishes_new_context: true,
        },
        LayoutDisplay::Block | LayoutDisplay::ListItem => {
11130
            if has_only_inline_children(styled_dom, node_id) {
1820
                FormattingContext::Inline
            } else {
9310
                FormattingContext::Block {
9310
                    establishes_new_context: establishes_new_block_formatting_context(
9310
                        styled_dom, node_id,
9310
                    ),
9310
                }
            }
        }
70
        LayoutDisplay::InlineBlock => FormattingContext::InlineBlock,
        // +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
        // +spec:table-layout:023714 - map display values to table formatting contexts per CSS 2.2 §17.2
        // +spec:table-layout:6c5039 - row-primary table model: rows/cells/captions/columns mapped here
        // +spec:table-layout:75eea9 - display property values for table elements (table, tr, td, etc.)
        // +spec:table-layout:3ee121 - layout-internal display types map to table formatting context
        // +spec:display-property:b02b7f - table display types map to table formatting contexts;
        // table-column/table-column-group not rendered (treated as display:none for box generation)
1155
        LayoutDisplay::Table | LayoutDisplay::InlineTable => FormattingContext::Table,
        LayoutDisplay::TableRowGroup
        | LayoutDisplay::TableHeaderGroup
        | LayoutDisplay::TableFooterGroup => FormattingContext::TableRowGroup,
1190
        LayoutDisplay::TableRow => FormattingContext::TableRow,
2975
        LayoutDisplay::TableCell => FormattingContext::TableCell,
        // +spec:display-property:da3fc7 - display:none/contents generate no boxes (no inner/outer display types)
        // +spec:display-property:e370af - display:none generates no boxes or text sequences
        LayoutDisplay::None => FormattingContext::None,
315
        LayoutDisplay::Flex | LayoutDisplay::InlineFlex => FormattingContext::Flex,
        LayoutDisplay::TableColumnGroup => FormattingContext::TableColumnGroup,
        LayoutDisplay::TableCaption => FormattingContext::TableCaption,
        LayoutDisplay::Grid | LayoutDisplay::InlineGrid => FormattingContext::Grid,
        // table-column elements are used only for column styling, not for generating boxes
        LayoutDisplay::TableColumn => FormattingContext::None,
        // +spec:display-contents:584072 - no special behavior for legend/HTML elements; contents handled normally
        // display:contents - element generates no box, children are promoted to parent
        LayoutDisplay::Contents => FormattingContext::Contents,
        // +spec:display-property:b89b80 - run-in box falls back to block (merging into next block not implemented)
        // +spec:display-property:ccd4e6 - run-in falls back to block; reparenting not implemented
        // These less common display types default to block behavior
        // +spec:display-property:7d77f5 - run-in treated as block (run-in sequencing fixup not yet implemented)
        // +spec:display-property:0c30c4 - run-in boxes fall back to block (run-in reparenting not implemented, matches browser behavior)
        // +spec:display-property:2f5c52 - run-in treated as block (full run-in merging not implemented)
        LayoutDisplay::RunIn | LayoutDisplay::Marker => {
            FormattingContext::Block {
                establishes_new_context: true,
            }
        }
    }
20405
}
/// The logic now correctly identifies all BFC roots.
25270
fn determine_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> FormattingContext {
25270
    let node_data = &styled_dom.node_data.as_container()[node_id];
25270
    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
4865
        return FormattingContext::Inline;
20405
    }
20405
    let display_type = get_display_type(styled_dom, node_id);
20405
    determine_formatting_context_for_display(styled_dom, node_id, display_type)
25270
}