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
70312
    pub fn unique() -> Self {
34
70312
        Self(IFC_ID_COUNTER.fetch_add(1, Ordering::Relaxed))
35
70312
    }
36

            
37
    /// Reset the IFC ID counter. Called at the start of each layout pass.
38
4708
    pub fn reset_counter() {
39
4708
        IFC_ID_COUNTER.store(0, Ordering::Relaxed);
40
4708
    }
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
    codegen::format::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
    /// Hash of the `InlineContent` this layout was shaped from. The Phase 2d
228
    /// fast-path reuse in fc.rs keys cache validity on WIDTH only; without this,
229
    /// a same-width RefreshDom whose text CHANGED would reuse the stale shaped
230
    /// layout (#11 stale display list). 0 = unknown ⇒ never fast-path-reuse.
231
    pub inline_content_hash: u64,
232
}
233

            
234
impl CachedInlineLayout {
235
    /// Creates a new cached inline layout.
236
220
    pub fn new(
237
220
        layout: Arc<UnifiedLayout>,
238
220
        available_width: AvailableSpace,
239
220
        has_floats: bool,
240
220
    ) -> Self {
241
220
        let item_metrics = Self::extract_item_metrics(&layout);
242
220
        Self {
243
220
            layout,
244
220
            available_width,
245
220
            has_floats,
246
220
            constraints: None,
247
220
            item_metrics,
248
220
            line_breaks: None,
249
220
            inline_content_hash: 0,
250
220
        }
251
220
    }
252

            
253
    /// Creates a new cached inline layout with full constraints.
254
49588
    pub fn new_with_constraints(
255
49588
        layout: Arc<UnifiedLayout>,
256
49588
        available_width: AvailableSpace,
257
49588
        has_floats: bool,
258
49588
        constraints: UnifiedConstraints,
259
49588
    ) -> Self {
260
49588
        let item_metrics = Self::extract_item_metrics(&layout);
261
49588
        let available_width_px = match available_width {
262
28732
            AvailableSpace::Definite(w) => w,
263
20856
            _ => f32::MAX,
264
        };
265
49588
        let line_breaks = Some(crate::text3::cache::extract_line_breaks(
266
49588
            &layout.items, available_width_px,
267
49588
        ));
268
49588
        Self {
269
49588
            layout,
270
49588
            available_width,
271
49588
            has_floats,
272
49588
            constraints: Some(constraints),
273
49588
            item_metrics,
274
49588
            line_breaks,
275
49588
            inline_content_hash: 0,
276
49588
        }
277
49588
    }
278

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

            
288
360580
        layout.items.iter().map(|positioned_item| {
289
360580
            let bounds = positioned_item.item.bounds();
290
360580
            let (ascent, descent) = get_item_vertical_metrics_approx(&positioned_item.item);
291

            
292
360580
            let source_node_id = match &positioned_item.item {
293
360052
                ShapedItem::Cluster(c) => c.source_node_id,
294
                // Objects (inline-blocks, images) and other generated items
295
                // don't expose source_node_id directly on ShapedItem.
296
                // Phase 2c will refine this via the ContentIndex mapping.
297
                ShapedItem::Object { .. }
298
                | ShapedItem::CombinedBlock { .. }
299
                | ShapedItem::Tab { .. }
300
528
                | ShapedItem::Break { .. } => None,
301
            };
302

            
303
            // For Phase 2a, default can_break = true for all items.
304
            // Phase 2c will refine this by checking the white-space property
305
            // on the IFC root's style or the item's own style context.
306
            // (Note: text3::StyleProperties doesn't carry white-space;
307
            //  that's resolved at the IFC/BFC boundary level.)
308
360580
            let can_break = !matches!(&positioned_item.item, ShapedItem::Break { .. });
309

            
310
360580
            InlineItemMetrics {
311
360580
                source_node_id,
312
360580
                advance_width: bounds.width,
313
360580
                line_height_contribution: ascent + descent,
314
360580
                can_break,
315
360580
                line_index: positioned_item.line_index as u32,
316
360580
                x_offset: positioned_item.position.x,
317
360580
            }
318
360580
        }).collect()
319
49808
    }
320

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

            
338
        // Otherwise, require exact width match
339
46728
        self.width_constraint_matches(new_width)
340
46728
    }
341

            
342
    /// Tolerance for comparing definite layout widths (in logical pixels).
343
    /// Sub-pixel differences below this threshold are treated as identical
344
    /// to avoid unnecessary relayout from floating-point rounding.
345
    const LAYOUT_WIDTH_EPSILON: f32 = 0.1;
346

            
347
    /// Checks if the width constraint matches.
348
72160
    fn width_constraint_matches(&self, new_width: AvailableSpace) -> bool {
349
72160
        match (self.available_width, new_width) {
350
            // Definite widths must match within a small epsilon
351
22176
            (AvailableSpace::Definite(old), AvailableSpace::Definite(new)) => {
352
22176
                (old - new).abs() < Self::LAYOUT_WIDTH_EPSILON
353
            }
354
            // MinContent matches MinContent
355
            (AvailableSpace::MinContent, AvailableSpace::MinContent) => true,
356
            // MaxContent matches MaxContent
357
            (AvailableSpace::MaxContent, AvailableSpace::MaxContent) => true,
358
            // Different constraint types don't match
359
49984
            _ => false,
360
        }
361
72160
    }
362

            
363
    /// Determines if this cached layout should be replaced by a new layout.
364
    ///
365
    /// Returns true if the new layout should replace this one.
366
25432
    pub fn should_replace_with(&self, new_width: AvailableSpace, new_has_floats: bool) -> bool {
367
        // Always replace if we gain float information
368
25432
        if new_has_floats && !self.has_floats {
369
            return true;
370
25432
        }
371

            
372
        // Replace if width constraint changed
373
25432
        !self.width_constraint_matches(new_width)
374
25432
    }
375

            
376
    /// Returns a reference to the inner UnifiedLayout.
377
    ///
378
    /// This is a convenience method for code that only needs the layout data
379
    /// and doesn't care about the caching metadata.
380
    #[inline]
381
33220
    pub fn get_layout(&self) -> &Arc<UnifiedLayout> {
382
33220
        &self.layout
383
33220
    }
384

            
385
    /// Returns a clone of the inner Arc<UnifiedLayout>.
386
    ///
387
    /// This is useful for APIs that need to return an owned reference
388
    /// to the layout without exposing the caching metadata.
389
    #[inline]
390
    pub fn clone_layout(&self) -> Arc<UnifiedLayout> {
391
        self.layout.clone()
392
    }
393
}
394

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

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

            
442
    // ── WARM tier: frequently accessed but not on every node ─────────────
443

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

            
512
    // ── COLD tier: construction / reconciliation / debugging only ────────
513

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

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

            
579
// Note: LayoutNode methods that cross hot/warm/cold boundaries have been
580
// moved to LayoutTree methods (resolve_box_props, get_content_size).
581

            
582
/// CSS pseudo-elements that can be generated
583
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
584
pub enum PseudoElement {
585
    /// ::marker pseudo-element for list items
586
    Marker,
587
    /// ::before pseudo-element
588
    Before,
589
    /// ::after pseudo-element
590
    After,
591
}
592

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

            
613
// =============================================================================
614
// SoA (struct-of-arrays) layout node split for cache performance
615
// =============================================================================
616

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

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

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

            
694
impl Default for LayoutNodeCold {
695
    fn default() -> Self {
696
        Self {
697
            anonymous_type: None,
698
            node_data_fingerprint: NodeDataFingerprint::default(),
699
            subtree_hash: SubtreeHash::default(),
700
            dirty_flag: DirtyFlag::default(),
701
            unresolved_box_props: Default::default(),
702
            ifc_id: None,
703
        }
704
    }
705
}
706

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

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

            
790
/// Approximate per-field heap-byte breakdown of a [`LayoutTree`].
791
#[derive(Debug, Clone, Default)]
792
pub struct LayoutTreeMemoryReport {
793
    pub node_count: usize,
794
    pub hot_bytes: usize,
795
    pub warm_bytes: usize,
796
    pub warm_inline_layout_bytes: usize,
797
    pub warm_taffy_cache_bytes: usize,
798
    pub cold_bytes: usize,
799
    pub dom_to_layout_bytes: usize,
800
    pub children_arena_bytes: usize,
801
    pub children_offsets_bytes: usize,
802
}
803

            
804
impl LayoutTreeMemoryReport {
805
    pub fn total_bytes(&self) -> usize {
806
        self.hot_bytes
807
            + self.warm_bytes
808
            + self.warm_inline_layout_bytes
809
            + self.warm_taffy_cache_bytes
810
            + self.cold_bytes
811
            + self.dom_to_layout_bytes
812
            + self.children_arena_bytes
813
            + self.children_offsets_bytes
814
    }
815
}
816

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

            
864
    /// Returns the children of node `index` as a contiguous slice from the arena.
865
    #[inline]
866
708840
    pub fn children(&self, index: usize) -> &[usize] {
867
708840
        if let Some(&(start, len)) = self.children_offsets.get(index) {
868
708840
            &self.children_arena[(start as usize)..((start as usize) + (len as usize))]
869
        } else {
870
            &[]
871
        }
872
708840
    }
873

            
874
    /// Get hot layout data for a node (box_props, dom_node_id, used_size, etc.)
875
    #[inline]
876
4114352
    pub fn get(&self, index: usize) -> Option<&LayoutNodeHot> {
877
4114352
        self.nodes.get(index)
878
4114352
    }
879

            
880
    /// Get mutable hot layout data for a node.
881
    #[inline]
882
228624
    pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNodeHot> {
883
228624
        self.nodes.get_mut(index)
884
228624
    }
885

            
886
    /// Get warm layout data for a node (intrinsic_sizes, baseline, inline_layout, etc.)
887
    #[inline]
888
980760
    pub fn warm(&self, index: usize) -> Option<&LayoutNodeWarm> {
889
980760
        self.warm.get(index)
890
980760
    }
891

            
892
    /// Get mutable warm layout data for a node.
893
    #[inline]
894
409068
    pub fn warm_mut(&mut self, index: usize) -> Option<&mut LayoutNodeWarm> {
895
409068
        self.warm.get_mut(index)
896
409068
    }
897

            
898
    /// Get cold layout data for a node (dirty_flag, subtree_hash, fingerprint, etc.)
899
    #[inline]
900
59972
    pub fn cold(&self, index: usize) -> Option<&LayoutNodeCold> {
901
59972
        self.cold.get(index)
902
59972
    }
903

            
904
    /// Get mutable cold layout data for a node.
905
    #[inline]
906
70312
    pub fn cold_mut(&mut self, index: usize) -> Option<&mut LayoutNodeCold> {
907
70312
        self.cold.get_mut(index)
908
70312
    }
909

            
910
    pub fn root_node(&self) -> &LayoutNodeHot {
911
        &self.nodes[self.root]
912
    }
913

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

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

            
972
    /// Marks a node and its ancestors as dirty with the given flag.
973
    pub fn mark_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
974
        if flag == DirtyFlag::None {
975
            return;
976
        }
977

            
978
        let mut current_index = Some(start_index);
979
        while let Some(index) = current_index {
980
            let cold = match self.cold.get_mut(index) {
981
                Some(c) => c,
982
                None => break,
983
            };
984
            if cold.dirty_flag >= flag {
985
                break;
986
            }
987
            cold.dirty_flag = flag;
988
            current_index = self.nodes.get(index).and_then(|n| n.parent);
989
        }
990
    }
991

            
992
    /// Marks a node and its entire subtree of descendants with the given dirty flag.
993
    pub fn mark_subtree_dirty(&mut self, start_index: usize, flag: DirtyFlag) {
994
        if flag == DirtyFlag::None {
995
            return;
996
        }
997

            
998
        let mut stack = vec![start_index];
999
        while let Some(index) = stack.pop() {
            let children = self.children(index).to_vec();
            if let Some(cold) = self.cold.get_mut(index) {
                if cold.dirty_flag < flag {
                    cold.dirty_flag = flag;
                }
                stack.extend_from_slice(&children);
            }
        }
    }
    /// 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.
60500
    pub fn get_inline_layout_for_node(&self, layout_index: usize) -> Option<&std::sync::Arc<UnifiedLayout>> {
60500
        let warm = self.warm.get(layout_index)?;
        // First, check if this node has its own inline_layout_result (it's an IFC root)
60500
        if let Some(cached) = &warm.inline_layout_result {
24508
            return Some(cached.get_layout());
35992
        }
        // For text nodes, check if they have ifc_membership pointing to the IFC root
35992
        if let Some(ifc_membership) = &warm.ifc_membership {
8712
            let ifc_root_warm = self.warm.get(ifc_membership.ifc_root_layout_index)?;
8712
            if let Some(cached) = &ifc_root_warm.inline_layout_result {
8712
                return Some(cached.get_layout());
            }
27280
        }
27280
        None
60500
    }
    /// Return the layout index of the IFC root that owns `layout_index`'s inline content.
    /// If the node IS an IFC root (has its own `inline_layout_result`) or has no
    /// `ifc_membership`, returns `layout_index` unchanged. Inline text nodes never get
    /// their own box position (it stays the `f32::MIN` sentinel) — their geometry lives
    /// in the IFC root's content box, so selection/inline painting must anchor to the
    /// IFC root's position, not the text node's. See `get_inline_layout_for_node`.
31900
    pub fn get_ifc_root_layout_index(&self, layout_index: usize) -> usize {
31900
        if let Some(warm) = self.warm.get(layout_index) {
31900
            if warm.inline_layout_result.is_none() {
8052
                if let Some(ifc_membership) = &warm.ifc_membership {
8052
                    return ifc_membership.ifc_root_layout_index;
                }
23848
            }
        }
23848
        layout_index
31900
    }
    /// Get the content size of a node (for scrollbar calculations).
90772
    pub fn get_content_size(&self, index: usize) -> LogicalSize {
90772
        let warm = match self.warm.get(index) {
90772
            Some(w) => w,
            None => return LogicalSize::default(),
        };
90772
        if let Some(content_size) = warm.overflow_content_size {
78056
            return content_size;
12716
        }
12716
        let hot = match self.nodes.get(index) {
12716
            Some(h) => h,
            None => return LogicalSize::default(),
        };
12716
        let mut content_size = hot.used_size.unwrap_or_default();
12716
        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);
12716
        }
12716
        content_size
90772
    }
}
/// Generate layout tree from styled DOM with proper anonymous box generation
59
pub fn generate_layout_tree<T: ParsedFontTrait>(
59
    ctx: &mut LayoutContext<'_, T>,
59
) -> Result<LayoutTree> {
59
    let mut builder = LayoutTreeBuilder::new(ctx.viewport_size);
59
    let root_id = ctx
59
        .styled_dom
59
        .root
59
        .into_crate_internal()
59
        .unwrap_or(NodeId::ZERO);
59
    let root_index =
59
        builder.process_node(ctx.styled_dom, root_id, None, &mut ctx.debug_messages)?;
59
    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).
59
    layout_tree.subtree_needs_intrinsic = compute_subtree_needs_intrinsic(ctx.styled_dom, &layout_tree);
59
    debug_log!(
59
        ctx,
59
        "Generated layout tree with {} nodes (incl. anonymous)",
59
        layout_tree.nodes.len()
    );
59
    Ok(layout_tree)
59
}
/// 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.
48136
pub(crate) fn is_shrink_to_fit_context(
48136
    styled_dom: &StyledDom,
48136
    dom_node_id: Option<NodeId>,
48136
    fc: &FormattingContext,
48136
) -> bool {
    use crate::solver3::getters::{get_float, MultiValue};
    use crate::solver3::positioning::get_position_type;
    use azul_css::props::layout::{LayoutFloat, LayoutPosition};
48136
    match fc {
        FormattingContext::Flex
        | FormattingContext::Grid
        | FormattingContext::Table
4840
        | FormattingContext::InlineBlock => return true,
43296
        _ => {}
    }
43296
    let Some(dom_id) = dom_node_id else { return false; };
43032
    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
43032
    let float_val = match get_float(styled_dom, dom_id, node_state) {
43032
        MultiValue::Exact(v) => v,
        _ => LayoutFloat::None,
    };
43032
    if float_val != LayoutFloat::None {
2288
        return true;
40744
    }
40744
    let pos = get_position_type(styled_dom, Some(dom_id));
40744
    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.
88
        return true;
40656
    }
40656
    false
48136
}
/// 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.
2596
fn compute_subtree_needs_intrinsic(
2596
    styled_dom: &StyledDom,
2596
    tree: &LayoutTree,
2596
) -> Vec<bool> {
2596
    let n = tree.nodes.len();
2596
    let mut out = vec![false; n];
13244
    for idx in (0..n).rev() {
13244
        let hot = &tree.nodes[idx];
13244
        let self_stf = is_shrink_to_fit_context(styled_dom, hot.dom_node_id, &hot.formatting_context);
13244
        let mut any = self_stf;
13244
        if !any {
11440
            for &child in tree.children(idx) {
8932
                if out.get(child).copied().unwrap_or(false) {
2684
                    any = true;
2684
                    break;
6248
                }
            }
1804
        }
13244
        out[idx] = any;
    }
2596
    out
2596
}
/// 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 {
7304
    pub fn new(viewport_size: LogicalSize) -> Self {
7304
        Self {
7304
            nodes: Vec::new(),
7304
            dom_to_layout: BTreeMap::new(),
7304
            viewport_size,
7304
        }
7304
    }
122012
    pub fn get(&self, index: usize) -> Option<&LayoutNode> {
122012
        self.nodes.get(index)
122012
    }
44484
    pub fn get_mut(&mut self, index: usize) -> Option<&mut LayoutNode> {
44484
        self.nodes.get_mut(index)
44484
    }
    // +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.
13068
    pub fn process_node(
13068
        &mut self,
13068
        styled_dom: &StyledDom,
13068
        dom_id: NodeId,
13068
        parent_idx: Option<usize>,
13068
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
13068
    ) -> Result<usize> {
13068
        let node_data = &styled_dom.node_data.as_container()[dom_id];
13068
        let node_idx = self.create_node_from_dom(styled_dom, dom_id, parent_idx, debug_messages);
13068
        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."
13068
        let raw_display = if raw_display.is_layout_internal() && is_replaced_element(node_data) {
            LayoutDisplay::Inline
        } else {
13068
            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
13068
        let node_position = self.nodes.get(node_idx).map(|n| n.computed_style.position).unwrap_or_default();
13068
        let node_float = self.nodes.get(node_idx).map(|n| n.computed_style.float).unwrap_or_default();
13068
        let is_absolute_or_fixed = matches!(node_position, LayoutPosition::Absolute | LayoutPosition::Fixed);
13068
        let is_floated = node_float != LayoutFloat::None;
13068
        let is_root = parent_idx.is_none();
        // Per CSS 2.2 §9.7: if position is absolute or fixed, float computes to 'none'
13068
        if is_absolute_or_fixed && is_floated {
            if let Some(node) = self.nodes.get_mut(node_idx) {
                node.computed_style.float = LayoutFloat::None;
            }
13068
        }
13068
        let is_flex_grid_child = parent_idx
13068
            .and_then(|p| self.nodes.get(p).map(|n| matches!(n.formatting_context, FormattingContext::Flex | FormattingContext::Grid)))
13068
            .unwrap_or(false);
13068
        let display_type = crate::solver3::getters::get_computed_display(
13068
            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
13068
        if display_type != raw_display {
264
            if let Some(node) = self.nodes.get_mut(node_idx) {
264
                node.computed_style.display = display_type;
264
                node.formatting_context = determine_formatting_context_for_display(
264
                    styled_dom, dom_id, display_type,
264
                );
264
            }
12804
        }
        // Compute containing block index for abs-pos clip exemption
13068
        if is_absolute_or_fixed {
88
            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
88
                let mut ancestor = parent_idx;
                loop {
88
                    match ancestor {
88
                        Some(idx) => {
88
                            let pos = self.nodes.get(idx)
88
                                .map(|n| n.computed_style.position)
88
                                .unwrap_or_default();
88
                            if pos.is_positioned() {
88
                                break Some(idx);
                            }
                            ancestor = self.nodes.get(idx).and_then(|n| n.parent);
                        }
                        None => break None, // root
                    }
                }
            };
88
            if let Some(node) = self.nodes.get_mut(node_idx) {
88
                node.containing_block_index = cb_index;
88
            }
12980
        }
13068
        if parent_idx.is_none() {
2596
            if let Some(node) = self.nodes.get_mut(node_idx) {
2596
                if let FormattingContext::Block { ref mut establishes_new_context } = node.formatting_context {
2508
                    *establishes_new_context = true;
2508
                }
            }
10472
        }
        // +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
13068
        if display_type == LayoutDisplay::ListItem {
            self.create_marker_pseudo_element(styled_dom, dom_id, node_idx);
13068
        }
        // +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.
13068
        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);
13068
        }
13068
        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);
13068
        }
13068
        match display_type {
            LayoutDisplay::Block
            | LayoutDisplay::InlineBlock
            | LayoutDisplay::FlowRoot
            | LayoutDisplay::ListItem => {
10824
                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.
2244
                let children: Vec<NodeId> = dom_id
2244
                    .az_children(&styled_dom.node_hierarchy.as_container())
                    // +spec:display-property:9f02c6 - display:none elements generate no boxes
2244
                    .filter(|&child_id| {
                        // +spec:display-property:3b507e - display:none excludes subtree from box tree
1452
                        if get_display_type(styled_dom, child_id) == LayoutDisplay::None {
                            return false;
1452
                        }
                        // Check for whitespace-only text
1452
                        let node_data = &styled_dom.node_data.as_container()[child_id];
1452
                        if let NodeType::Text(text) = node_data.get_node_type() {
                            // Skip if text is empty or just whitespace
880
                            return !text.as_str().trim().is_empty();
572
                        }
572
                        true
1452
                    })
2244
                    .collect();
2244
                let is_flex_or_grid = matches!(
2244
                    display_type,
                    LayoutDisplay::Flex | LayoutDisplay::InlineFlex
                    | LayoutDisplay::Grid | LayoutDisplay::InlineGrid
                );
3212
                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
968
                    let child_display = get_display_type(styled_dom, child_dom_id);
968
                    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 {
968
                        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)
968
                        if is_flex_or_grid {
836
                            blockify_flex_item_if_table_internal(&mut self.nodes, child_idx);
836
                        }
                    }
                }
            }
        }
13068
        Ok(node_idx)
13068
    }
    // +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
10824
    fn process_block_children(
10824
        &mut self,
10824
        styled_dom: &StyledDom,
10824
        parent_dom_id: NodeId,
10824
        parent_idx: usize,
10824
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
10824
    ) -> Result<()> {
        // Filter out display: none children - they don't participate in layout
10824
        let children: Vec<NodeId> = parent_dom_id
10824
            .az_children(&styled_dom.node_hierarchy.as_container())
14828
            .filter(|&child_id| get_display_type(styled_dom, child_id) != LayoutDisplay::None)
10824
            .collect();
        // Debug: log which children we found
10824
        if let Some(msgs) = debug_messages.as_mut() {
10824
            msgs.push(LayoutDebugMessage::info(format!(
10824
                "[process_block_children] DOM node {} has {} children: {:?}",
10824
                parent_dom_id.index(),
10824
                children.len(),
14828
                children.iter().map(|c| c.index()).collect::<Vec<_>>()
            )));
        }
10824
        let has_block_child = children.iter().any(|&id| is_block_level(styled_dom, id));
10824
        if let Some(msgs) = debug_messages.as_mut() {
10824
            msgs.push(LayoutDebugMessage::info(format!(
10824
                "[process_block_children] has_block_child={}, children display types: {:?}",
                has_block_child,
10824
                children
10824
                    .iter()
14828
                    .map(|c| {
14828
                        let dt = get_display_type(styled_dom, *c);
14828
                        let is_block = is_block_level(styled_dom, *c);
14828
                        format!("{}:{:?}(block={})", c.index(), dt, is_block)
14828
                    })
10824
                    .collect::<Vec<_>>()
            )));
        }
10824
        if !has_block_child {
            // All children are inline, no anonymous boxes needed.
4488
            if let Some(msgs) = debug_messages.as_mut() {
4488
                msgs.push(LayoutDebugMessage::info(format!(
4488
                    "[process_block_children] All inline, processing {} children directly",
4488
                    children.len()
4488
                )));
4488
            }
5852
            for child_id in children {
1364
                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
            }
4488
            return Ok(());
6336
        }
        // Mixed block and inline content requires anonymous wrappers.
6336
        let mut inline_run = Vec::new();
19800
        for child_id in children {
13464
            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."
7964
                if !inline_run.is_empty() {
3564
                    let all_whitespace = inline_run
3564
                        .iter()
3564
                        .all(|id| is_whitespace_only_text(styled_dom, *id));
3564
                    if all_whitespace {
3476
                        if let Some(msgs) = debug_messages.as_mut() {
3476
                            msgs.push(LayoutDebugMessage::info(format!(
3476
                                "[process_block_children] Skipping whitespace-only inline run between blocks: {:?}",
3476
                                inline_run.iter().map(|c: &NodeId| c.index()).collect::<Vec<_>>()
                            )));
                        }
3476
                        inline_run.clear();
                    } else {
88
                        if let Some(msgs) = debug_messages.as_mut() {
88
                            msgs.push(LayoutDebugMessage::info(format!(
88
                                "[process_block_children] Creating anon wrapper for inline run: {:?}",
88
                                inline_run
88
                                    .iter()
88
                                    .map(|c: &NodeId| c.index())
88
                                    .collect::<Vec<_>>()
                            )));
                        }
88
                        let anon_idx = self.create_anonymous_node(
88
                            parent_idx,
88
                            AnonymousBoxType::InlineWrapper,
88
                            FormattingContext::Block {
88
                                // Anonymous wrappers are BFC roots
88
                                establishes_new_context: true,
88
                            },
                        );
88
                        for inline_child_id in inline_run.drain(..) {
88
                            self.process_node(
88
                                styled_dom,
88
                                inline_child_id,
88
                                Some(anon_idx),
88
                                debug_messages,
                            )?;
                        }
                    }
4400
                }
                // Process the block-level child directly
7964
                if let Some(msgs) = debug_messages.as_mut() {
7964
                    msgs.push(LayoutDebugMessage::info(format!(
7964
                        "[process_block_children] Processing block child DOM {}",
7964
                        child_id.index()
7964
                    )));
7964
                }
7964
                self.process_node(styled_dom, child_id, Some(parent_idx), debug_messages)?;
5500
            } else {
5500
                inline_run.push(child_id);
5500
            }
        }
        // Process any remaining inline children at the end — skip if all whitespace
6336
        if !inline_run.is_empty() {
1936
            let all_whitespace = inline_run
1936
                .iter()
1936
                .all(|id| is_whitespace_only_text(styled_dom, *id));
1936
            if all_whitespace {
1848
                if let Some(msgs) = debug_messages.as_mut() {
1848
                    msgs.push(LayoutDebugMessage::info(format!(
1848
                        "[process_block_children] Skipping trailing whitespace-only inline run: {:?}",
1848
                        inline_run.iter().map(|c| c.index()).collect::<Vec<_>>()
                    )));
                }
            } else {
88
                if let Some(msgs) = debug_messages.as_mut() {
88
                    msgs.push(LayoutDebugMessage::info(format!(
88
                        "[process_block_children] Creating anon wrapper for remaining inline run: {:?}",
88
                        inline_run.iter().map(|c| c.index()).collect::<Vec<_>>()
                    )));
                }
88
                let anon_idx = self.create_anonymous_node(
88
                    parent_idx,
88
                    AnonymousBoxType::InlineWrapper,
88
                    FormattingContext::Block {
88
                        establishes_new_context: true, // Anonymous wrappers are BFC roots
88
                    },
                );
176
                for inline_child_id in inline_run {
88
                    self.process_node(
88
                        styled_dom,
88
                        inline_child_id,
88
                        Some(anon_idx),
88
                        debug_messages,
                    )?;
                }
            }
4400
        }
6336
        Ok(())
10824
    }
    // +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
264
    pub fn create_anonymous_node(
264
        &mut self,
264
        parent: usize,
264
        anon_type: AnonymousBoxType,
264
        fc: FormattingContext,
264
    ) -> usize {
264
        let index = self.nodes.len();
        // +spec:display-property:e67146 - Anonymous boxes inherit from enclosing non-anonymous box; non-inherited props use initial values
264
        let parent_fc = self.nodes.get(parent).map(|n| n.formatting_context.clone());
264
        self.nodes.push(LayoutNode {
264
            // ── HOT ──
264
            box_props: BoxProps::default(),
264
            dom_node_id: None,
264
            children: Vec::new(),
264
            used_size: None,
264
            formatting_context: fc,
264
            parent: Some(parent),
264
            // ── WARM ──
264
            intrinsic_sizes: None,
264
            baseline: None,
264
            inline_layout_result: None,
264
            scrollbar_info: None,
264
            relative_position: None,
264
            overflow_content_size: None,
264
            taffy_cache: TaffyCache::new(),
264
            computed_style: ComputedLayoutStyle::default(),
264
            pseudo_element: None,
264
            escaped_top_margin: None,
264
            escaped_bottom_margin: None,
264
            parent_formatting_context: parent_fc,
264
            ifc_membership: None,
264
            containing_block_index: None,
264
            // ── COLD ──
264
            anonymous_type: Some(anon_type),
264
            node_data_fingerprint: NodeDataFingerprint::default(),
264
            subtree_hash: SubtreeHash(0),
264
            dirty_flag: DirtyFlag::Layout,
264
            unresolved_box_props: crate::solver3::geometry::UnresolvedBoxProps::default(),
264
            ifc_id: None,
264
        });
264
        self.nodes[parent].children.push(index);
264
        index
264
    }
    /// 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 `?`.
    /// Apply CSS Display 3 §2.7/§2.8 blockification to a freshly-created node:
    /// a flex/grid item (or root / abs-pos / floated box) whose specified display
    /// is inline-level computes to its block-level equivalent.
    ///
    /// `process_node` (the full tree build) does this inline, but the INCREMENTAL
    /// tree builder (`cache.rs` reconcile → `create_node_from_dom`) bypassed it.
    /// Without it, a replaced inline flex item — e.g. an `<img>` canvas with
    /// `flex-grow: 1` (AzulPaint) — stayed inline, so its flex-grow was ignored
    /// and it was laid out 300×0 (the replaced-element default width, 0 height).
    /// Must be called AFTER the node is created and AFTER its parent's
    /// formatting context is known (the build is top-down, so the parent exists).
42284
    pub fn blockify_node_display(
42284
        &mut self,
42284
        styled_dom: &StyledDom,
42284
        dom_id: NodeId,
42284
        node_idx: usize,
42284
        parent_idx: Option<usize>,
42284
    ) {
42284
        let node_data = &styled_dom.node_data.as_container()[dom_id];
        // CSS Display 3 §2.4: a replaced element with a layout-internal display
        // value uses 'inline' — so it's inline-level and thus blockifiable.
42284
        let raw_display = {
42284
            let d = get_display_type(styled_dom, dom_id);
42284
            if d.is_layout_internal() && is_replaced_element(node_data) {
                LayoutDisplay::Inline
            } else {
42284
                d
            }
        };
42284
        let (position, float) = self
42284
            .nodes
42284
            .get(node_idx)
42284
            .map(|n| (n.computed_style.position, n.computed_style.float))
42284
            .unwrap_or_default();
42284
        let is_absolute_or_fixed =
42284
            matches!(position, LayoutPosition::Absolute | LayoutPosition::Fixed);
42284
        let is_floated = float != LayoutFloat::None;
42284
        let is_root = parent_idx.is_none();
42284
        let is_flex_grid_child = parent_idx
42284
            .and_then(|p| self.nodes.get(p))
42284
            .map(|n| {
33000
                matches!(
37708
                    n.formatting_context,
                    FormattingContext::Flex | FormattingContext::Grid
                )
37708
            })
42284
            .unwrap_or(false);
42284
        let display_type = crate::solver3::getters::get_computed_display(
42284
            raw_display,
42284
            is_absolute_or_fixed,
42284
            is_floated,
42284
            is_root,
42284
            is_flex_grid_child,
        );
42284
        if display_type != raw_display {
7392
            if let Some(node) = self.nodes.get_mut(node_idx) {
7392
                node.computed_style.display = display_type;
7392
                node.formatting_context =
7392
                    determine_formatting_context_for_display(styled_dom, dom_id, display_type);
7392
            }
34892
        }
42284
    }
55352
    pub fn create_node_from_dom(
55352
        &mut self,
55352
        styled_dom: &StyledDom,
55352
        dom_id: NodeId,
55352
        parent: Option<usize>,
55352
        debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
55352
    ) -> usize {
55352
        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.
55352
        { let _ = (0xCE00_0000u32 | (index as u32 & 0xffff)); }
55352
        let parent_fc =
55352
            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).
55352
        { let _ = (0xCD00_0001u32 | ((parent_fc.is_some() as u32) << 8)); }
55352
        let collected = collect_box_props(styled_dom, dom_id, debug_messages, self.viewport_size);
55352
        { let _ = (0xCA00_0001u32); }
55352
        self.nodes.push(LayoutNode {
            // ── HOT ──
55352
            box_props: collected.resolved,
55352
            dom_node_id: Some(dom_id),
55352
            children: Vec::new(),
55352
            used_size: None,
55352
            formatting_context: determine_formatting_context(styled_dom, dom_id),
55352
            parent,
            // ── WARM ──
55352
            intrinsic_sizes: None,
55352
            baseline: None,
55352
            inline_layout_result: None,
55352
            scrollbar_info: None,
55352
            relative_position: None,
55352
            overflow_content_size: None,
55352
            taffy_cache: TaffyCache::new(),
            // +spec:overflow:8f9f7e - viewport overflow propagation: visible→auto, clip→hidden
            computed_style: {
55352
                let mut style = compute_layout_style(styled_dom, dom_id);
55352
                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;
7172
                    if style.overflow_x == LayoutOverflow::Visible {
6864
                        style.overflow_x = LayoutOverflow::Auto;
6864
                    } else if style.overflow_x == LayoutOverflow::Clip {
                        style.overflow_x = LayoutOverflow::Hidden;
308
                    }
7172
                    if style.overflow_y == LayoutOverflow::Visible {
6864
                        style.overflow_y = LayoutOverflow::Auto;
6864
                    } else if style.overflow_y == LayoutOverflow::Clip {
                        style.overflow_y = LayoutOverflow::Hidden;
308
                    }
48180
                }
55352
                style
            },
55352
            pseudo_element: None,
55352
            escaped_top_margin: None,
55352
            escaped_bottom_margin: None,
55352
            parent_formatting_context: parent_fc,
55352
            ifc_membership: None,
55352
            containing_block_index: None,
            // ── COLD ──
55352
            anonymous_type: None,
55352
            node_data_fingerprint: NodeDataFingerprint::compute(
55352
                &styled_dom.node_data.as_container()[dom_id],
55352
                styled_dom.styled_nodes.as_container().get(dom_id).map(|n| &n.styled_node_state),
            ),
55352
            subtree_hash: SubtreeHash(0),
55352
            dirty_flag: DirtyFlag::Layout,
55352
            unresolved_box_props: collected.unresolved,
55352
            ifc_id: None,
        });
55352
        { let _ = (0xCB00_0001u32 | ((self.nodes.len() as u32 & 0xff) << 8)); }
55352
        if let Some(p) = parent {
48180
            self.nodes[p].children.push(index);
48180
        }
55352
        self.dom_to_layout.entry(dom_id).or_default().push(index);
        // DEBUG (2026-06-02 children-None tree-build): count create_node_from_dom
        // calls @0x40500 + record each dom_id into a 14-slot ring @0x40504. REVERT
        // before commit. Runs only in lifted wasm (server lifts, never runs natively).
        unsafe {
55352
            let c = crate::az_mark_read(0x40500);
55352
            crate::az_mark((0x60500) as u32, (c.wrapping_add(1)) as u32);
55352
            if (c as usize) < 14 {
55352
                crate::az_mark(((0x40504 + (c as usize) * 4)) as u32, (0xDD000000 | (dom_id.index() as u32 & 0xffff)) as u32);
55352
            }
        }
55352
        index
55352
    }
2200
    pub fn clone_node_from_old(&mut self, old_node: &LayoutNode, parent: Option<usize>) -> usize {
2200
        let index = self.nodes.len();
2200
        let mut new_node = old_node.clone();
2200
        new_node.parent = parent;
        new_node.parent_formatting_context =
2200
            parent.and_then(|p| self.nodes.get(p).map(|n| n.formatting_context.clone()));
2200
        new_node.children = Vec::new();
2200
        new_node.dirty_flag = DirtyFlag::None;
2200
        self.nodes.push(new_node);
2200
        if let Some(p) = parent {
2068
            self.nodes[p].children.push(index);
2068
        }
2200
        if let Some(dom_id) = old_node.dom_node_id {
2200
            self.dom_to_layout.entry(dom_id).or_default().push(index);
2200
        }
2200
        index
2200
    }
7304
    pub fn build(self, root_idx: usize) -> LayoutTree {
7304
        let nodes = self.nodes;
7304
        let node_count = nodes.len();
        // Flatten per-node children Vecs into a single contiguous arena.
57816
        let total_children: usize = nodes.iter().map(|n| n.children.len()).sum();
7304
        let mut arena = Vec::with_capacity(total_children);
7304
        let mut offsets = Vec::with_capacity(node_count);
        // Split monolithic LayoutNodes into hot/warm/cold SoA arrays
7304
        let mut hot_nodes = Vec::with_capacity(node_count);
7304
        let mut warm_nodes = Vec::with_capacity(node_count);
7304
        let mut cold_nodes = Vec::with_capacity(node_count);
65120
        for node in nodes {
57816
            // Flatten children into arena first
57816
            let start = arena.len() as u32;
57816
            let len = node.children.len() as u32;
57816
            arena.extend_from_slice(&node.children);
57816
            offsets.push((start, len));
57816

            
57816
            // Split into hot/warm/cold
57816
            let (hot, warm, cold) = node.split();
57816
            hot_nodes.push(hot);
57816
            warm_nodes.push(warm);
57816
            cold_nodes.push(cold);
57816
        }
        // 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.
7304
        LayoutTree {
7304
            nodes: hot_nodes,
7304
            warm: warm_nodes,
7304
            cold: cold_nodes,
7304
            root: root_idx,
7304
            dom_to_layout: self.dom_to_layout,
7304
            children_arena: arena,
7304
            children_offsets: offsets,
7304
            // Populated by `generate_layout_tree` after the tree is built,
7304
            // since the computation needs styled_dom for float/position lookup.
7304
            subtree_needs_intrinsic: Vec::new(),
7304
        }
7304
    }
}
// +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)
127864
pub fn is_block_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
55704
    matches!(
127864
        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
    )
127864
}
// +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
22132
fn is_inline_level(styled_dom: &StyledDom, node_id: NodeId) -> bool {
    // Text nodes are always inline-level
22132
    let node_data = &styled_dom.node_data.as_container()[node_id];
22132
    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
12804
        return true;
9328
    }
    // Check the display property
8932
    matches!(
9328
        get_display_type(styled_dom, node_id),
        LayoutDisplay::Inline
            | LayoutDisplay::InlineBlock
            | LayoutDisplay::InlineTable
            | LayoutDisplay::InlineFlex
            | LayoutDisplay::InlineGrid
    )
22132
}
// +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
25212
pub(crate) fn has_only_inline_children(styled_dom: &StyledDom, node_id: NodeId) -> bool {
25212
    let hierarchy = styled_dom.node_hierarchy.as_container();
25212
    let node_hier = match hierarchy.get(node_id) {
25212
        Some(n) => n,
        None => {
            return false;
        }
    };
    // Get the first child
25212
    let mut current_child = node_hier.first_child_id(node_id);
    // If there are no children, it's not an IFC (it's empty)
25212
    if current_child.is_none() {
5192
        return false;
20020
    }
    // Check all children
33220
    while let Some(child_id) = current_child {
22132
        let is_inline = is_inline_level(styled_dom, child_id);
22132
        if !is_inline {
            // Found a block-level child
8932
            return false;
13200
        }
        // Move to next sibling
13200
        if let Some(child_hier) = hierarchy.get(child_id) {
13200
            current_child = child_hier.next_sibling_id();
13200
        } else {
            break;
        }
    }
    // All children are inline-level
11088
    true
25212
}
/// 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²)).
55352
fn compute_layout_style(styled_dom: &StyledDom, dom_id: NodeId) -> ComputedLayoutStyle {
55352
    let styled_node_state = styled_dom
55352
        .styled_nodes
55352
        .as_container()
55352
        .get(dom_id)
55352
        .map(|n| n.styled_node_state.clone())
55352
        .unwrap_or_default();
    // Get display property
55352
    let display = match get_display_property(styled_dom, Some(dom_id)) {
55352
        MultiValue::Exact(d) => d,
        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => LayoutDisplay::Block,
    };
    // Get position property
55352
    let position = get_position(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
    // Get float property  
55352
    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
55352
    let is_replaced = matches!(
55352
        styled_dom.node_data.as_container()[dom_id].get_node_type(),
        NodeType::Image(_) | NodeType::VirtualView
    );
55352
    let overflow_x = {
55352
        let v = get_overflow_x(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
55352
        if is_replaced && v == LayoutOverflow::Hidden { LayoutOverflow::Clip } else { v }
    };
55352
    let overflow_y = {
55352
        let v = get_overflow_y(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
55352
        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
55352
    let writing_mode = {
55352
        let own_wm = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
55352
        let nd = &styled_dom.node_data.as_container()[dom_id];
55352
        if matches!(nd.node_type, NodeType::Html) {
            // If root <html>, propagate writing-mode from first <body> child
2508
            styled_dom
2508
                .node_hierarchy
2508
                .as_container()
2508
                .get(dom_id)
2508
                .and_then(|node| node.first_child_id(dom_id))
2508
                .and_then(|child_id| {
2508
                    let child_data = &styled_dom.node_data.as_container()[child_id];
2508
                    if matches!(child_data.node_type, NodeType::Body) {
2464
                        let child_state = &styled_dom
2464
                            .styled_nodes
2464
                            .as_container()[child_id]
2464
                            .styled_node_state;
2464
                        Some(get_writing_mode(styled_dom, child_id, child_state)
2464
                            .unwrap_or_default())
                    } else {
44
                        None
                    }
2508
                })
2508
                .unwrap_or(own_wm)
        } else {
52844
            own_wm
        }
    };
55352
    let direction = get_direction(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
55352
    let text_orientation = get_text_orientation(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
    // Get text-align
55352
    let text_align = get_text_align(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
    // Get explicit width/height (None = auto)
55352
    let width = match get_css_width(styled_dom, dom_id, &styled_node_state) {
12584
        MultiValue::Exact(w) => Some(w),
42768
        _ => None,
    };
55352
    let height = match get_css_height(styled_dom, dom_id, &styled_node_state) {
13684
        MultiValue::Exact(h) => Some(h),
41668
        _ => None,
    };
    // Get min/max constraints
55352
    let min_width = match get_css_min_width(styled_dom, dom_id, &styled_node_state) {
        MultiValue::Exact(v) => Some(v),
55352
        _ => None,
    };
55352
    let min_height = match get_css_min_height(styled_dom, dom_id, &styled_node_state) {
352
        MultiValue::Exact(v) => Some(v),
55000
        _ => None,
    };
55352
    let max_width = match get_css_max_width(styled_dom, dom_id, &styled_node_state) {
44
        MultiValue::Exact(v) => Some(v),
55308
        _ => None,
    };
55352
    let max_height = match get_css_max_height(styled_dom, dom_id, &styled_node_state) {
        MultiValue::Exact(v) => Some(v),
55352
        _ => None,
    };
55352
    ComputedLayoutStyle {
55352
        display,
55352
        position,
55352
        float,
55352
        overflow_x,
55352
        overflow_y,
55352
        writing_mode,
55352
        direction,
55352
        text_orientation,
55352
        width,
55352
        height,
55352
        min_width,
55352
        min_height,
55352
        max_width,
55352
        max_height,
55352
        text_align,
55352
    }
55352
}
// hash_node_data() removed — replaced by NodeDataFingerprint::compute()
/// Helper function to get element's computed font-size
158884
fn get_element_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
158884
    { let _ = (0xC3_000001u32); } // 2-arg wrapper entered
158884
    let node_state = styled_dom
158884
        .styled_nodes
158884
        .as_container()
158884
        .get(dom_id)
158884
        .map(|n| &n.styled_node_state)
158884
        .cloned()
158884
        .unwrap_or_default();
158884
    { let _ = (0xC3_000002u32); } // after node_state (clone); next = 3-arg call
158884
    crate::solver3::getters::get_element_font_size(styled_dom, dom_id, &node_state)
158884
}
/// Helper function to get parent's computed font-size
55352
fn get_parent_font_size(styled_dom: &StyledDom, dom_id: NodeId) -> f32 {
55352
    styled_dom
55352
        .node_hierarchy
55352
        .as_container()
55352
        .get(dom_id)
55352
        .and_then(|node| node.parent_id())
55352
        .map(|parent_id| get_element_font_size(styled_dom, parent_id))
55352
        .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE)
55352
}
/// Helper function to get root element's font-size
55352
fn get_root_font_size(styled_dom: &StyledDom) -> f32 {
    // Root is always NodeId(0) in Azul
55352
    get_element_font_size(styled_dom, NodeId::new(0))
55352
}
/// Create a ResolutionContext for a given node
55352
fn create_resolution_context(
55352
    styled_dom: &StyledDom,
55352
    dom_id: NodeId,
55352
    containing_block_size: Option<azul_css::props::basic::PhysicalSize>,
55352
    viewport_size: LogicalSize,
55352
) -> azul_css::props::basic::ResolutionContext {
55352
    { let _ = (0xC1_000001u32); } // create_resolution_context entered
55352
    let element_font_size = get_element_font_size(styled_dom, dom_id);
55352
    { let _ = (0xC1_000002u32); } // after get_element_font_size
55352
    let parent_font_size = get_parent_font_size(styled_dom, dom_id);
55352
    { let _ = (0xC1_000003u32); } // after get_parent_font_size
55352
    let root_font_size = get_root_font_size(styled_dom);
55352
    { let _ = (0xC1_000004u32); } // after get_root_font_size
55352
    ResolutionContext {
55352
        element_font_size,
55352
        parent_font_size,
55352
        root_font_size,
55352
        // +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
55352
        containing_block_size: containing_block_size.unwrap_or(PhysicalSize::new(0.0, 0.0)),
55352
        element_size: None, // Not yet laid out
55352
        viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
55352
    }
55352
}
/// 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.
55352
fn collect_box_props(
55352
    styled_dom: &StyledDom,
55352
    dom_id: NodeId,
55352
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
55352
    viewport_size: LogicalSize,
55352
) -> CollectedBoxProps {
    use crate::solver3::geometry::{UnresolvedBoxProps, UnresolvedEdge, UnresolvedMargin};
    use crate::solver3::getters::*;
    // before create_node step A is the diverging call.
55352
    { let _ = (0xC0_000001u32); } // entered
55352
    let node_data = &styled_dom.node_data.as_container()[dom_id];
    // Get styled node state
55352
    let node_state = styled_dom
55352
        .styled_nodes
55352
        .as_container()
55352
        .get(dom_id)
55352
        .map(|n| &n.styled_node_state)
55352
        .cloned()
55352
        .unwrap_or_default();
55352
    { 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
55352
    let context = create_resolution_context(styled_dom, dom_id, None, viewport_size);
55352
    { let _ = (0xC0_000003u32); } // after create_resolution_context
    // Read margin values from styled_dom
55352
    let margin_top_mv = get_css_margin_top(styled_dom, dom_id, &node_state);
55352
    { let _ = (0xC0_000004u32); } // after get_css_margin_top
55352
    let margin_right_mv = get_css_margin_right(styled_dom, dom_id, &node_state);
55352
    let margin_bottom_mv = get_css_margin_bottom(styled_dom, dom_id, &node_state);
55352
    let margin_left_mv = get_css_margin_left(styled_dom, dom_id, &node_state);
    // Convert MultiValue to UnresolvedMargin
221408
    let to_unresolved_margin = |mv: &MultiValue<PixelValue>| -> UnresolvedMargin {
221408
        match mv {
88
            MultiValue::Auto => UnresolvedMargin::Auto,
221320
            MultiValue::Exact(pv) => UnresolvedMargin::Length(*pv),
            _ => UnresolvedMargin::Zero,
        }
221408
    };
    // Build unresolved margins
55352
    let unresolved_margin = UnresolvedEdge {
55352
        top: to_unresolved_margin(&margin_top_mv),
55352
        right: to_unresolved_margin(&margin_right_mv),
55352
        bottom: to_unresolved_margin(&margin_bottom_mv),
55352
        left: to_unresolved_margin(&margin_left_mv),
55352
    };
55352
    { let _ = (0xC0_000005u32); } // after margin block
    // Read padding values
55352
    let padding_top_mv = get_css_padding_top(styled_dom, dom_id, &node_state);
55352
    let padding_right_mv = get_css_padding_right(styled_dom, dom_id, &node_state);
55352
    let padding_bottom_mv = get_css_padding_bottom(styled_dom, dom_id, &node_state);
55352
    let padding_left_mv = get_css_padding_left(styled_dom, dom_id, &node_state);
    // Convert MultiValue to PixelValue (default to 0px)
248556
    let to_pixel_value = |mv: MultiValue<PixelValue>| -> PixelValue {
248556
        match mv {
248556
            MultiValue::Exact(pv) => pv,
            _ => PixelValue::const_px(0),
        }
248556
    };
    // Build unresolved padding
55352
    let unresolved_padding = UnresolvedEdge {
55352
        top: to_pixel_value(padding_top_mv),
55352
        right: to_pixel_value(padding_right_mv),
55352
        bottom: to_pixel_value(padding_bottom_mv),
55352
        left: to_pixel_value(padding_left_mv),
55352
    };
55352
    { 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).
55352
    let unresolved_padding = match get_display_type(styled_dom, dom_id) {
        LayoutDisplay::TableRow
        | LayoutDisplay::TableRowGroup
        | LayoutDisplay::TableHeaderGroup
        | LayoutDisplay::TableFooterGroup
        | LayoutDisplay::TableColumn
1496
        | LayoutDisplay::TableColumnGroup => UnresolvedEdge {
1496
            top: PixelValue::const_px(0),
1496
            right: PixelValue::const_px(0),
1496
            bottom: PixelValue::const_px(0),
1496
            left: PixelValue::const_px(0),
1496
        },
53856
        _ => unresolved_padding,
    };
55352
    { let _ = (0xC0_000006u32); } // after padding block
    // Read border values
55352
    let border_top_mv = get_css_border_top_width(styled_dom, dom_id, &node_state);
55352
    let border_right_mv = get_css_border_right_width(styled_dom, dom_id, &node_state);
55352
    let border_bottom_mv = get_css_border_bottom_width(styled_dom, dom_id, &node_state);
55352
    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;
221408
    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.
55352
    let (bs_top, bs_right, bs_bottom, bs_left) = {
55352
        let cache_ptr = &styled_dom.css_property_cache.ptr;
55352
        if node_state.is_normal() {
55352
            if let Some(ref cc) = cache_ptr.compact_cache {
55352
                let idx = dom_id.index();
55352
                (cc.get_border_top_style(idx), cc.get_border_right_style(idx),
55352
                 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
55352
    let unresolved_border = UnresolvedEdge {
55352
        top: if style_zeroes_width(bs_top) { PixelValue::const_px(0) } else { to_pixel_value(border_top_mv) },
55352
        right: if style_zeroes_width(bs_right) { PixelValue::const_px(0) } else { to_pixel_value(border_right_mv) },
55352
        bottom: if style_zeroes_width(bs_bottom) { PixelValue::const_px(0) } else { to_pixel_value(border_bottom_mv) },
55352
        left: if style_zeroes_width(bs_left) { PixelValue::const_px(0) } else { to_pixel_value(border_left_mv) },
    };
55352
    { 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)
55352
    let display_type = get_display_type(styled_dom, dom_id);
55352
    let unresolved_margin = match display_type {
        LayoutDisplay::TableRow
        | LayoutDisplay::TableRowGroup
        | LayoutDisplay::TableHeaderGroup
        | LayoutDisplay::TableFooterGroup
        | LayoutDisplay::TableCell
        | LayoutDisplay::TableColumn
5236
        | LayoutDisplay::TableColumnGroup => UnresolvedEdge {
5236
            top: UnresolvedMargin::Zero,
5236
            right: UnresolvedMargin::Zero,
5236
            bottom: UnresolvedMargin::Zero,
5236
            left: UnresolvedMargin::Zero,
5236
        },
        // +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 => {
20724
            let is_replaced = matches!(
20724
                node_data.get_node_type(),
                NodeType::Image(_) | NodeType::VirtualView
            );
20724
            if is_replaced {
                unresolved_margin
            } else {
20724
                UnresolvedEdge {
20724
                    top: UnresolvedMargin::Zero,
20724
                    bottom: UnresolvedMargin::Zero,
20724
                    ..unresolved_margin
20724
                }
            }
        },
29392
        _ => unresolved_margin,
    };
    // Build the UnresolvedBoxProps
55352
    let unresolved = UnresolvedBoxProps {
55352
        margin: unresolved_margin,
55352
        padding: unresolved_padding,
55352
        border: unresolved_border,
55352
    };
    // Create initial resolution params (with viewport as containing block for now)
55352
    let params = crate::solver3::geometry::ResolutionParams {
55352
        containing_block: viewport_size,
55352
        viewport_size,
55352
        element_font_size: context.parent_font_size,
55352
        root_font_size: context.root_font_size,
55352
    };
    // Resolve to get initial box_props
55352
    let resolved = unresolved.resolve(&params);
    // Debug ALL node box props (padding, margin, border) for cascade debugging
55352
    if let Some(msgs) = debug_messages.as_mut() {
54208
        msgs.push(LayoutDebugMessage::box_props(format!(
54208
            "[BOX] node[{}] {:?} pad=[{:.1} {:.1} {:.1} {:.1}] mar=[{:.1} {:.1} {:.1} {:.1}] bor=[{:.1} {:.1} {:.1} {:.1}]",
54208
            dom_id.index(), node_data.node_type,
54208
            resolved.padding.top, resolved.padding.right, resolved.padding.bottom, resolved.padding.left,
54208
            resolved.margin.top, resolved.margin.right, resolved.margin.bottom, resolved.margin.left,
54208
            resolved.border.top, resolved.border.right, resolved.border.bottom, resolved.border.left,
54208
        )));
54208
    }
    // Debug nodes with non-zero margins or vh units
55352
    if let Some(msgs) = debug_messages.as_mut() {
        // Check if any margin uses vh
54208
        let has_vh = match &unresolved_margin.top {
28248
            UnresolvedMargin::Length(pv) => pv.metric == azul_css::props::basic::SizeMetric::Vh,
25960
            _ => false,
        };
54208
        if has_vh || resolved.margin.top > 0.0 || resolved.margin.left > 0.0 {
5896
            msgs.push(LayoutDebugMessage::box_props(format!(
5896
                "NodeId {:?} ({:?}): unresolved_margin_top={:?}, resolved_margin_top={:.2}, viewport_size={:?}",
5896
                dom_id, node_data.node_type,
5896
                unresolved_margin.top,
5896
                resolved.margin.top,
5896
                viewport_size
5896
            )));
48312
        }
1144
    }
    // Debug margin_auto detection
55352
    if let Some(msgs) = debug_messages.as_mut() {
54208
        msgs.push(LayoutDebugMessage::box_props(format!(
54208
            "NodeId {:?} ({:?}): margin_auto: left={}, right={}, top={}, bottom={} | margin_left={:?}",
54208
            dom_id, node_data.node_type,
54208
            resolved.margin_auto.left, resolved.margin_auto.right,
54208
            resolved.margin_auto.top, resolved.margin_auto.bottom,
54208
            unresolved_margin.left
54208
        )));
54208
    }
    // Debug for Body nodes
55352
    if matches!(node_data.node_type, azul_core::dom::NodeType::Body) {
4224
        if let Some(msgs) = debug_messages.as_mut() {
4180
            msgs.push(LayoutDebugMessage::box_props(format!(
4180
                "Body margin resolved: top={:.2}, right={:.2}, bottom={:.2}, left={:.2}",
4180
                resolved.margin.top, resolved.margin.right,
4180
                resolved.margin.bottom, resolved.margin.left
4180
            )));
4180
        }
51128
    }
55352
    CollectedBoxProps { unresolved, resolved }
55352
}
/// 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)
16412
pub fn is_whitespace_only_text(styled_dom: &StyledDom, node_id: NodeId) -> bool {
16412
    let binding = styled_dom.node_data.as_container();
16412
    let node_data = binding.get(node_id);
16412
    if let Some(data) = node_data {
16412
        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
46508
            if !text.chars().all(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')) {
440
                return false;
5412
            }
            // 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.
5412
            let white_space = styled_dom
5412
                .styled_nodes
5412
                .as_container()
5412
                .get(node_id)
5412
                .map(|n| {
5412
                    match get_white_space_property(styled_dom, node_id, &n.styled_node_state) {
5412
                        MultiValue::Exact(ws) => ws,
                        _ => StyleWhiteSpace::Normal,
                    }
5412
                })
5412
                .unwrap_or(StyleWhiteSpace::Normal);
5412
            return match white_space {
                // These values collapse whitespace — whitespace-only text is collapsible
5324
                StyleWhiteSpace::Normal | StyleWhiteSpace::Nowrap | StyleWhiteSpace::PreLine => true,
                // These values preserve whitespace — whitespace-only text is NOT collapsible
88
                StyleWhiteSpace::Pre | StyleWhiteSpace::PreWrap | StyleWhiteSpace::BreakSpaces => false,
            };
10560
        }
    }
10560
    false
16412
}
/// 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.)
492976
pub fn get_display_type(styled_dom: &StyledDom, node_id: NodeId) -> LayoutDisplay {
    use crate::solver3::getters::get_display_property;
492976
    get_display_property(styled_dom, Some(node_id)).unwrap_or(LayoutDisplay::Inline)
492976
}
// +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.
836
fn blockify_flex_item_if_table_internal(nodes: &mut Vec<LayoutNode>, node_idx: usize) {
836
    if let Some(node) = nodes.get_mut(node_idx) {
836
        let is_table_internal = matches!(
836
            node.formatting_context,
            FormattingContext::TableCell
                | FormattingContext::TableRow
                | FormattingContext::TableRowGroup
                | FormattingContext::TableColumnGroup
                | FormattingContext::TableCaption
                | FormattingContext::Table
        );
836
        if is_table_internal {
            node.formatting_context = FormattingContext::Block {
                establishes_new_context: true,
            };
836
        }
    }
836
}
/// 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.
17116
fn is_replaced_element(node_data: &NodeData) -> bool {
16852
    matches!(
17116
        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
    )
17116
}
// +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.
14124
fn establishes_new_block_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> bool {
14124
    let display = get_display_type(styled_dom, node_id);
13904
    if matches!(
14124
        display,
        LayoutDisplay::InlineBlock | LayoutDisplay::TableCell | LayoutDisplay::TableCaption | LayoutDisplay::FlowRoot
    ) {
220
        return true;
13904
    }
13904
    if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
13904
        let overflow_x = get_overflow_x(styled_dom, node_id, &styled_node.styled_node_state);
13904
        if !overflow_x.is_visible_or_clip() {
792
            return true;
13112
        }
13112
        let overflow_y = get_overflow_y(styled_dom, node_id, &styled_node.styled_node_state);
13112
        if !overflow_y.is_visible_or_clip() {
            return true;
13112
        }
13112
        let position = get_position(styled_dom, node_id, &styled_node.styled_node_state);
13112
        if position.is_absolute_or_fixed() {
132
            return true;
12980
        }
12980
        let float = get_float(styled_dom, node_id, &styled_node.styled_node_state);
12980
        if !float.is_none() {
1100
            return true;
11880
        }
    }
    // CSS Writing Modes 4 § 3.2: block container with different writing-mode than parent establishes BFC
11880
    if let Some(styled_node) = styled_dom.styled_nodes.as_container().get(node_id) {
11880
        let hierarchy = styled_dom.node_hierarchy.as_container();
11880
        if let Some(parent_dom_id) = hierarchy[node_id].parent_id() {
7392
            let parent_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
7392
            let child_wm = get_writing_mode(styled_dom, node_id, &styled_node.styled_node_state).unwrap_or_default();
7392
            let parent_wm = get_writing_mode(styled_dom, parent_dom_id, parent_state).unwrap_or_default();
7392
            if child_wm != parent_wm {
                return true;
7392
            }
4488
        }
    }
    // +spec:replaced-elements:4f494d - replaced elements always establish an independent formatting context
11880
    let node_data = &styled_dom.node_data.as_container()[node_id];
11880
    if is_replaced_element(node_data) {
264
        return true;
11616
    }
    // The root element (<html>) also establishes a BFC.
11616
    if styled_dom.root.into_crate_internal() == Some(node_id) {
4488
        return true;
7128
    }
7128
    false
14124
}
// +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)
46772
fn determine_formatting_context_for_display(
46772
    styled_dom: &StyledDom,
46772
    node_id: NodeId,
46772
    display_type: LayoutDisplay,
46772
) -> FormattingContext {
46772
    let node_data = &styled_dom.node_data.as_container()[node_id];
46772
    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
        // [g147h az-web-lift DIAG] CONSTANT marker of the COMPUTED FC per DOM node_id (0x60B60+slot),
        // written WITHOUT reading the stored field. 1=text→Inline, 2=block-with-inline→Inline, 4=Block.
        // For the divs (node_id 1,3): 2 ⇒ computed Inline correctly (bug is store/clone/read); 4 ⇒
        // has_only_inline_children mis-lifted to false (computed Block).
        #[cfg(feature = "web_lift")]
        unsafe { crate::az_mark(((0x60B60 + (node_id.index() & 7) * 4)) as u32, (0xC0DE0001) as u32); }
7436
        return FormattingContext::Inline;
39336
    }
    // +spec:display-property:2a8d62 - block containers with inline-level content establish an IFC
39336
    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)
4488
        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 => {
25212
            if has_only_inline_children(styled_dom, node_id) {
                #[cfg(feature = "web_lift")]
                unsafe { crate::az_mark(((0x60B60 + (node_id.index() & 7) * 4)) as u32, (0xC0DE0002) as u32); }
11088
                FormattingContext::Inline
            } else {
                #[cfg(feature = "web_lift")]
                unsafe { crate::az_mark(((0x60B60 + (node_id.index() & 7) * 4)) as u32, (0xC0DE0004) as u32); }
14124
                FormattingContext::Block {
14124
                    establishes_new_context: establishes_new_block_formatting_context(
14124
                        styled_dom, node_id,
14124
                    ),
14124
                }
            }
        }
308
        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)
1452
        LayoutDisplay::Table | LayoutDisplay::InlineTable => FormattingContext::Table,
        LayoutDisplay::TableRowGroup
        | LayoutDisplay::TableHeaderGroup
        | LayoutDisplay::TableFooterGroup => FormattingContext::TableRowGroup,
1496
        LayoutDisplay::TableRow => FormattingContext::TableRow,
3740
        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,
2640
        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,
            }
        }
    }
46772
}
/// The logic now correctly identifies all BFC roots.
55352
fn determine_formatting_context(styled_dom: &StyledDom, node_id: NodeId) -> FormattingContext {
55352
    let node_data = &styled_dom.node_data.as_container()[node_id];
    // [g147j az-web-lift DIAG] OUTER determine_ entry (0x60BB0+slot): 1=Text early-exit,
    // 0x10|disc = went through for_display and returned that repr(C,u8) discriminant.
    // Discriminates "never called during the lifted build" (slot stays 0) vs "called but
    // the for_display match mis-routes" (here=0x10|x while the g147h inner markers stay 0)
    // vs "value correct at build, corrupted later" (here says Inline, dispatch reads garbage).
55352
    if matches!(node_data.get_node_type(), NodeType::Text(_)) {
        #[cfg(feature = "web_lift")]
        unsafe { crate::az_mark(0x60BB0 + (node_id.index() & 7) as u32 * 4, 0xC0DE0001); }
16236
        return FormattingContext::Inline;
39116
    }
39116
    let display_type = get_display_type(styled_dom, node_id);
39116
    let fc = determine_formatting_context_for_display(styled_dom, node_id, display_type);
    #[cfg(feature = "web_lift")]
    unsafe {
        let disc: u8 = core::ptr::read_volatile((&fc) as *const FormattingContext as *const u8);
        crate::az_mark(0x60BB0 + (node_id.index() & 7) as u32 * 4, 0xC0DE0010 | disc as u32);
    }
39116
    fc
55352
}