1
//! Handling Viewport Resizing and Layout Thrashing
2
//!
3
//! The viewport size is a fundamental input to the entire layout process.
4
//! A change in viewport size must trigger a relayout.
5
//!
6
//! 1. The `layout_document` function takes the `viewport` as an argument. The `LayoutCache` stores
7
//!    the `viewport` from the previous frame.
8
//! 2. The `reconcile_and_invalidate` function detects that the viewport has changed size
9
//! 3. This single change—marking the root as a layout root—forces a full top-down pass
10
//!    (`calculate_layout_for_subtree` starting from the root). This correctly recalculates all
11
//!    percentage-based sizes and repositions all elements according to the new viewport dimensions.
12
//! 4. The intrinsic size calculation (bottom-up) can often be skipped, as it's independent of the
13
//!    container size, which is a significant optimization.
14

            
15
use std::{
16
    collections::{BTreeMap, BTreeSet, HashMap},
17
    hash::{DefaultHasher, Hash, Hasher},
18
};
19

            
20
/// Floating-point comparison epsilon for cache size lookups.
21
/// Controls the tolerance for cache hit matching in the per-node multi-slot cache.
22
const CACHE_SIZE_EPSILON: f32 = 0.1;
23

            
24
use azul_core::{
25
    diff::NodeDataFingerprint,
26
    dom::{FormattingContext, NodeId, NodeType},
27
    geom::{LogicalPosition, LogicalRect, LogicalSize},
28
    styled_dom::{StyledDom, StyledNode},
29
};
30
use azul_css::{
31
    css::CssPropertyValue,
32
    props::{
33
        layout::{
34
            LayoutDisplay, LayoutFlexWrap, LayoutHeight, LayoutJustifyContent, LayoutOverflow,
35
            LayoutPosition, LayoutWritingMode,
36
        },
37
        property::{CssProperty, CssPropertyType},
38
        style::StyleTextAlign,
39
    },
40
    LayoutDebugMessage, LayoutDebugMessageType,
41
};
42

            
43
use crate::{
44
    font_traits::{FontLoaderTrait, ParsedFontTrait, TextLayoutCache},
45
    solver3::{
46
        fc::{self, layout_formatting_context, LayoutConstraints, OverflowBehavior},
47
        geometry::PositionedRectangle,
48
        getters::{
49
            get_css_height, get_display_property, get_justify_content, get_overflow_x,
50
            get_overflow_y, get_scrollbar_gutter_property, get_text_align, get_white_space_property, get_wrap, get_writing_mode,
51
            MultiValue,
52
        },
53
        layout_tree::{
54
            get_display_type, is_block_level, AnonymousBoxType, DirtyFlag, LayoutNode, LayoutNodeHot, LayoutTreeBuilder, SubtreeHash,
55
        },
56
        positioning::get_position_type,
57
        scrollbar::ScrollbarRequirements,
58
        sizing::calculate_used_size_for_node,
59
        LayoutContext, LayoutError, LayoutTree, Result,
60
    },
61
    text3::cache::AvailableSpace as Text3AvailableSpace,
62
};
63

            
64
// ============================================================================
65
// Per-Node Multi-Slot Cache (inspired by Taffy's 9+1 slot cache architecture)
66
//
67
// Instead of a global BTreeMap keyed by (node_index, available_size), each node
68
// gets its own deterministic cache with 9 measurement slots + 1 full layout slot.
69
// This eliminates O(log n) lookups, prevents slot collisions between MinContent/
70
// MaxContent/Definite measurements, and cleanly separates sizing from positioning.
71
//
72
// Reference: https://github.com/DioxusLabs/taffy — Cache struct in src/tree/cache.rs
73
// Azul improvement: cache is EXTERNAL (Vec<NodeCache> parallel to LayoutTree.nodes)
74
// rather than stored on the node, keeping LayoutNode slim and avoiding &mut tree
75
// for cache operations.
76
// ============================================================================
77

            
78
/// Determines whether `calculate_layout_for_subtree` should only compute
79
/// the node's size (for parent's sizing pass) or perform full layout
80
/// including child positioning.
81
///
82
/// Inspired by Taffy's `RunMode` enum. The two-mode approach enables the
83
/// classic CSS two-pass layout: Pass 1 (ComputeSize) measures all children,
84
/// Pass 2 (PerformLayout) positions them using the measured sizes.
85
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86
pub enum ComputeMode {
87
    /// Only compute the node's border-box size and baseline.
88
    /// Does NOT store child positions. Used in BFC Pass 1 (sizing).
89
    ComputeSize,
90
    /// Compute size AND position all children.
91
    /// Stores the full layout result including child positions.
92
    /// Used in BFC Pass 2 (positioning) and as the final layout step.
93
    PerformLayout,
94
}
95

            
96
/// Constraint classification for deterministic cache slot selection.
97
///
98
/// Inspired by Taffy's `AvailableSpace` enum. Each constraint type maps to a
99
/// different cache slot, preventing collisions between e.g. MinContent and
100
/// Definite measurements of the same node.
101
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
102
pub enum AvailableWidthType {
103
    /// A definite pixel value (or percentage resolved to pixels).
104
    Definite,
105
    /// Shrink-to-fit: the smallest size that doesn't cause overflow.
106
    MinContent,
107
    /// Use all available space: the largest size the content can use.
108
    MaxContent,
109
}
110

            
111
/// Cache entry for sizing (ComputeSize mode) — stores NO positions.
112
///
113
/// This is the lightweight entry stored in the 9 measurement slots.
114
/// It records what constraints were provided and what size resulted,
115
/// enabling Taffy's "result matches request" optimization.
116
#[derive(Debug, Clone)]
117
pub struct SizingCacheEntry {
118
    /// The available size that was provided as input.
119
    pub available_size: LogicalSize,
120
    /// The computed border-box size (output).
121
    pub result_size: LogicalSize,
122
    /// Baseline for inline alignment (if applicable).
123
    pub baseline: Option<f32>,
124
    /// First child's escaped top margin (CSS 2.2 § 8.3.1).
125
    pub escaped_top_margin: Option<f32>,
126
    /// Last child's escaped bottom margin (CSS 2.2 § 8.3.1).
127
    pub escaped_bottom_margin: Option<f32>,
128
}
129

            
130
/// Cache entry for full layout (PerformLayout mode).
131
///
132
/// This is the single "final layout" slot. It includes child positions
133
/// (relative to parent's content-box) and overflow/scrollbar info.
134
#[derive(Debug, Clone)]
135
pub struct LayoutCacheEntry {
136
    /// The available size that was provided as input.
137
    pub available_size: LogicalSize,
138
    /// The computed border-box size (output).
139
    pub result_size: LogicalSize,
140
    /// Content overflow size (for scrolling).
141
    pub content_size: LogicalSize,
142
    /// Child positions relative to parent's content-box (NOT absolute).
143
    pub child_positions: Vec<(usize, LogicalPosition)>,
144
    /// First child's escaped top margin.
145
    pub escaped_top_margin: Option<f32>,
146
    /// Last child's escaped bottom margin.
147
    pub escaped_bottom_margin: Option<f32>,
148
    /// Scrollbar requirements for this node.
149
    pub scrollbar_info: ScrollbarRequirements,
150
}
151

            
152
/// Per-node cache entry with 9 measurement slots + 1 full layout slot.
153
///
154
/// Inspired by Taffy's `Cache` struct (9+1 slots per node). The deterministic
155
/// slot index is computed from the constraint combination, so entries never
156
/// clobber each other (unlike the old global BTreeMap where fixed-point
157
/// collisions were possible).
158
///
159
/// NOT stored on LayoutNode — lives in the external `LayoutCacheMap`.
160
#[derive(Debug, Clone)]
161
pub struct NodeCache {
162
    /// 9 measurement slots (Taffy's deterministic scheme):
163
    /// - Slot 0: both dimensions known
164
    /// - Slots 1-2: only width known (MaxContent/Definite vs MinContent)
165
    /// - Slots 3-4: only height known (MaxContent/Definite vs MinContent)
166
    /// - Slots 5-8: neither known (2×2 combos of width/height constraint types)
167
    pub measure_entries: [Option<SizingCacheEntry>; 9],
168

            
169
    /// 1 full layout slot (with child positions, overflow, baseline).
170
    /// Only populated after PerformLayout, not after ComputeSize.
171
    pub layout_entry: Option<LayoutCacheEntry>,
172

            
173
    /// Fast check for dirty propagation (Taffy optimization).
174
    /// When true, all slots are empty — ancestors are also dirty.
175
    pub is_empty: bool,
176
}
177

            
178
impl Default for NodeCache {
179
25445
    fn default() -> Self {
180
25445
        Self {
181
25445
            measure_entries: [None, None, None, None, None, None, None, None, None],
182
25445
            layout_entry: None,
183
25445
            is_empty: true, // fresh cache is empty/dirty
184
25445
        }
185
25445
    }
186
}
187

            
188
impl NodeCache {
189
    /// Clear all cache entries, marking this node as dirty.
190
18900
    pub fn clear(&mut self) {
191
18900
        self.measure_entries = [None, None, None, None, None, None, None, None, None];
192
18900
        self.layout_entry = None;
193
18900
        self.is_empty = true;
194
18900
    }
195

            
196
    /// Compute the deterministic slot index from constraint dimensions.
197
    ///
198
    /// This is Taffy's slot selection scheme: given whether width/height are
199
    /// "known" (definite constraint provided by parent) and what type of
200
    /// constraint applies to the unknown dimension(s), we get a unique slot 0–8.
201
    pub fn slot_index(
202
        width_known: bool,
203
        height_known: bool,
204
        width_type: AvailableWidthType,
205
        height_type: AvailableWidthType,
206
    ) -> usize {
207
        match (width_known, height_known) {
208
            (true, true) => 0,
209
            (true, false) => {
210
                if width_type == AvailableWidthType::MinContent { 2 } else { 1 }
211
            }
212
            (false, true) => {
213
                if height_type == AvailableWidthType::MinContent { 4 } else { 3 }
214
            }
215
            (false, false) => {
216
                let w = if width_type == AvailableWidthType::MinContent { 1 } else { 0 };
217
                let h = if height_type == AvailableWidthType::MinContent { 1 } else { 0 };
218
                5 + w * 2 + h
219
            }
220
        }
221
    }
222

            
223
    /// Look up a sizing cache entry, implementing Taffy's "result matches request"
224
    /// optimization: if the caller provides the result size as a known dimension
225
    /// (common in Pass1→Pass2 transitions), it's still a cache hit.
226
21840
    pub fn get_size(&self, slot: usize, known_dims: LogicalSize) -> Option<&SizingCacheEntry> {
227
21840
        let entry = self.measure_entries[slot].as_ref()?;
228
        // Exact match on input constraints
229
2205
        if (known_dims.width - entry.available_size.width).abs() < CACHE_SIZE_EPSILON
230
2205
            && (known_dims.height - entry.available_size.height).abs() < CACHE_SIZE_EPSILON
231
        {
232
35
            return Some(entry);
233
2170
        }
234
        // "Result matches request" — if the caller provides the result size
235
        // as a known dimension, it's still a hit. This is the key optimization
236
        // that makes two-pass layout O(n): Pass 1 measures a node, Pass 2
237
        // provides the measured size as a constraint → automatic cache hit.
238
2170
        if (known_dims.width - entry.result_size.width).abs() < CACHE_SIZE_EPSILON
239
2100
            && (known_dims.height - entry.result_size.height).abs() < CACHE_SIZE_EPSILON
240
        {
241
            return Some(entry);
242
2170
        }
243
2170
        None
244
21840
    }
245

            
246
    /// Store a sizing result in the given slot.
247
28525
    pub fn store_size(&mut self, slot: usize, entry: SizingCacheEntry) {
248
28525
        self.measure_entries[slot] = Some(entry);
249
28525
        self.is_empty = false;
250
28525
    }
251

            
252
    /// Look up the full layout cache entry.
253
28525
    pub fn get_layout(&self, known_dims: LogicalSize) -> Option<&LayoutCacheEntry> {
254
28525
        let entry = self.layout_entry.as_ref()?;
255
4200
        if (known_dims.width - entry.available_size.width).abs() < CACHE_SIZE_EPSILON
256
2170
            && (known_dims.height - entry.available_size.height).abs() < CACHE_SIZE_EPSILON
257
        {
258
            return Some(entry);
259
4200
        }
260
        // "Result matches request" for layout too
261
4200
        if (known_dims.width - entry.result_size.width).abs() < CACHE_SIZE_EPSILON
262
2100
            && (known_dims.height - entry.result_size.height).abs() < CACHE_SIZE_EPSILON
263
        {
264
            return Some(entry);
265
4200
        }
266
4200
        None
267
28525
    }
268

            
269
    /// Store a full layout result.
270
28525
    pub fn store_layout(&mut self, entry: LayoutCacheEntry) {
271
28525
        self.layout_entry = Some(entry);
272
28525
        self.is_empty = false;
273
28525
    }
274
}
275

            
276
/// External layout cache, parallel to `LayoutTree.nodes`.
277
///
278
/// `cache_map.entries[i]` holds the cache for `LayoutTree.nodes[i]`.
279
/// Stored on `LayoutCache` (persists across frames).
280
///
281
/// This is Azul's improvement over Taffy's on-node cache:
282
/// - `LayoutNode` stays slim (0 bytes overhead)
283
/// - No `&mut tree` needed to read/write cache entries
284
/// - Cache can be resized independently after reconciliation
285
/// - O(1) indexed lookup (Vec) instead of O(log n) (BTreeMap)
286
#[derive(Debug, Clone, Default)]
287
pub struct LayoutCacheMap {
288
    pub entries: Vec<NodeCache>,
289
}
290

            
291
impl LayoutCacheMap {
292
    /// Resize to match tree length after reconciliation.
293
    /// New nodes get empty (dirty) caches. Removed nodes' caches are dropped.
294
4655
    pub fn resize_to_tree(&mut self, tree_len: usize) {
295
4655
        self.entries.resize_with(tree_len, NodeCache::default);
296
4655
    }
297

            
298
    /// O(1) lookup by layout tree index.
299
    #[inline]
300
    pub fn get(&self, node_index: usize) -> &NodeCache {
301
        &self.entries[node_index]
302
    }
303

            
304
    /// O(1) mutable lookup by layout tree index.
305
    #[inline]
306
57050
    pub fn get_mut(&mut self, node_index: usize) -> &mut NodeCache {
307
57050
        &mut self.entries[node_index]
308
57050
    }
309

            
310
    /// Invalidate a node and propagate dirty flags upward through ancestors.
311
    ///
312
    /// Implements Taffy's early-stop optimization: propagation halts at the
313
    /// first ancestor whose cache is already empty (i.e., already dirty).
314
    /// This prevents redundant O(depth) propagation when multiple children
315
    /// of the same parent are dirtied.
316
30065
    pub fn mark_dirty(&mut self, node_index: usize, tree: &[LayoutNodeHot]) {
317
30065
        if node_index >= self.entries.len() {
318
            return;
319
30065
        }
320
30065
        let cache = &mut self.entries[node_index];
321
30065
        if cache.is_empty {
322
30065
            return; // Already dirty → ancestors are too
323
        }
324
        cache.clear();
325

            
326
        // Propagate upward (Taffy's early-stop optimization)
327
        let mut current = tree.get(node_index).and_then(|n| n.parent);
328
        while let Some(parent_idx) = current {
329
            if parent_idx >= self.entries.len() {
330
                break;
331
            }
332
            let parent_cache = &mut self.entries[parent_idx];
333
            if parent_cache.is_empty {
334
                break; // Stop early — ancestor already dirty
335
            }
336
            parent_cache.clear();
337
            current = tree.get(parent_idx).and_then(|n| n.parent);
338
        }
339
30065
    }
340
}
341

            
342
/// The persistent cache that holds the layout state between frames.
343
#[derive(Debug, Clone, Default)]
344
pub struct LayoutCache {
345
    /// The fully laid-out tree from the previous frame. This is our primary cache.
346
    pub tree: Option<LayoutTree>,
347
    /// The final, absolute positions of all nodes from the previous frame.
348
    pub calculated_positions: super::PositionVec,
349
    /// The viewport size from the last layout pass, used to detect resizes.
350
    pub viewport: Option<LogicalRect>,
351
    /// Stable scroll IDs computed from node_data_hash (layout index -> scroll ID)
352
    pub scroll_ids: HashMap<usize, u64>,
353
    /// Mapping from scroll ID to DOM NodeId for hit testing
354
    pub scroll_id_to_node_id: HashMap<u64, NodeId>,
355
    /// CSS counter values for each node and counter name.
356
    /// Key: (layout_index, counter_name), Value: counter value
357
    /// This stores the computed counter values after processing counter-reset and
358
    /// counter-increment.
359
    pub counters: HashMap<(usize, String), i32>,
360
    /// Cache of positioned floats for each BFC node (layout_index -> FloatingContext).
361
    /// This persists float positions across multiple layout passes, ensuring IFC
362
    /// children always have access to correct float exclusions even when layout is
363
    /// recalculated.
364
    pub float_cache: HashMap<usize, fc::FloatingContext>,
365
    /// Per-node multi-slot cache (inspired by Taffy's 9+1 architecture).
366
    /// External to LayoutTree — indexed by node index for O(1) lookup.
367
    /// Persists across frames; resized after reconciliation.
368
    pub cache_map: LayoutCacheMap,
369
    /// Snapshot of calculated_positions from the previous frame, used by the
370
    /// compositor to compute damage rects (old bounds vs new bounds).
371
    pub previous_positions: super::PositionVec,
372
    /// Cached display list keyed by `(root_subtree_hash, viewport)`.
373
    /// When the reconciled tree has the same root subtree_hash AND
374
    /// the same viewport as the cached one, the display list is
375
    /// returned as-is — skipping layout, positioning, and
376
    /// display-list generation entirely. Cleared whenever
377
    /// `mark_dirty` fires on any node (since the root's upstream
378
    /// invalidation chain clears its ancestors).
379
    pub cached_display_list: Option<(SubtreeHash, LogicalRect, super::display_list::DisplayList)>,
380
    /// Raw pointer of the StyledDom from the previous layout pass. When the
381
    /// same `&StyledDom` reference is passed again AND the viewport is unchanged,
382
    /// skip reconcile entirely and return the cached display list (saves ~0.8 ms).
383
    pub prev_dom_ptr: usize,
384
    pub prev_viewport: LogicalRect,
385
}
386

            
387
/// Approximate heap-byte breakdown of the solver3 LayoutCache.
388
#[derive(Debug, Clone, Default)]
389
pub struct Solver3CacheMemoryReport {
390
    pub tree_bytes: usize,
391
    pub tree_report: Option<super::layout_tree::LayoutTreeMemoryReport>,
392
    pub calculated_positions_bytes: usize,
393
    pub previous_positions_bytes: usize,
394
    pub scroll_ids_bytes: usize,
395
    pub scroll_id_to_node_id_bytes: usize,
396
    pub counters_bytes: usize,
397
    pub float_cache_bytes: usize,
398
    pub cache_map_bytes: usize,
399
    pub cached_display_list_bytes: usize,
400
}
401

            
402
impl Solver3CacheMemoryReport {
403
    pub fn total_bytes(&self) -> usize {
404
        self.tree_bytes
405
            + self.calculated_positions_bytes
406
            + self.previous_positions_bytes
407
            + self.scroll_ids_bytes
408
            + self.scroll_id_to_node_id_bytes
409
            + self.counters_bytes
410
            + self.float_cache_bytes
411
            + self.cache_map_bytes
412
            + self.cached_display_list_bytes
413
    }
414
}
415

            
416
impl LayoutCache {
417
    /// Approximate heap bytes retained by this LayoutCache.
418
    pub fn memory_report(&self) -> Solver3CacheMemoryReport {
419
        let tree_report = self.tree.as_ref().map(|t| t.memory_report());
420
        let tree_bytes = tree_report.as_ref().map(|r| r.total_bytes()).unwrap_or(0);
421
        // cache_map: Vec<NodeCache>; NodeCache has 9 Option<SizingCacheEntry>
422
        // + 1 Option<LayoutCacheEntry>. Count filled layout entries' child_positions.
423
        let mut cache_map_bytes = self.cache_map.entries.capacity()
424
            * core::mem::size_of::<NodeCache>();
425
        for e in &self.cache_map.entries {
426
            if let Some(le) = &e.layout_entry {
427
                cache_map_bytes += le.child_positions.capacity()
428
                    * core::mem::size_of::<(usize, LogicalPosition)>();
429
            }
430
        }
431
        Solver3CacheMemoryReport {
432
            tree_bytes,
433
            tree_report,
434
            calculated_positions_bytes: self.calculated_positions.len()
435
                * core::mem::size_of::<LogicalPosition>(),
436
            previous_positions_bytes: self.previous_positions.len()
437
                * core::mem::size_of::<LogicalPosition>(),
438
            scroll_ids_bytes: self.scroll_ids.len()
439
                * (core::mem::size_of::<usize>() + core::mem::size_of::<u64>()),
440
            scroll_id_to_node_id_bytes: self.scroll_id_to_node_id.len()
441
                * (core::mem::size_of::<u64>() + core::mem::size_of::<NodeId>()),
442
            counters_bytes: self.counters.iter().map(|((_, name), _)| {
443
                core::mem::size_of::<(usize, String)>()
444
                    + core::mem::size_of::<i32>()
445
                    + name.capacity()
446
            }).sum(),
447
            float_cache_bytes: self.float_cache.len() * 256, // conservative per-FC
448
            cache_map_bytes,
449
            cached_display_list_bytes: if self.cached_display_list.is_some() { 2048 } else { 0 },
450
        }
451
    }
452
}
453

            
454
/// The result of a reconciliation pass.
455
#[derive(Debug, Default)]
456
pub struct ReconciliationResult {
457
    /// Set of nodes whose intrinsic size needs to be recalculated (bottom-up pass).
458
    pub intrinsic_dirty: BTreeSet<usize>,
459
    /// Set of layout roots whose subtrees need a new top-down layout pass.
460
    pub layout_roots: BTreeSet<usize>,
461
    /// Set of nodes that only need a paint/display-list update (no relayout).
462
    pub paint_dirty: BTreeSet<usize>,
463
}
464

            
465
impl ReconciliationResult {
466
    /// Checks if any layout or paint work is needed.
467
4655
    pub fn is_clean(&self) -> bool {
468
4655
        self.intrinsic_dirty.is_empty()
469
            && self.layout_roots.is_empty()
470
            && self.paint_dirty.is_empty()
471
4655
    }
472

            
473
    /// Returns true if full layout work is needed for at least one node.
474
    pub fn needs_layout(&self) -> bool {
475
        !self.intrinsic_dirty.is_empty() || !self.layout_roots.is_empty()
476
    }
477

            
478
    /// Returns true if only paint work is needed (no layout).
479
    pub fn needs_paint_only(&self) -> bool {
480
        !self.needs_layout() && !self.paint_dirty.is_empty()
481
    }
482
}
483

            
484
/// After dirty subtrees are laid out, this repositions their clean siblings
485
/// without recalculating their internal layout. This is a critical optimization.
486
///
487
/// This function acts as a dispatcher, inspecting the parent's formatting context
488
/// and calling the appropriate repositioning algorithm. For complex layout modes
489
/// like Flexbox or Grid, this optimization is skipped, as a full relayout is
490
/// often required to correctly recalculate spacing and sizing for all siblings.
491
4655
pub fn reposition_clean_subtrees(
492
4655
    styled_dom: &StyledDom,
493
4655
    tree: &LayoutTree,
494
4655
    layout_roots: &BTreeSet<usize>,
495
4655
    calculated_positions: &mut super::PositionVec,
496
4655
) {
497
    // Find the unique parents of all dirty layout roots. These are the containers
498
    // where sibling positions need to be adjusted.
499
4655
    let mut parents_to_reposition = BTreeSet::new();
500
9310
    for &root_idx in layout_roots {
501
4655
        if let Some(parent_idx) = tree.get(root_idx).and_then(|n| n.parent) {
502
            parents_to_reposition.insert(parent_idx);
503
4655
        }
504
    }
505

            
506
4655
    for parent_idx in parents_to_reposition {
507
        let parent_node = match tree.get(parent_idx) {
508
            Some(n) => n,
509
            None => continue,
510
        };
511

            
512
        // Dispatch to the correct repositioning logic based on the parent's layout mode.
513
        match parent_node.formatting_context {
514
            // Cases that use simple block-flow stacking can be optimized.
515
            FormattingContext::Block { .. } | FormattingContext::TableRowGroup => {
516
                reposition_block_flow_siblings(
517
                    styled_dom,
518
                    parent_idx,
519
                    tree,
520
                    layout_roots,
521
                    calculated_positions,
522
                );
523
            }
524

            
525
            FormattingContext::Flex | FormattingContext::Grid => {
526
                // Taffy handles this, so if a child is dirty, the parent would have
527
                // already been marked as a layout_root and re-laid out by Taffy.
528
                // We do nothing here for Flex or Grid.
529
            }
530

            
531
            FormattingContext::Table | FormattingContext::TableRow => {
532
                // TODO: Table layout is interdependent. A change in one cell's size
533
                // can affect the entire column's width or row's height, requiring a
534
                // full relayout of the table. This optimization is skipped.
535
            }
536

            
537
            // Other contexts either don't contain children in a way that this
538
            // optimization applies (e.g., Inline, TableCell) or are handled by other
539
            // layout mechanisms (e.g., OutOfFlow).
540
            _ => { /* Do nothing */ }
541
        }
542
    }
543
4655
}
544

            
545
/// Convert LayoutOverflow to OverflowBehavior
546
/// CSS Overflow Module Level 3: initial value of `overflow` is `visible`.
547
// +spec:overflow:3a6297 - initial value 'visible', maps hidden/scroll/auto overflow behaviors
548
62475
pub fn to_overflow_behavior(overflow: MultiValue<LayoutOverflow>) -> fc::OverflowBehavior {
549
62475
    match overflow.unwrap_or(LayoutOverflow::Visible) {
550
62370
        LayoutOverflow::Visible => fc::OverflowBehavior::Visible,
551
105
        LayoutOverflow::Hidden | LayoutOverflow::Clip => fc::OverflowBehavior::Hidden,
552
        LayoutOverflow::Scroll => fc::OverflowBehavior::Scroll,
553
        LayoutOverflow::Auto => fc::OverflowBehavior::Auto,
554
    }
555
62475
}
556

            
557
/// Convert StyleTextAlign to fc::TextAlign
558
// +spec:text-alignment-spacing:43ea0a - text-align-all shorthand: aligns all lines except last (overridden by text-align-last)
559
28525
pub const fn style_text_align_to_fc(text_align: StyleTextAlign) -> fc::TextAlign {
560
28525
    match text_align {
561
28525
        StyleTextAlign::Start | StyleTextAlign::Left => fc::TextAlign::Start,
562
        StyleTextAlign::End | StyleTextAlign::Right => fc::TextAlign::End,
563
        StyleTextAlign::Center => fc::TextAlign::Center,
564
        StyleTextAlign::Justify => fc::TextAlign::Justify,
565
    }
566
28525
}
567

            
568
/// Collects DOM child IDs from the node hierarchy into a Vec.
569
///
570
/// This is a helper function that flattens the sibling iteration into a simple loop.
571
/// Children with `display: none` are filtered out since they generate no boxes.
572
15890
pub fn collect_children_dom_ids(styled_dom: &StyledDom, parent_dom_id: NodeId) -> Vec<NodeId> {
573
15890
    let hierarchy_container = styled_dom.node_hierarchy.as_container();
574
15890
    let mut children = Vec::new();
575

            
576
15890
    let Some(hierarchy_item) = hierarchy_container.get(parent_dom_id) else {
577
        return children;
578
    };
579

            
580
15890
    let Some(mut child_id) = hierarchy_item.first_child_id(parent_dom_id) else {
581
4970
        return children;
582
    };
583

            
584
    // +spec:display-property:9f02c6 - display:none elements generate no boxes
585
    // +spec:display-property:3b507e - display:none excludes subtree from box tree
586
10920
    if get_display_type(styled_dom, child_id) != LayoutDisplay::None {
587
10920
        children.push(child_id);
588
10920
    }
589
13160
    while let Some(hierarchy_item) = hierarchy_container.get(child_id) {
590
13160
        let Some(next) = hierarchy_item.next_sibling_id() else {
591
10920
            break;
592
        };
593
2240
        if get_display_type(styled_dom, next) != LayoutDisplay::None {
594
2205
            children.push(next);
595
2205
        }
596
2240
        child_id = next;
597
    }
598

            
599
10920
    children
600
15890
}
601

            
602
/// Checks if a flex container is simple enough to be treated like a block-stack for
603
/// repositioning.
604
pub fn is_simple_flex_stack(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> bool {
605
    let Some(id) = dom_id else { return false };
606
    let binding = styled_dom.styled_nodes.as_container();
607
    let styled_node = match binding.get(id) {
608
        Some(styled_node) => styled_node,
609
        None => return false,
610
    };
611

            
612
    // Must be a single-line flex container
613
    let wrap = get_wrap(styled_dom, id, &styled_node.styled_node_state);
614

            
615
    if wrap.unwrap_or_default() != LayoutFlexWrap::NoWrap {
616
        return false;
617
    }
618

            
619
    // Must be start-aligned, so there's no space distribution to recalculate.
620
    let justify = get_justify_content(styled_dom, id, &styled_node.styled_node_state);
621

            
622
    if !matches!(
623
        justify.unwrap_or_default(),
624
        LayoutJustifyContent::FlexStart | LayoutJustifyContent::Start
625
    ) {
626
        return false;
627
    }
628

            
629
    // Crucially, no clean siblings can have flexible sizes, otherwise a dirty
630
    // sibling's size change could affect their resolved size.
631
    // NOTE: This check is expensive and incomplete. A more robust solution might
632
    // store flags on the LayoutNode indicating if flex factors are present.
633
    // For now, we assume that if a container *could* have complex flex behavior,
634
    // we play it safe and require a full relayout. This heuristic is a compromise.
635
    // To be truly safe, we'd have to check all children for flex-grow/shrink > 0.
636

            
637
    true
638
}
639

            
640
/// Repositions clean children within a simple block-flow layout (like a BFC or a
641
/// table-row-group). It stacks children along the main axis, preserving their
642
/// previously calculated cross-axis alignment.
643
pub fn reposition_block_flow_siblings(
644
    styled_dom: &StyledDom,
645
    parent_idx: usize,
646
    tree: &LayoutTree,
647
    layout_roots: &BTreeSet<usize>,
648
    calculated_positions: &mut super::PositionVec,
649
) {
650
    let parent_node = match tree.get(parent_idx) {
651
        Some(n) => n,
652
        None => return,
653
    };
654
    let dom_id = parent_node.dom_node_id.unwrap_or(NodeId::ZERO);
655
    let styled_node_state = styled_dom
656
        .styled_nodes
657
        .as_container()
658
        .get(dom_id)
659
        .map(|n| n.styled_node_state.clone())
660
        .unwrap_or_default();
661

            
662
    let writing_mode = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
663

            
664
    let parent_pos = calculated_positions
665
        .get(parent_idx)
666
        .copied()
667
        .unwrap_or_default();
668

            
669
    let parent_bp = parent_node.box_props.unpack();
670
    let content_box_origin = LogicalPosition::new(
671
        parent_pos.x + parent_bp.padding.left,
672
        parent_pos.y + parent_bp.padding.top,
673
    );
674

            
675
    let mut main_pen = 0.0;
676

            
677
    for &child_idx in tree.children(parent_idx) {
678
        let child_node = match tree.get(child_idx) {
679
            Some(n) => n,
680
            None => continue,
681
        };
682

            
683
        let child_size = child_node.used_size.unwrap_or_default();
684
        let child_bp = child_node.box_props.unpack();
685
        let child_main_sum = child_bp.margin.main_sum(writing_mode);
686
        let margin_box_main_size = child_size.main(writing_mode) + child_main_sum;
687

            
688
        if layout_roots.contains(&child_idx) {
689
            // This child was DIRTY and has been correctly repositioned.
690
            // Update the pen to the position immediately after this child.
691
            let new_pos = match calculated_positions.get(child_idx) {
692
                Some(p) => *p,
693
                None => continue,
694
            };
695

            
696
            let main_axis_offset = if writing_mode.is_vertical() {
697
                new_pos.x - content_box_origin.x
698
            } else {
699
                new_pos.y - content_box_origin.y
700
            };
701

            
702
            main_pen = main_axis_offset
703
                + child_size.main(writing_mode)
704
                + child_bp.margin.main_end(writing_mode);
705
        } else {
706
            // This child is *clean*. Calculate its new position and shift its
707
            // entire subtree.
708
            let old_pos = match calculated_positions.get(child_idx) {
709
                Some(p) => *p,
710
                None => continue,
711
            };
712

            
713
            let child_main_start = child_bp.margin.main_start(writing_mode);
714
            let new_main_pos = main_pen + child_main_start;
715
            let old_relative_pos = tree.warm(child_idx)
716
                .and_then(|w| w.relative_position)
717
                .unwrap_or_default();
718
            let cross_pos = if writing_mode.is_vertical() {
719
                old_relative_pos.y
720
            } else {
721
                old_relative_pos.x
722
            };
723
            let new_relative_pos =
724
                LogicalPosition::from_main_cross(new_main_pos, cross_pos, writing_mode);
725

            
726
            let new_absolute_pos = LogicalPosition::new(
727
                content_box_origin.x + new_relative_pos.x,
728
                content_box_origin.y + new_relative_pos.y,
729
            );
730

            
731
            if old_pos != new_absolute_pos {
732
                let delta = LogicalPosition::new(
733
                    new_absolute_pos.x - old_pos.x,
734
                    new_absolute_pos.y - old_pos.y,
735
                );
736
                shift_subtree_position(child_idx, delta, tree, calculated_positions);
737
            }
738

            
739
            main_pen += margin_box_main_size;
740
        }
741
    }
742
}
743

            
744
/// Helper to recursively shift the absolute position of a node and all its descendants.
745
pub fn shift_subtree_position(
746
    node_idx: usize,
747
    delta: LogicalPosition,
748
    tree: &LayoutTree,
749
    calculated_positions: &mut super::PositionVec,
750
) {
751
    if let Some(pos) = calculated_positions.get_mut(node_idx) {
752
        pos.x += delta.x;
753
        pos.y += delta.y;
754
    }
755

            
756
    if let Some(node) = tree.get(node_idx) {
757
        let children = tree.children(node_idx).to_vec();
758
        for &child_idx in &children {
759
            shift_subtree_position(child_idx, delta, tree, calculated_positions);
760
        }
761
    }
762
}
763

            
764
/// Compares the new DOM against the cached tree, creating a new tree
765
/// and identifying which parts need to be re-laid out.
766
/// Count how many of the supplied DOM children would actually end up
767
/// in the layout tree. Mirrors the filters applied by
768
/// `LayoutTreeBuilder::build_recursive` so reconciliation can compare
769
/// like-for-like:
770
///
771
/// - `display: none` nodes are skipped entirely.
772
/// - In table structural contexts (table, row-group, row) whitespace
773
///   text nodes are skipped (CSS 2.2 §17.2.1, matches
774
///   `should_skip_for_table_structure`).
775
/// - Whitespace-only inline runs that sit between block siblings
776
///   collapse to zero boxes (CSS 2.2 §9.2.2.1).
777
///
778
/// The first two rules drop children unconditionally; the third only
779
/// fires on siblings surrounding a block-level child, so we detect it
780
/// by walking the run pairs. We do not build the runs — just count
781
/// survivors.
782
15890
fn layout_relevant_child_count(
783
15890
    styled_dom: &azul_core::styled_dom::StyledDom,
784
15890
    children: &[NodeId],
785
15890
    parent_id: NodeId,
786
15890
) -> usize {
787
    use super::getters::{get_display_property, MultiValue};
788
    use super::layout_tree::{is_block_level, is_whitespace_only_text};
789

            
790
15890
    let parent_display = match get_display_property(styled_dom, Some(parent_id)) {
791
15890
        MultiValue::Exact(d) => d,
792
        _ => azul_css::props::layout::display::LayoutDisplay::Block,
793
    };
794
15890
    let is_table_structural = matches!(
795
15890
        parent_display,
796
        azul_css::props::layout::display::LayoutDisplay::Table
797
            | azul_css::props::layout::display::LayoutDisplay::InlineTable
798
            | azul_css::props::layout::display::LayoutDisplay::TableRowGroup
799
            | azul_css::props::layout::display::LayoutDisplay::TableHeaderGroup
800
            | azul_css::props::layout::display::LayoutDisplay::TableFooterGroup
801
            | azul_css::props::layout::display::LayoutDisplay::TableRow
802
    );
803

            
804
15890
    let has_any_block_child = children
805
15890
        .iter()
806
15890
        .any(|&id| is_block_level(styled_dom, id));
807

            
808
15890
    let mut count = 0usize;
809
    // When parent has any block child, whitespace-only inline runs
810
    // surrounding blocks collapse. We approximate that by skipping
811
    // whitespace text whenever any block sibling exists.
812
15890
    let collapse_inline_whitespace = has_any_block_child;
813
29015
    for &id in children {
814
        // display:none drops
815
13125
        let display = match get_display_property(styled_dom, Some(id)) {
816
13125
            MultiValue::Exact(d) => d,
817
            _ => azul_css::props::layout::display::LayoutDisplay::Block,
818
        };
819
13125
        if matches!(display, azul_css::props::layout::display::LayoutDisplay::None) {
820
            continue;
821
13125
        }
822
        // Table-structural whitespace drops.
823
13125
        if is_table_structural && is_whitespace_only_text(styled_dom, id) {
824
            continue;
825
13125
        }
826
        // Whitespace-only inline run collapse when mixed with blocks.
827
13125
        if collapse_inline_whitespace
828
5950
            && !is_block_level(styled_dom, id)
829
35
            && is_whitespace_only_text(styled_dom, id)
830
        {
831
            continue;
832
13125
        }
833
13125
        count += 1;
834
    }
835
15890
    count
836
15890
}
837

            
838
2765
pub fn reconcile_and_invalidate<T: ParsedFontTrait>(
839
2765
    ctx: &mut LayoutContext<'_, T>,
840
2765
    cache: &LayoutCache,
841
2765
    viewport: LogicalRect,
842
2765
) -> Result<(LayoutTree, ReconciliationResult)> {
843
2765
    let _probe_outer = crate::probe::Probe::span("reconcile_and_invalidate");
844
2765
    let mut new_tree_builder = LayoutTreeBuilder::new(ctx.viewport_size);
845
2765
    let mut recon_result = ReconciliationResult::default();
846
2765
    let old_tree = cache.tree.as_ref();
847

            
848
    // Check for viewport resize, which dirties the root for a top-down pass.
849
2765
    if cache.viewport.map_or(true, |v| v.size != viewport.size) {
850
2730
        recon_result.layout_roots.insert(0); // Root is always index 0
851
2730
    }
852

            
853
2765
    let root_dom_id = ctx
854
2765
        .styled_dom
855
2765
        .root
856
2765
        .into_crate_internal()
857
2765
        .unwrap_or(NodeId::ZERO);
858
2765
    let root_idx = reconcile_recursive(
859
2765
        ctx.styled_dom,
860
2765
        root_dom_id,
861
2765
        old_tree.map(|t| t.root),
862
2765
        None,
863
2765
        old_tree,
864
2765
        &mut new_tree_builder,
865
2765
        &mut recon_result,
866
2765
        &mut ctx.debug_messages,
867
    )?;
868

            
869
    // Clean up layout roots: if a parent is a layout root, its children don't need to be.
870
2765
    let final_layout_roots = recon_result
871
2765
        .layout_roots
872
2765
        .iter()
873
15820
        .filter(|&&idx| {
874
15820
            let mut current = new_tree_builder.get(idx).and_then(|n| n.parent);
875
15855
            while let Some(p_idx) = current {
876
13125
                if recon_result.layout_roots.contains(&p_idx) {
877
13090
                    return false;
878
35
                }
879
35
                current = new_tree_builder.get(p_idx).and_then(|n| n.parent);
880
            }
881
2730
            true
882
15820
        })
883
2765
        .copied()
884
2765
        .collect();
885
2765
    recon_result.layout_roots = final_layout_roots;
886

            
887
2765
    let new_tree = new_tree_builder.build(root_idx);
888
    // layout_document's step marker is stuck at 1 (post-`?` not reached), the
889
    // lifted `?` mis-discriminated this Ok as Err (niche-Result mis-lift).
890
2765
    { let _ = (0xCC00_0001u32); }
891
2765
    Ok((new_tree, recon_result))
892
2765
}
893

            
894
/// CSS 2.2 § 9.2.2.1: Checks whether an inline run consists entirely of
895
/// whitespace-only text nodes, in which case it should NOT generate an
896
/// anonymous IFC wrapper in a BFC mixed-content context.
897
///
898
/// This prevents whitespace between block elements from creating empty
899
/// anonymous blocks that take up vertical space (regression c33e94b0).
900
///
901
/// Exception: if the parent (or any ancestor) has `white-space: pre`,
902
/// `pre-wrap`, or `pre-line`, whitespace IS significant and the wrapper
903
/// must still be created.
904
35
fn is_whitespace_only_inline_run(
905
35
    styled_dom: &StyledDom,
906
35
    inline_run: &[(usize, NodeId)],
907
35
    parent_dom_id: NodeId,
908
35
) -> bool {
909
    use azul_css::props::style::text::StyleWhiteSpace;
910

            
911
35
    if inline_run.is_empty() {
912
        return true;
913
35
    }
914

            
915
    // Check if the parent preserves whitespace
916
35
    let parent_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
917
35
    let white_space = match get_white_space_property(styled_dom, parent_dom_id, parent_state) {
918
35
        MultiValue::Exact(ws) => Some(ws),
919
        _ => None,
920
    };
921

            
922
    // If white-space preserves whitespace, don't strip
923
35
    if matches!(
924
35
        white_space,
925
        Some(StyleWhiteSpace::Pre) | Some(StyleWhiteSpace::PreWrap) | Some(StyleWhiteSpace::PreLine)
926
    ) {
927
        return false;
928
35
    }
929

            
930
    // Check that every node in the run is a whitespace-only text node
931
35
    let binding = styled_dom.node_data.as_container();
932
35
    for &(_, dom_id) in inline_run {
933
35
        if let Some(data) = binding.get(dom_id) {
934
35
            match data.get_node_type() {
935
35
                NodeType::Text(text) => {
936
35
                    let s = text.as_str();
937
35
                    if !s.chars().all(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')) {
938
35
                        return false; // Non-whitespace text → must create wrapper
939
                    }
940
                }
941
                _ => {
942
                    return false; // Non-text inline element → must create wrapper
943
                }
944
            }
945
        }
946
    }
947

            
948
    true // All nodes are whitespace-only text
949
35
}
950

            
951
/// Recursively traverses the new DOM and old tree, building a new tree and marking dirty nodes.
952
15890
pub fn reconcile_recursive(
953
15890
    styled_dom: &StyledDom,
954
15890
    new_dom_id: NodeId,
955
15890
    old_tree_idx: Option<usize>,
956
15890
    new_parent_idx: Option<usize>,
957
15890
    old_tree: Option<&LayoutTree>,
958
15890
    new_tree_builder: &mut LayoutTreeBuilder,
959
15890
    recon: &mut ReconciliationResult,
960
15890
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
961
15890
) -> Result<usize> {
962
15890
    let node_data = &styled_dom.node_data.as_container()[new_dom_id];
963

            
964
15890
    let old_cold = old_tree.and_then(|t| old_tree_idx.and_then(|idx| t.cold(idx)));
965
15890
    match (old_tree.is_some(), old_tree_idx.is_some(), old_cold.is_some()) {
966
15820
        (false, _, _) => drop(crate::probe::Probe::span("recon_old_tree_none")),
967
        (true, false, _) => drop(crate::probe::Probe::span("recon_old_idx_none")),
968
        (true, true, false) => drop(crate::probe::Probe::span("recon_cold_none")),
969
70
        (true, true, true) => drop(crate::probe::Probe::span("recon_cold_some")),
970
    }
971

            
972
    // Compute the new multi-field fingerprint instead of a single hash.
973
15890
    let new_fingerprint = {
974
15890
        let _p = crate::probe::Probe::span("fingerprint_compute");
975
15890
        NodeDataFingerprint::compute(
976
15890
            node_data,
977
15890
            styled_dom.styled_nodes.as_container().get(new_dom_id).map(|n| &n.styled_node_state),
978
        )
979
    };
980

            
981
    // Compare fingerprints to determine what changed (Layout, Paint, or Nothing).
982
15890
    let dirty_flag = match old_cold {
983
        None => {
984
15820
            drop(crate::probe::Probe::span("fp_new_node"));
985
15820
            DirtyFlag::Layout // new node → full layout
986
        },
987
70
        Some(old_c) => {
988
70
            let change_set = old_c.node_data_fingerprint.diff(&new_fingerprint);
989
70
            if change_set.needs_layout() {
990
                drop(crate::probe::Probe::span("fp_needs_layout"));
991
                // Cache the env check in a `OnceLock<bool>`: this branch
992
                // fires once per dirty node (hundreds on cold layout),
993
                // and a direct `env::var` is a mutex + hashmap lookup
994
                // on macOS (~100 ns/call) even when the env var is unset.
995
                static FP_DUMP_ENABLED: std::sync::OnceLock<bool> =
996
                    std::sync::OnceLock::new();
997
                let enabled = *FP_DUMP_ENABLED.get_or_init(|| {
998
                    std::env::var_os("AZ_FP_DUMP").is_some()
999
                });
                if enabled {
                    use std::sync::atomic::{AtomicUsize, Ordering};
                    static DUMPED: AtomicUsize = AtomicUsize::new(0);
                    let n = DUMPED.fetch_add(1, Ordering::Relaxed);
                    if n < 10 {
                        eprintln!(
                            "[fp_diff {n}] dom={} old={:?} new={:?}",
                            new_dom_id.index(),
                            old_c.node_data_fingerprint,
                            new_fingerprint,
                        );
                    }
                }
                DirtyFlag::Layout
70
            } else if change_set.needs_paint() {
                drop(crate::probe::Probe::span("fp_needs_paint"));
                DirtyFlag::Paint
            } else {
70
                drop(crate::probe::Probe::span("fp_clean"));
70
                DirtyFlag::None
            }
        }
    };
15890
    let is_dirty = dirty_flag >= DirtyFlag::Paint;
    // M12.7: `|| old_tree.is_none()` — on COLD layout there is no old tree to
    // clone, so we MUST create a fresh node; taking the else-branch would hit
    // `ok_or(InvalidTree)` on a None old_tree. This is both semantically correct
    // AND robust against a mis-lifted `dirty_flag`/Option match (the suspected
    // niche-enum mis-discriminant) wrongly steering cold nodes into the else.
15890
    let new_node_idx = if dirty_flag >= DirtyFlag::Layout || old_tree.is_none() {
15820
        { let _ = (0xBB00_0001u32); }
15820
        new_tree_builder.create_node_from_dom(
15820
            styled_dom,
15820
            new_dom_id,
15820
            new_parent_idx,
15820
            debug_messages,
        )
    } else {
70
        { let _ = (0xBB00_0002u32); }
        // Paint-only or clean: clone the old node (preserving layout cache)
70
        let old_full_node = old_tree
70
            .and_then(|t| old_tree_idx.and_then(|idx| t.get_full_node(idx)))
70
            .ok_or(LayoutError::InvalidTree)?;
70
        let mut idx = new_tree_builder.clone_node_from_old(&old_full_node, new_parent_idx);
        // If paint-only change, update the fingerprint and dirty flag
70
        if dirty_flag == DirtyFlag::Paint {
            if let Some(cloned) = new_tree_builder.get_mut(idx) {
                cloned.node_data_fingerprint = new_fingerprint;
                cloned.dirty_flag = DirtyFlag::Paint;
            }
70
        }
70
        idx
    };
    // reconcile_recursive sees it. 0 = correct (the first node); 64 (matching the
    // build-marker root_idx) = the usize return mis-reads here.
15890
    { let _ = (0xAB00_0000u32 | (new_node_idx as u32 & 0xffff)); }
    // CRITICAL: For list-items, create a ::marker pseudo-element as the first child
    // This must be done after the node is created but before processing children
    // Per CSS Lists Module Level 3, ::marker is generated as the first child of list-items
    {
        use crate::solver3::getters::get_display_property;
15890
        let display = get_display_property(styled_dom, Some(new_dom_id))
15890
            .exact();
15890
        if matches!(display, Some(LayoutDisplay::ListItem)) {
            // Create ::marker pseudo-element for this list-item
            new_tree_builder.create_marker_pseudo_element(styled_dom, new_dom_id, new_node_idx);
15890
        }
    }
    // Reconcile children to check for structural changes and build the new tree structure.
15890
    let mut new_children_dom_ids: Vec<_> = collect_children_dom_ids(styled_dom, new_dom_id);
    // CSS 2.2 §17.2.1: Filter whitespace-only text nodes from table structural elements
    // (table, row-group, row). Without this, the reconciler sees them as "inline" children
    // mixed with block-level <td>/<th>, triggering incorrect anonymous IFC wrapping.
    // The layout tree builder already does this via should_skip_for_table_structure().
    {
        use super::getters::{get_display_property, MultiValue};
15890
        let parent_display = match get_display_property(styled_dom, Some(new_dom_id)) {
15890
            MultiValue::Exact(d) => d,
            _ => azul_css::props::layout::display::LayoutDisplay::Block,
        };
15890
        if matches!(parent_display,
            azul_css::props::layout::display::LayoutDisplay::Table
            | azul_css::props::layout::display::LayoutDisplay::InlineTable
            | azul_css::props::layout::display::LayoutDisplay::TableRowGroup
            | azul_css::props::layout::display::LayoutDisplay::TableHeaderGroup
            | azul_css::props::layout::display::LayoutDisplay::TableFooterGroup
            | azul_css::props::layout::display::LayoutDisplay::TableRow
        ) {
4165
            new_children_dom_ids.retain(|&id| {
4165
                !super::layout_tree::is_whitespace_only_text(styled_dom, id)
4165
            });
13545
        }
    }
    // Compute both positional and DOM-keyed lookups for the old
    // tree's children. The DOM-keyed map is authoritative for
    // reconciliation (positional drifts every time the layout-tree
    // builder drops a DOM child — whitespace text, display:none,
    // table-structural whitespace — or inserts an anonymous
    // wrapper that isn't in the DOM).
15890
    let old_children_indices: Vec<usize> = old_tree
15890
        .and_then(|t| old_tree_idx.map(|idx| t.children(idx).to_vec()))
15890
        .unwrap_or_default();
15890
    let old_children_by_dom: alloc::collections::BTreeMap<NodeId, usize> = old_tree
15890
        .and_then(|t| old_tree_idx.map(|idx| {
70
            t.children(idx).iter()
70
                .filter_map(|&cidx| t.get(cidx).and_then(|n| n.dom_node_id).map(|did| (did, cidx)))
70
                .collect()
70
        }))
15890
        .unwrap_or_default();
    // Count of old layout children that correspond to a real DOM
    // node (exclude anonymous wrappers). This is what we compare
    // against the layout-relevant subset of new DOM children to
    // decide whether the structural shape actually changed.
15890
    let old_layout_relevant_count = old_children_by_dom.len();
    // Filter new DOM children to the subset the layout-tree builder
    // would actually emit. This mirrors `should_skip_for_table_structure`
    // and the `is_whitespace_only_inline_run` logic. Without this
    // filter, `children_are_different` fires on every reconcile
    // because the DOM has whitespace text nodes the layout tree
    // drops.
15890
    let new_layout_relevant_count = layout_relevant_child_count(styled_dom, &new_children_dom_ids, new_dom_id);
15890
    let mut children_are_different = new_layout_relevant_count != old_layout_relevant_count;
15890
    let mut new_child_hashes = Vec::new();
    // +spec:display-property:42f9c0 - anonymous block boxes wrap inline runs when block container has mixed block/inline children
    // CSS 2.2 Section 9.2.1.1: Anonymous Block Boxes
    // When a block container has mixed block/inline children, we must:
    // 1. Wrap consecutive inline children in anonymous block boxes
    // 2. Leave block-level children as direct children
15890
    let has_block_child = new_children_dom_ids
15890
        .iter()
15890
        .any(|&id| is_block_level(styled_dom, id));
15890
    if !has_block_child {
        // All children are inline - no anonymous boxes needed
        // Simple case: process each child directly
12145
        for (i, &new_child_dom_id) in new_children_dom_ids.iter().enumerate() {
            // DOM-ID match rather than positional — tree builder
            // may have dropped some DOM children (whitespace text
            // nodes) so positional drift mis-aligns the cache.
            // DOM-id match only: positional fallback would align
            // anonymous wrappers against real DOM nodes and trigger
            // spurious fingerprint mismatches (see fp_diff dump).
7175
            let old_child_idx = old_children_by_dom.get(&new_child_dom_id).copied();
7175
            let reconciled_child_idx = reconcile_recursive(
7175
                styled_dom,
7175
                new_child_dom_id,
7175
                old_child_idx,
7175
                Some(new_node_idx),
7175
                old_tree,
7175
                new_tree_builder,
7175
                recon,
7175
                debug_messages,
            )?;
7175
            if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
7175
                new_child_hashes.push(child_node.subtree_hash.0);
7175
            }
7175
            if old_tree.and_then(|t| t.cold(old_child_idx?).map(|n| n.subtree_hash))
7175
                != new_tree_builder
7175
                    .get(reconciled_child_idx)
7175
                    .map(|n| n.subtree_hash)
7175
            {
7175
                children_are_different = true;
7175
            }
        }
    } else {
        // Mixed content: block and inline children
        // We must create anonymous block boxes around consecutive inline runs
3745
        if let Some(msgs) = debug_messages.as_mut() {
3675
            msgs.push(LayoutDebugMessage::info(format!(
3675
                "[reconcile_recursive] Mixed content in node {}: creating anonymous IFC wrappers",
3675
                new_dom_id.index()
3675
            )));
3675
        }
3745
        let mut inline_run: Vec<(usize, NodeId)> = Vec::new(); // (dom_child_index, dom_id)
5950
        for (i, &new_child_dom_id) in new_children_dom_ids.iter().enumerate() {
5950
            if is_block_level(styled_dom, new_child_dom_id) {
                // End current inline run if any
5915
                if !inline_run.is_empty() {
                    // CSS 2.2 § 9.2.2.1: If the inline run consists entirely of
                    // whitespace-only text nodes (and white-space doesn't preserve it),
                    // skip creating the anonymous IFC wrapper. This prevents inter-block
                    // whitespace from creating empty blocks that take up vertical space.
                    // +spec:display-property:bef3fc - anonymous blocks of only collapsible whitespace removed from rendering tree
35
                    if is_whitespace_only_inline_run(styled_dom, &inline_run, new_dom_id) {
                        if let Some(msgs) = debug_messages.as_mut() {
                            msgs.push(LayoutDebugMessage::info(format!(
                                "[reconcile_recursive] Skipping whitespace-only inline run ({} nodes) between blocks in node {}",
                                inline_run.len(),
                                new_dom_id.index()
                            )));
                        }
                        inline_run.clear();
                    } else {
                    // Create anonymous IFC wrapper for the inline run
                    // This wrapper establishes an Inline Formatting Context
35
                    let anon_idx = new_tree_builder.create_anonymous_node(
35
                        new_node_idx,
35
                        AnonymousBoxType::InlineWrapper,
35
                        FormattingContext::Inline, // IFC for inline content
                    );
35
                    if let Some(msgs) = debug_messages.as_mut() {
35
                        msgs.push(LayoutDebugMessage::info(format!(
35
                            "[reconcile_recursive] Created anonymous IFC wrapper (layout_idx={}) for {} inline children: {:?}",
                            anon_idx,
35
                            inline_run.len(),
35
                            inline_run.iter().map(|(_, id)| id.index()).collect::<Vec<_>>()
                        )));
                    }
                    // Process each inline child under the anonymous wrapper
35
                    for (pos, inline_dom_id) in inline_run.drain(..) {
                        // Inline children live under the anon wrapper
                        // in the old tree, so the parent's direct
                        // `old_children_by_dom` map won't hit them.
                        // Fall through to the global `dom_to_layout`
                        // map; we don't care which anon wrapper they
                        // were under, only that their cold data
                        // (fingerprint) gets matched correctly.
35
                        let old_child_idx = old_children_by_dom.get(&inline_dom_id).copied()
35
                            .or_else(|| old_tree
35
                                .and_then(|t| t.dom_to_layout.get(&inline_dom_id))
35
                                .and_then(|v| v.first().copied()));
35
                        let reconciled_child_idx = reconcile_recursive(
35
                            styled_dom,
35
                            inline_dom_id,
35
                            old_child_idx,
35
                            Some(anon_idx), // Parent is the anonymous wrapper
35
                            old_tree,
35
                            new_tree_builder,
35
                            recon,
35
                            debug_messages,
                        )?;
35
                        if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
35
                            new_child_hashes.push(child_node.subtree_hash.0);
35
                        }
                    }
                    // NOTE: We intentionally do NOT unconditionally
                    // mark the anonymous wrapper as intrinsic_dirty
                    // here. If any of the inline children are
                    // themselves dirty, their own `mark_dirty` call
                    // propagates upward through this wrapper, so
                    // wrappers whose content is unchanged keep their
                    // cached layout. Setting `children_are_different`
                    // when the wrapper is newly created (no matching
                    // old anon) flips the parent to layout-dirty,
                    // which is what triggers a fresh wrapper layout.
35
                    children_are_different = true;
                    } // end else (non-whitespace run)
5880
                }
                // Process block-level child directly under parent
5915
                let old_child_idx = old_children_by_dom.get(&new_child_dom_id).copied()
5915
                    .or_else(|| old_children_indices.get(i).copied());
5915
                let reconciled_child_idx = reconcile_recursive(
5915
                    styled_dom,
5915
                    new_child_dom_id,
5915
                    old_child_idx,
5915
                    Some(new_node_idx),
5915
                    old_tree,
5915
                    new_tree_builder,
5915
                    recon,
5915
                    debug_messages,
                )?;
5915
                if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
5915
                    new_child_hashes.push(child_node.subtree_hash.0);
5915
                }
5915
                if old_tree.and_then(|t| t.cold(old_child_idx?).map(|n| n.subtree_hash))
5915
                    != new_tree_builder
5915
                        .get(reconciled_child_idx)
5915
                        .map(|n| n.subtree_hash)
5880
                {
5880
                    children_are_different = true;
5880
                }
35
            } else {
35
                // Inline-level child - add to current run
35
                inline_run.push((i, new_child_dom_id));
35
            }
        }
        // Process any remaining inline run at the end
3745
        if !inline_run.is_empty() {
            // CSS 2.2 § 9.2.2.1: Skip whitespace-only trailing inline runs
            if is_whitespace_only_inline_run(styled_dom, &inline_run, new_dom_id) {
                if let Some(msgs) = debug_messages.as_mut() {
                    msgs.push(LayoutDebugMessage::info(format!(
                        "[reconcile_recursive] Skipping trailing whitespace-only inline run ({} nodes) in node {}",
                        inline_run.len(),
                        new_dom_id.index()
                    )));
                }
                // Don't create a wrapper — just drop the run
            } else {
            let anon_idx = new_tree_builder.create_anonymous_node(
                new_node_idx,
                AnonymousBoxType::InlineWrapper,
                FormattingContext::Inline, // IFC for inline content
            );
            if let Some(msgs) = debug_messages.as_mut() {
                msgs.push(LayoutDebugMessage::info(format!(
                    "[reconcile_recursive] Created trailing anonymous IFC wrapper (layout_idx={}) for {} inline children: {:?}",
                    anon_idx,
                    inline_run.len(),
                    inline_run.iter().map(|(_, id)| id.index()).collect::<Vec<_>>()
                )));
            }
            for (pos, inline_dom_id) in inline_run.drain(..) {
                let old_child_idx = old_children_by_dom.get(&inline_dom_id).copied();
                let reconciled_child_idx = reconcile_recursive(
                    styled_dom,
                    inline_dom_id,
                    old_child_idx,
                    Some(anon_idx),
                    old_tree,
                    new_tree_builder,
                    recon,
                    debug_messages,
                )?;
                if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
                    new_child_hashes.push(child_node.subtree_hash.0);
                }
            }
            // See note in main mixed-content branch: rely on
            // children's own mark_dirty to propagate upward rather
            // than invalidating the whole wrapper each reconcile.
            children_are_different = true;
            } // end else (non-whitespace trailing run)
3745
        }
    }
    // After reconciling children, calculate this node's full subtree hash.
    // Use a combined hash of the fingerprint fields for the subtree hash.
15890
    let node_self_hash = {
        use std::hash::{DefaultHasher, Hash, Hasher};
15890
        let mut h = DefaultHasher::new();
15890
        new_fingerprint.hash(&mut h);
15890
        h.finish()
    };
15890
    let final_subtree_hash = calculate_subtree_hash(node_self_hash, &new_child_hashes);
15890
    if let Some(current_node) = new_tree_builder.get_mut(new_node_idx) {
15890
        current_node.subtree_hash = final_subtree_hash;
15890
    }
    // Classify this node into the appropriate dirty set based on what changed.
15890
    if dirty_flag >= DirtyFlag::Layout || children_are_different {
15820
        recon.intrinsic_dirty.insert(new_node_idx);
15820
        recon.layout_roots.insert(new_node_idx);
15820
    } else if dirty_flag == DirtyFlag::Paint {
        recon.paint_dirty.insert(new_node_idx);
70
    }
15890
    Ok(new_node_idx)
15890
}
/// Result of `prepare_layout_context`: contains the layout constraints and
/// intermediate values needed for `calculate_layout_for_subtree`.
struct PreparedLayoutContext<'a> {
    constraints: LayoutConstraints<'a>,
    /// DOM ID for the node. None for anonymous boxes.
    dom_id: Option<NodeId>,
    writing_mode: LayoutWritingMode,
    final_used_size: LogicalSize,
    box_props: crate::solver3::geometry::BoxProps,
}
/// Prepares the layout context for a single node by calculating its used size
/// and building the layout constraints for its children.
///
/// For anonymous boxes (no dom_node_id), we use default values and inherit
/// from the containing block.
20399
fn prepare_layout_context<'a, T: ParsedFontTrait>(
20399
    ctx: &LayoutContext<'a, T>,
20399
    tree: &LayoutTree,
20399
    node_index: usize,
20399
    containing_block_size: LogicalSize,
20399
) -> Result<PreparedLayoutContext<'a>> {
20399
    let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
20399
    let warm = tree.warm(node_index).ok_or(LayoutError::InvalidTree)?;
20399
    let dom_id = node.dom_node_id; // Can be None for anonymous boxes
    // Phase 1: Calculate this node's provisional used size
    // This size is based on the node's CSS properties (width, height, etc.) and
    // its containing block. If height is 'auto', this is a temporary value.
20399
    let intrinsic = warm.intrinsic_sizes.clone().unwrap_or_default();
20399
    let final_used_size = calculate_used_size_for_node(
20399
        ctx.styled_dom,
20399
        dom_id, // Now Option<NodeId>
20399
        &containing_block_size,
20399
        intrinsic,
20399
        &node.box_props.unpack(),
20399
        &ctx.viewport_size,
    )?;
    // Phase 2: Layout children using a formatting context
    // Use pre-computed styles from LayoutNodeWarm instead of repeated lookups
20399
    let writing_mode = warm.computed_style.writing_mode;
20399
    let text_align = warm.computed_style.text_align;
20399
    let display = warm.computed_style.display;
20399
    let overflow_y = warm.computed_style.overflow_y;
    // Check if height is auto (no explicit height set)
20399
    let height_is_auto = warm.computed_style.height.is_none();
20399
    let available_size_for_children = if height_is_auto {
        // Height is auto - use containing block size as available size
17896
        let inner_size = node.box_props.inner_size(final_used_size, writing_mode);
        // For inline elements (display: inline), the available width comes from
        // the containing block, not from the element's own intrinsic size.
        // CSS 2.2 § 10.3.1: Inline, non-replaced elements use containing block width.
17896
        let available_width = match display {
7599
            LayoutDisplay::Inline => containing_block_size.width,
10297
            _ => inner_size.width,
        };
17896
        LogicalSize {
17896
            width: available_width,
17896
            // Use containing block height!
17896
            height: containing_block_size.height,
17896
        }
    } else {
        // Height is explicit - use inner size (after padding/border)
2503
        node.box_props.inner_size(final_used_size, writing_mode)
    };
    // NOTE: Scrollbar reservation is handled inside layout_bfc() where it subtracts
    // scrollbar width from children_containing_block_size. We do NOT subtract here
    // to avoid double-subtraction (layout_bfc already handles both the used_size
    // and available_size code paths).
20399
    let wm_ctx = crate::solver3::geometry::WritingModeContext::new(
20399
        writing_mode,
20399
        warm.computed_style.direction,
20399
        warm.computed_style.text_orientation,
    );
20399
    let constraints = LayoutConstraints {
20399
        available_size: available_size_for_children,
20399
        bfc_state: None,
20399
        writing_mode,
20399
        writing_mode_ctx: wm_ctx,
20399
        text_align: style_text_align_to_fc(text_align),
20399
        containing_block_size,
20399
        available_width_type: Text3AvailableSpace::Definite(available_size_for_children.width),
20399
    };
20399
    Ok(PreparedLayoutContext {
20399
        constraints,
20399
        dom_id,
20399
        writing_mode,
20399
        final_used_size,
20399
        box_props: node.box_props.unpack(),
20399
    })
20399
}
/// Core scrollbar info computation: given pre-computed content and container sizes plus
/// a DOM node for style look-up, determines whether scrollbars are needed.
///
/// This is the single source of truth for scrollbar detection. Both the BFC path
/// (`compute_scrollbar_info`) and the Taffy flex/grid path (`compute_child_layout`
/// in taffy_bridge.rs) call this function, ensuring consistent behaviour.
///
/// For paged media (PDF), scrollbars are never added since they don't exist in print.
21077
pub fn compute_scrollbar_info_core<T: ParsedFontTrait>(
21077
    ctx: &LayoutContext<'_, T>,
21077
    dom_id: NodeId,
21077
    styled_node_state: &azul_core::styled_dom::StyledNodeState,
21077
    content_size: LogicalSize,
21077
    container_size: LogicalSize,
21077
) -> ScrollbarRequirements {
    // +spec:overflow:08b60d - non-interactive media: UA may show scroll indicators but we skip them for print
21077
    if ctx.fragmentation_context.is_some() {
252
        return ScrollbarRequirements::default();
20825
    }
20825
    let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, styled_node_state);
20825
    let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, styled_node_state);
    // Resolve the full scrollbar style **once** and reuse it
    // across the rest of this function + any further calls from
    // the same layout pass via `LayoutContext::scrollbar_style_cache`.
    // Previously we called `get_layout_scrollbar_width_px` (which
    // builds the full scrollbar_style internally, keeps only
    // `reserve_width_px`, then drops it) and then
    // `get_scrollbar_style` again — each build performs 9 cascade
    // walks (track/thumb/button/corner/width/color/visibility/
    // fade-delay/fade-duration). With the memo, subsequent calls
    // on the same (dom_id, state) are a HashMap hit.
20825
    let scrollbar_style = crate::solver3::getters::get_scrollbar_style_cached(
20825
        ctx, dom_id, styled_node_state,
    );
20825
    let scrollbar_width_px = scrollbar_style.reserve_width_px;
20825
    let mut reqs = fc::check_scrollbar_necessity(
20825
        content_size,
20825
        container_size,
20825
        to_overflow_behavior(overflow_x),
20825
        to_overflow_behavior(overflow_y),
20825
        scrollbar_width_px,
    );
20825
    reqs.visual_width_px = scrollbar_style.visual_width_px;
    // +spec:overflow:e90f12 - scrollbar-gutter reserves space independently of scrollbar presence
    // +spec:overflow:3c44cc - scrollbar-gutter: stable reserves gutter even when no scrollbar is shown
    // +spec:overflow:3a6966 - classic scrollbar gutter width == scrollbar width; overlay scrollbars have no gutter
    //
    // scrollbar-gutter only applies to scroll containers (overflow: auto or scroll).
    // "stable" reserves gutter on the inline-end edge even if no scrollbar is needed.
    // "stable both-edges" reserves gutter on both inline edges.
20825
    let scrollbar_gutter = get_scrollbar_gutter_property(ctx.styled_dom, dom_id, styled_node_state)
20825
        .unwrap_or(azul_css::props::layout::overflow::StyleScrollbarGutter::Auto);
20825
    let ob_y = to_overflow_behavior(overflow_y);
20825
    let is_scroll_container = matches!(ob_y, fc::OverflowBehavior::Scroll | fc::OverflowBehavior::Auto);
20825
    if is_scroll_container {
        use azul_css::props::layout::overflow::StyleScrollbarGutter;
        match scrollbar_gutter {
            StyleScrollbarGutter::Stable => {
                // Reserve gutter on inline-end even if no scrollbar is currently needed
                if !reqs.needs_vertical {
                    reqs.scrollbar_width = scrollbar_width_px;
                }
            }
            StyleScrollbarGutter::StableBothEdges => {
                // Reserve gutter on both inline edges
                reqs.scrollbar_width = scrollbar_width_px * 2.0;
            }
            StyleScrollbarGutter::Auto => {
                // Default: gutter only present when scrollbar is present (already handled)
            }
        }
20825
    }
20825
    reqs
21077
}
/// Determines scrollbar requirements for a node based on content overflow.
///
/// Convenience wrapper around `compute_scrollbar_info_core` for the BFC layout path,
/// where the container size is derived from `box_props.inner_size(final_used_size, …)`.
20360
fn compute_scrollbar_info<T: ParsedFontTrait>(
20360
    ctx: &LayoutContext<'_, T>,
20360
    dom_id: NodeId,
20360
    styled_node_state: &azul_core::styled_dom::StyledNodeState,
20360
    content_size: LogicalSize,
20360
    box_props: &crate::solver3::geometry::BoxProps,
20360
    final_used_size: LogicalSize,
20360
    writing_mode: LayoutWritingMode,
20360
) -> ScrollbarRequirements {
20360
    let container_size = box_props.inner_size(final_used_size, writing_mode);
20360
    compute_scrollbar_info_core(ctx, dom_id, styled_node_state, content_size, container_size)
20360
}
/// Checks if scrollbars changed compared to previous layout and if reflow is needed.
///
/// Detects both addition AND removal of scrollbars. Oscillation (add → remove → add)
/// is prevented by the outer layout loop's iteration limit (`loop_count > 10` in mod.rs),
/// not by suppressing removal detection here. This allows scrollbars to correctly
/// disappear when content shrinks or the window is resized larger.
28525
fn check_scrollbar_change(
28525
    tree: &LayoutTree,
28525
    node_index: usize,
28525
    scrollbar_info: &ScrollbarRequirements,
28525
    skip_scrollbar_check: bool,
28525
) -> bool {
28525
    if skip_scrollbar_check {
8365
        return false;
20160
    }
20160
    let Some(warm_node) = tree.warm(node_index) else {
        return false;
    };
20160
    match &warm_node.scrollbar_info {
10080
        None => scrollbar_info.needs_reflow(),
10080
        Some(old_info) => {
            // Trigger reflow if scrollbar state changed in either direction
10080
            let horizontal_changed = old_info.needs_horizontal != scrollbar_info.needs_horizontal;
10080
            let vertical_changed = old_info.needs_vertical != scrollbar_info.needs_vertical;
10080
            horizontal_changed || vertical_changed
        }
    }
28525
}
/// Returns the new scrollbar info directly, replacing any previous state.
///
/// Previous versions used `||` to make scrollbars "sticky" (never removed once added).
/// This prevented oscillation but caused scrollbars to persist forever—even after
/// content shrinks or the window grows. The outer layout loop's iteration cap
/// now handles oscillation safety, so we can faithfully reflect the current state.
28525
fn merge_scrollbar_info(
28525
    _tree: &LayoutTree,
28525
    _node_index: usize,
28525
    new_info: &ScrollbarRequirements,
28525
) -> ScrollbarRequirements {
28525
    new_info.clone()
28525
}
/// Calculates the content-box position from a margin-box position.
///
/// The content-box is offset from the margin-box by border + padding.
/// Margin is NOT added here because containing_block_pos already accounts for it.
47460
fn calculate_content_box_pos(
47460
    containing_block_pos: LogicalPosition,
47460
    box_props: &crate::solver3::geometry::BoxProps,
47460
) -> LogicalPosition {
47460
    LogicalPosition::new(
47460
        containing_block_pos.x + box_props.border.left + box_props.padding.left,
47460
        containing_block_pos.y + box_props.border.top + box_props.padding.top,
    )
47460
}
/// Emits debug logging for content-box calculation if debug messages are enabled.
20399
fn log_content_box_calculation<T: ParsedFontTrait>(
20399
    ctx: &mut LayoutContext<'_, T>,
20399
    node_index: usize,
20399
    current_node: &LayoutNodeHot,
20399
    containing_block_pos: LogicalPosition,
20399
    self_content_box_pos: LogicalPosition,
20399
) {
20399
    let Some(debug_msgs) = ctx.debug_messages.as_mut() else {
490
        return;
    };
19909
    let dom_name = current_node
19909
        .dom_node_id
19909
        .and_then(|id| {
19870
            ctx.styled_dom
19870
                .node_data
19870
                .as_container()
19870
                .internal
19870
                .get(id.index())
19870
        })
19909
        .map(|n| format!("{:?}", n.node_type))
19909
        .unwrap_or_else(|| "Unknown".to_string());
19909
    let cbp = current_node.box_props.unpack();
19909
    debug_msgs.push(LayoutDebugMessage::new(
19909
        LayoutDebugMessageType::PositionCalculation,
19909
        format!(
19909
            "[CONTENT BOX {}] {} - margin-box pos=({:.2}, {:.2}) + border=({:.2},{:.2}) + \
19909
             padding=({:.2},{:.2}) = content-box pos=({:.2}, {:.2})",
            node_index,
            dom_name,
            containing_block_pos.x,
            containing_block_pos.y,
            cbp.border.left,
            cbp.border.top,
            cbp.padding.left,
            cbp.padding.top,
            self_content_box_pos.x,
            self_content_box_pos.y
        ),
    ));
20399
}
/// Emits debug logging for child positioning if debug messages are enabled.
12747
fn log_child_positioning<T: ParsedFontTrait>(
12747
    ctx: &mut LayoutContext<'_, T>,
12747
    child_index: usize,
12747
    child_node: &LayoutNodeHot,
12747
    self_content_box_pos: LogicalPosition,
12747
    child_relative_pos: LogicalPosition,
12747
    child_absolute_pos: LogicalPosition,
12747
) {
    // Always print positioning info for debugging
12747
    let child_dom_name = child_node
12747
        .dom_node_id
12747
        .and_then(|id| {
12708
            ctx.styled_dom
12708
                .node_data
12708
                .as_container()
12708
                .internal
12708
                .get(id.index())
12708
        })
12747
        .map(|n| format!("{:?}", n.node_type))
12747
        .unwrap_or_else(|| "Unknown".to_string());
12747
    let Some(debug_msgs) = ctx.debug_messages.as_mut() else {
140
        return;
    };
12607
    debug_msgs.push(LayoutDebugMessage::new(
12607
        LayoutDebugMessageType::PositionCalculation,
12607
        format!(
12607
            "[CHILD POS {}] {} - parent content-box=({:.2}, {:.2}) + relative=({:.2}, {:.2}) + \
12607
             margin=({:.2}, {:.2}) = absolute=({:.2}, {:.2})",
            child_index,
            child_dom_name,
            self_content_box_pos.x,
            self_content_box_pos.y,
            child_relative_pos.x,
            child_relative_pos.y,
12607
            child_node.box_props.unpack().margin.left,
12607
            child_node.box_props.unpack().margin.top,
            child_absolute_pos.x,
            child_absolute_pos.y
        ),
    ));
12747
}
/// Processes a single in-flow child: sets position and recurses.
///
/// For Flex/Grid containers, Taffy has already laid out the children completely.
/// We only recurse to position their grandchildren.
/// For Block/Inline/Table, layout_bfc/layout_ifc already laid out children in Pass 1.
/// We only need to set absolute positions and recurse for positioning grandchildren.
12747
fn process_inflow_child<T: ParsedFontTrait>(
12747
    ctx: &mut LayoutContext<'_, T>,
12747
    tree: &mut LayoutTree,
12747
    text_cache: &mut TextLayoutCache,
12747
    child_index: usize,
12747
    child_relative_pos: LogicalPosition,
12747
    self_content_box_pos: LogicalPosition,
12747
    inner_size_after_scrollbars: LogicalSize,
12747
    writing_mode: LayoutWritingMode,
12747
    is_flex_or_grid: bool,
12747
    calculated_positions: &mut super::PositionVec,
12747
    reflow_needed_for_scrollbars: &mut bool,
12747
    float_cache: &mut HashMap<usize, fc::FloatingContext>,
12747
) -> Result<()> {
    // Set relative position on child
    // child_relative_pos is [CoordinateSpace::Parent] - relative to parent's content-box
12747
    let child_warm = tree.warm_mut(child_index).ok_or(LayoutError::InvalidTree)?;
12747
    child_warm.relative_position = Some(child_relative_pos);
    // Calculate absolute position
    // self_content_box_pos is [CoordinateSpace::Window] - absolute position of parent's content-box
    // child_absolute_pos becomes [CoordinateSpace::Window] - absolute window position of child
12747
    let child_absolute_pos = LogicalPosition::new(
12747
        self_content_box_pos.x + child_relative_pos.x,
12747
        self_content_box_pos.y + child_relative_pos.y,
    );
    // Debug logging
    {
12747
        let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
12747
        log_child_positioning(
12747
            ctx,
12747
            child_index,
12747
            child_node,
12747
            self_content_box_pos,
12747
            child_relative_pos,
12747
            child_absolute_pos,
        );
    }
    // calculated_positions stores [CoordinateSpace::Window] - absolute positions
12747
    super::pos_set(calculated_positions, child_index, child_absolute_pos);
    // Get child's properties for recursion
12747
    let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
12747
    let child_bp = child_node.box_props.unpack();
12747
    let child_content_box_pos =
12747
        calculate_content_box_pos(child_absolute_pos, &child_bp);
12747
    let child_inner_size = child_bp
12747
        .inner_size(child_node.used_size.unwrap_or_default(), writing_mode);
12747
    let child_children: Vec<usize> = tree.children(child_index).to_vec();
12747
    let child_fc = child_node.formatting_context.clone();
    // Recurse to position grandchildren
    // OPTIMIZATION: For BFC/IFC children, layout_bfc/layout_ifc already computed their layout.
    // We just need to set absolute positions for descendants.
    // Only recurse if child has children to position.
12747
    if !child_children.is_empty() {
9950
        if is_flex_or_grid {
            // For Flex/Grid: Taffy already set used_size. Only recurse for grandchildren.
3
            position_flex_child_descendants(
3
                ctx,
3
                tree,
3
                text_cache,
3
                child_index,
3
                child_content_box_pos,
3
                child_inner_size,
3
                calculated_positions,
3
                reflow_needed_for_scrollbars,
3
                float_cache,
            )?;
9947
        } else {
9947
            // For Block/Inline/Table: The formatting context already laid out children.
9947
            // Recursively position grandchildren using their cached layout data.
9947
            position_bfc_child_descendants(
9947
                tree,
9947
                child_index,
9947
                child_content_box_pos,
9947
                calculated_positions,
9947
            );
9947
        }
2797
    }
12747
    Ok(())
12747
}
/// Recursively positions descendants of a BFC/IFC child without re-computing layout.
/// The layout was already computed by layout_bfc/layout_ifc.
/// We only need to convert relative positions to absolute positions.
41720
fn position_bfc_child_descendants(
41720
    tree: &LayoutTree,
41720
    node_index: usize,
41720
    content_box_pos: LogicalPosition,
41720
    calculated_positions: &mut super::PositionVec,
41720
) {
41720
    let Some(node) = tree.get(node_index) else { return };
41720
    for &child_index in tree.children(node_index) {
27965
        let Some(child_node) = tree.get(child_index) else { continue };
        // Use the relative_position that was set during formatting context layout
27965
        let child_rel_pos = tree.warm(child_index)
27965
            .and_then(|w| w.relative_position)
27965
            .unwrap_or_default();
27965
        let child_abs_pos = LogicalPosition::new(
27965
            content_box_pos.x + child_rel_pos.x,
27965
            content_box_pos.y + child_rel_pos.y,
        );
27965
        super::pos_set(calculated_positions, child_index, child_abs_pos);
        // Calculate child's content-box position for recursion
27965
        let cbp = child_node.box_props.unpack();
27965
        let child_content_box_pos = LogicalPosition::new(
27965
            child_abs_pos.x + cbp.border.left + cbp.padding.left,
27965
            child_abs_pos.y + cbp.border.top + cbp.padding.top,
        );
        // Recurse to grandchildren
27965
        position_bfc_child_descendants(tree, child_index, child_content_box_pos, calculated_positions);
    }
41720
}
/// Processes out-of-flow children (absolute/fixed positioned elements).
///
/// Out-of-flow elements don't appear in layout_output.positions but still need
/// a static position for when no explicit offsets are specified. This sets their
/// static position to the parent's content-box origin.
20399
fn process_out_of_flow_children<T: ParsedFontTrait>(
20399
    ctx: &mut LayoutContext<'_, T>,
20399
    tree: &mut LayoutTree,
20399
    text_cache: &mut TextLayoutCache,
20399
    node_index: usize,
20399
    self_content_box_pos: LogicalPosition,
20399
    containing_block_size: LogicalSize,
20399
    calculated_positions: &mut super::PositionVec,
20399
    reflow_needed_for_scrollbars: &mut bool,
20399
    float_cache: &mut HashMap<usize, fc::FloatingContext>,
20399
) -> Result<()> {
    // Collect out-of-flow children (those not already positioned)
20399
    let out_of_flow_children: Vec<(usize, Option<NodeId>)> = {
20399
        let current_node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
20399
        tree.children(node_index)
20399
            .iter()
20399
            .filter_map(|&child_index| {
17609
                if super::pos_contains(calculated_positions, child_index) {
9772
                    return None;
7837
                }
7837
                let child = tree.get(child_index)?;
7837
                Some((child_index, child.dom_node_id))
17609
            })
20399
            .collect()
    };
28236
    for (child_index, child_dom_id_opt) in out_of_flow_children {
7837
        let Some(child_dom_id) = child_dom_id_opt else {
            continue;
        };
7837
        let position_type = get_position_type(ctx.styled_dom, Some(child_dom_id));
7837
        if position_type != LayoutPosition::Absolute && position_type != LayoutPosition::Fixed {
7802
            continue;
35
        }
        // Set static position to parent's content-box origin
35
        super::pos_set(calculated_positions, child_index, self_content_box_pos);
        // Perform full layout for the absolutely positioned child so its
        // inline_layout_result is populated (text rendering needs this).
        // The containing block for abs-pos is the parent's padding box.
35
        calculate_layout_for_subtree(
35
            ctx,
35
            tree,
35
            text_cache,
35
            child_index,
35
            self_content_box_pos,
35
            containing_block_size,
35
            calculated_positions,
35
            reflow_needed_for_scrollbars,
35
            float_cache,
35
            ComputeMode::PerformLayout,
        )?;
    }
20399
    Ok(())
20399
}
/// Recursive, top-down pass to calculate used sizes and positions for a given subtree.
/// This is the single, authoritative function for in-flow layout.
///
/// Uses the per-node multi-slot cache (inspired by Taffy's 9+1 architecture) to
/// avoid O(n²) complexity. Each node has 9 measurement slots + 1 full layout slot.
///
/// ## Two-Mode Architecture (CSS Two-Pass Layout)
///
/// `compute_mode` determines behavior:
///
/// - **`ComputeSize`** (BFC Pass 1 — sizing):
///   Computes only the node's border-box size. On cache hit from measurement slots,
///   sets `used_size` and returns immediately — no child positioning. This is the
///   key to O(n) two-pass BFC: Pass 1 fills measurement caches cheaply.
///
/// - **`PerformLayout`** (BFC Pass 2 — positioning):
///   Computes size AND positions all children. On cache hit from layout slot,
///   applies cached child positions recursively. When Pass 2 provides the same
///   constraints as Pass 1, the "result matches request" optimization triggers
///   automatic cache hits.
///
/// ## Cache Hit Rates (Taffy's "result matches request" optimization)
///
/// When Pass 1 measures a node with available_size A and gets result_size R,
/// then Pass 2 provides R as a known_dimension, `get_size()` / `get_layout()`
/// recognize R == cached.result_size as a cache hit. This is the fundamental
/// mechanism ensuring O(n) total complexity across both passes.
20400
pub fn calculate_layout_for_subtree<T: ParsedFontTrait>(
20400
    ctx: &mut LayoutContext<'_, T>,
20400
    tree: &mut LayoutTree,
20400
    text_cache: &mut TextLayoutCache,
20400
    node_index: usize,
20400
    containing_block_pos: LogicalPosition,
20400
    containing_block_size: LogicalSize,
20400
    calculated_positions: &mut super::PositionVec,
20400
    reflow_needed_for_scrollbars: &mut bool,
20400
    float_cache: &mut HashMap<usize, fc::FloatingContext>,
20400
    compute_mode: ComputeMode,
20400
) -> Result<()> {
20400
    let _probe = match compute_mode {
15550
        ComputeMode::ComputeSize => crate::probe::Probe::span("size_node"),
4850
        ComputeMode::PerformLayout => crate::probe::Probe::span("pos_node"),
    };
    // HIT path; 0x60 = reached cache-miss compute.) Distinguishes stub/not-entered vs an
    // early Err in the cache-check vs the compute path.
    // === PER-NODE CACHE CHECK (Taffy-inspired 9+1 slot cache) ===
    //
    // Two-mode cache lookup (CSS two-pass architecture):
    //
    // ComputeSize (Pass 1 — sizing):
    //   1. Check measurement slots (get_size) → if hit, set used_size and return.
    //      No child positioning needed — we only need the node's border-box size.
    //   2. Fall back to layout slot → if hit, extract size from full layout result.
    //
    // PerformLayout (Pass 2 — positioning):
    //   1. Check layout slot (get_layout) → if hit, apply cached child positions.
    //   2. No fallback to measurement slots (we need full positions, not just size).
    //
    // This split is critical for O(n) two-pass BFC:
    // - Pass 1 populates measurement slots (cheap: no absolute positioning)
    // - Pass 2 hits layout slot or re-computes with positions
20400
    if node_index < ctx.cache_map.entries.len() {
20400
        match compute_mode {
            ComputeMode::ComputeSize => {
                // ComputeSize: check measurement slot first (Taffy's 9-slot scheme)
15550
                let sizing_hit = ctx.cache_map.entries[node_index]
15550
                    .get_size(0, containing_block_size)
15550
                    .cloned();
15550
                if let Some(cached_sizing) = sizing_hit {
                    // SIZING CACHE HIT — set used_size and return immediately.
                    // No child positioning needed in ComputeSize mode.
1
                    drop(crate::probe::Probe::span("size_cache_hit_sizing"));
1
                    if let Some(node) = tree.get_mut(node_index) {
1
                        node.used_size = Some(cached_sizing.result_size);
1
                    }
1
                    if let Some(warm) = tree.warm_mut(node_index) {
1
                        warm.escaped_top_margin = cached_sizing.escaped_top_margin;
1
                        warm.escaped_bottom_margin = cached_sizing.escaped_bottom_margin;
1
                        warm.baseline = cached_sizing.baseline;
1
                    }
1
                    return Ok(());
15549
                }
                // Fall through to layout slot check
15549
                let layout_hit = ctx.cache_map.entries[node_index]
15549
                    .get_layout(containing_block_size)
15549
                    .cloned();
15549
                if let Some(cached_layout) = layout_hit {
                    // Layout slot hit in ComputeSize mode — extract size only
                    drop(crate::probe::Probe::span("size_cache_hit_layout"));
                    if let Some(node) = tree.get_mut(node_index) {
                        node.used_size = Some(cached_layout.result_size);
                    }
                    if let Some(warm) = tree.warm_mut(node_index) {
                        warm.overflow_content_size = Some(cached_layout.content_size);
                        warm.scrollbar_info = Some(cached_layout.scrollbar_info.clone());
                    }
                    return Ok(());
15549
                }
15549
                drop(crate::probe::Probe::span("size_cache_miss"));
            }
            ComputeMode::PerformLayout => {
                // PerformLayout: check layout slot (the single "full layout" slot)
4850
                let layout_hit = ctx.cache_map.entries[node_index]
4850
                    .get_layout(containing_block_size)
4850
                    .cloned();
4850
                if let Some(cached_layout) = layout_hit {
                    drop(crate::probe::Probe::span("pos_cache_hit"));
                    // LAYOUT CACHE HIT — apply cached results with child positions
                    if let Some(node) = tree.get_mut(node_index) {
                        node.used_size = Some(cached_layout.result_size);
                    }
                    if let Some(warm) = tree.warm_mut(node_index) {
                        warm.overflow_content_size = Some(cached_layout.content_size);
                        warm.scrollbar_info = Some(cached_layout.scrollbar_info.clone());
                    }
                    let box_props = tree.get(node_index)
                        .map(|n| n.box_props.unpack())
                        .unwrap_or_default();
                    let self_content_box_pos = calculate_content_box_pos(containing_block_pos, &box_props);
                    // Apply cached child positions and recurse
                    let result_size = cached_layout.result_size;
                    for (child_index, child_relative_pos) in &cached_layout.child_positions {
                        let child_abs_pos = LogicalPosition::new(
                            self_content_box_pos.x + child_relative_pos.x,
                            self_content_box_pos.y + child_relative_pos.y,
                        );
                        super::pos_set(calculated_positions, *child_index, child_abs_pos);
                        let inner = box_props.inner_size(
                            result_size,
                            LayoutWritingMode::HorizontalTb,
                        );
                        // Subtract scrollbar reservation from the available size
                        // passed to children. This mirrors what layout_bfc does in
                        // the MISS path — without it, a reflow-loop cache hit
                        // would hand children the full content-box width, ignoring
                        // any vertical/horizontal scrollbar that was detected.
                        let child_available_size =
                            cached_layout.scrollbar_info.shrink_size(inner);
                        calculate_layout_for_subtree(
                            ctx,
                            tree,
                            text_cache,
                            *child_index,
                            child_abs_pos,
                            child_available_size,
                            calculated_positions,
                            reflow_needed_for_scrollbars,
                            float_cache,
                            compute_mode,
                        )?;
                    }
                    return Ok(());
4850
                }
            }
        }
    }
    // === CACHE MISS — compute layout ===
20399
    if compute_mode == ComputeMode::PerformLayout {
4850
        drop(crate::probe::Probe::span("pos_cache_miss"));
15549
    }
    // returned Ok; 0x64 = layout_formatting_context returned Ok. Last value before the
    // Err pins the failing phase (fires per recursive node; bare body is shallow).
    // Phase 1: Prepare layout context (calculate used size, constraints)
    let PreparedLayoutContext {
20399
        constraints,
20399
        dom_id,
20399
        writing_mode,
20399
        mut final_used_size,
20399
        box_props,
    } = {
20399
        let _p = crate::probe::Probe::span("prepare_layout_context");
20399
        prepare_layout_context(ctx, tree, node_index, containing_block_size)?
    };
    // Phase 1.5: Update used_size BEFORE calling layout_formatting_context.
    //
    // When a node is cloned from the old tree (clone_node_from_old), its used_size
    // retains the value from the previous layout pass. If the containing block changed
    // (e.g. viewport resize), the stale used_size would cause layout_bfc() to compute
    // an incorrect children_containing_block_size. By updating used_size here, we ensure
    // that layout_bfc reads the freshly resolved size from prepare_layout_context.
    {
20399
        let is_table_cell = tree.get(node_index).map_or(false, |n| {
20399
            matches!(n.formatting_context, FormattingContext::TableCell)
20399
        });
20399
        if !is_table_cell {
12419
            if let Some(node) = tree.get_mut(node_index) {
12419
                node.used_size = Some(final_used_size);
12419
            }
7980
        }
    }
    // Phase 2: Layout children using the formatting context
20399
    let layout_result = {
20399
        let _p = crate::probe::Probe::span("layout_formatting_context");
20399
        layout_formatting_context(ctx, tree, text_cache, node_index, &constraints, float_cache)?
    };
20399
    let content_size = layout_result.output.overflow_size;
    // If layout_formatting_context adjusted this node's used_size (e.g.
    // layout_flex_grid auto-applying box-sizing:border-box on the root),
    // propagate that back into final_used_size so Phase 3 (scrollbars),
    // Phase 4 (final write), and the self_content_box_pos calculation all
    // see the same border-box that the children were laid out inside.
20399
    if let Some(adjusted) = tree.get(node_index).and_then(|n| n.used_size) {
17424
        final_used_size = adjusted;
17424
    }
    // Phase 2.5: Resolve 'auto' main-axis size based on content
    // For anonymous boxes, use default styled node state
20399
    let styled_node_state = dom_id
20399
        .and_then(|id| ctx.styled_dom.styled_nodes.as_container().get(id).cloned())
20399
        .map(|n| n.styled_node_state)
20399
        .unwrap_or_default();
20399
    let css_height: MultiValue<LayoutHeight> = match dom_id {
20360
        Some(id) => get_css_height(ctx.styled_dom, id, &styled_node_state),
39
        None => MultiValue::Auto, // Anonymous boxes have auto height
    };
    // +spec:overflow:44ef3b - scroll container detection: overflow scroll/auto makes box a scroll container
    // Check if this node is a scroll container (overflow: scroll/auto).
    // Scroll containers must NOT expand to fit content — their height is
    // determined by the containing block, and overflow is scrollable.
    //
    // Exception: if the containing block height is infinite (unconstrained),
    // we must still grow, since you can't scroll inside an infinitely tall box.
20399
    let is_scroll_container = dom_id.map_or(false, |id| {
20360
        let ov_x = get_overflow_x(ctx.styled_dom, id, &styled_node_state);
20360
        let ov_y = get_overflow_y(ctx.styled_dom, id, &styled_node_state);
20360
        matches!(ov_x, MultiValue::Exact(LayoutOverflow::Scroll) | MultiValue::Exact(LayoutOverflow::Auto))
20360
            || matches!(ov_y, MultiValue::Exact(LayoutOverflow::Scroll) | MultiValue::Exact(LayoutOverflow::Auto))
20360
    });
20399
    if should_use_content_height(&css_height) {
17896
        let skip_expansion = is_scroll_container
            && containing_block_size.height.is_finite()
            && containing_block_size.height > 0.0;
17896
        if !skip_expansion {
17896
            final_used_size = apply_content_based_height(
17896
                final_used_size,
17896
                content_size,
17896
                tree,
17896
                node_index,
17896
                writing_mode,
17896
            );
17896
        }
2503
    }
    // Phase 3: Scrollbar handling
    // Anonymous boxes don't have scrollbars
20399
    let skip_scrollbar_check = ctx.fragmentation_context.is_some();
20399
    let scrollbar_info = match dom_id {
20360
        Some(id) => compute_scrollbar_info(
20360
            ctx,
20360
            id,
20360
            &styled_node_state,
20360
            content_size,
20360
            &box_props,
20360
            final_used_size,
20360
            writing_mode,
        ),
39
        None => ScrollbarRequirements::default(),
    };
20399
    if check_scrollbar_change(tree, node_index, &scrollbar_info, skip_scrollbar_check) {
        *reflow_needed_for_scrollbars = true;
20399
    }
20399
    let merged_scrollbar_info = merge_scrollbar_info(tree, node_index, &scrollbar_info);
20399
    let content_box_size = box_props.inner_size(final_used_size, writing_mode);
20399
    let inner_size_after_scrollbars = merged_scrollbar_info.shrink_size(content_box_size);
    // Phase 4: Update this node's state
20399
    let self_content_box_pos = {
        {
20399
            let current_node = tree.get_mut(node_index).ok_or(LayoutError::InvalidTree)?;
            // Table cells get their size from the table layout algorithm, don't overwrite
20399
            let is_table_cell = matches!(
20399
                current_node.formatting_context,
                FormattingContext::TableCell
            );
20399
            if !is_table_cell || current_node.used_size.is_none() {
15394
                current_node.used_size = Some(final_used_size);
15394
            }
        }
        // Update warm fields
20399
        if let Some(warm) = tree.warm_mut(node_index) {
20399
            warm.scrollbar_info = Some(merged_scrollbar_info.clone());
20399
            // Store overflow content size for scroll frame calculation
20399
            // +spec:overflow:f28d6a - hanging glyphs should be ink overflow, not scrollable overflow (not yet subtracted from content_size)
20399
            warm.overflow_content_size = Some(content_size);
20399
        }
        // self_content_box_pos is [CoordinateSpace::Window] - the absolute position of this node's content-box
20399
        let current_node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
20399
        let current_bp = current_node.box_props.unpack();
20399
        let pos = calculate_content_box_pos(containing_block_pos, &current_bp);
20399
        log_content_box_calculation(ctx, node_index, current_node, containing_block_pos, pos);
20399
        pos
    };
    // Phase 5: Determine formatting context type
20399
    let is_flex_or_grid = {
20399
        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
20187
        matches!(
20399
            node.formatting_context,
            FormattingContext::Flex | FormattingContext::Grid
        )
    };
    // Phase 6: Process in-flow children
    // Positions in layout_result.output.positions are [CoordinateSpace::Parent] - relative to this node's content-box
20399
    let positions: Vec<_> = layout_result
20399
        .output
20399
        .positions
20399
        .iter()
20399
        .map(|(&idx, &pos)| (idx, pos))
20399
        .collect();
    // Store child positions for cache
20399
    let child_positions_for_cache: Vec<(usize, LogicalPosition)> = positions.clone();
33146
    for (child_index, child_relative_pos) in positions {
12747
        process_inflow_child(
12747
            ctx,
12747
            tree,
12747
            text_cache,
12747
            child_index,
12747
            child_relative_pos,
12747
            self_content_box_pos,
12747
            inner_size_after_scrollbars,
12747
            writing_mode,
12747
            is_flex_or_grid,
12747
            calculated_positions,
12747
            reflow_needed_for_scrollbars,
12747
            float_cache,
        )?;
    }
    // Phase 7: Process out-of-flow children (absolute/fixed)
20399
    process_out_of_flow_children(
20399
        ctx,
20399
        tree,
20399
        text_cache,
20399
        node_index,
20399
        self_content_box_pos,
20399
        inner_size_after_scrollbars,
20399
        calculated_positions,
20399
        reflow_needed_for_scrollbars,
20399
        float_cache,
    )?;
    // === STORE RESULT IN PER-NODE CACHE (Taffy-inspired 9+1 slot cache) ===
    // Store both the full layout entry and a sizing measurement entry.
    // This enables O(n) two-pass BFC: Pass 1 populates cache, Pass 2 reads it.
20399
    if node_index < ctx.cache_map.entries.len() {
20399
        let warm_ref = tree.warm(node_index);
20399
        let baseline = warm_ref.and_then(|n| n.baseline);
20399
        let escaped_top = warm_ref.and_then(|n| n.escaped_top_margin);
20399
        let escaped_bottom = warm_ref.and_then(|n| n.escaped_bottom_margin);
        // Store in the layout slot (PerformLayout result)
20399
        ctx.cache_map.get_mut(node_index).store_layout(LayoutCacheEntry {
20399
            available_size: containing_block_size,
20399
            result_size: final_used_size,
20399
            content_size,
20399
            child_positions: child_positions_for_cache.clone(),
20399
            escaped_top_margin: escaped_top,
20399
            escaped_bottom_margin: escaped_bottom,
20399
            scrollbar_info: merged_scrollbar_info.clone(),
20399
        });
        // Also store in a measurement slot (slot 0: both dimensions known)
        // This enables the "result matches request" optimization (Taffy pattern):
        // when Pass 2 provides the same size as Pass 1 measured, it's a cache hit.
20399
        ctx.cache_map.get_mut(node_index).store_size(0, SizingCacheEntry {
20399
            available_size: containing_block_size,
20399
            result_size: final_used_size,
20399
            baseline,
20399
            escaped_top_margin: escaped_top,
20399
            escaped_bottom_margin: escaped_bottom,
20399
        });
    }
20399
    Ok(())
20400
}
/// Recursively set static positions for out-of-flow descendants without doing layout
/// Recursively positions descendants of Flex/Grid children.
///
/// When a Flex container lays out its children via Taffy, the children have their
/// used_size and relative_position set, but their GRANDCHILDREN don't have positions
/// in calculated_positions yet. This function traverses down the tree and positions
/// all descendants properly.
8
fn position_flex_child_descendants<T: ParsedFontTrait>(
8
    ctx: &mut LayoutContext<'_, T>,
8
    tree: &mut LayoutTree,
8
    text_cache: &mut TextLayoutCache,
8
    node_index: usize,
8
    content_box_pos: LogicalPosition,
8
    available_size: LogicalSize,
8
    calculated_positions: &mut super::PositionVec,
8
    reflow_needed_for_scrollbars: &mut bool,
8
    float_cache: &mut HashMap<usize, fc::FloatingContext>,
8
) -> Result<()> {
8
    let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
8
    let children: Vec<usize> = tree.children(node_index).to_vec();
8
    let fc = node.formatting_context.clone();
    // If this node is itself a Flex/Grid container, its children were laid out by Taffy
    // and already have relative_position set. We just need to convert to absolute and recurse.
8
    if matches!(fc, FormattingContext::Flex | FormattingContext::Grid) {
2
        for &child_index in &children {
1
            let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1
            let child_rel_pos = tree.warm(child_index)
1
                .and_then(|w| w.relative_position)
1
                .unwrap_or_default();
1
            let child_abs_pos = LogicalPosition::new(
1
                content_box_pos.x + child_rel_pos.x,
1
                content_box_pos.y + child_rel_pos.y,
            );
            // Insert position
1
            super::pos_set(calculated_positions, child_index, child_abs_pos);
            // Get child's content box for recursion
1
            let cbp = child_node.box_props.unpack();
1
            let child_content_box = LogicalPosition::new(
1
                child_abs_pos.x
1
                    + cbp.border.left
1
                    + cbp.padding.left,
1
                child_abs_pos.y
1
                    + cbp.border.top
1
                    + cbp.padding.top,
            );
1
            let child_inner_size = cbp.inner_size(
1
                child_node.used_size.unwrap_or_default(),
1
                LayoutWritingMode::HorizontalTb,
            );
            // Recurse
1
            position_flex_child_descendants(
1
                ctx,
1
                tree,
1
                text_cache,
1
                child_index,
1
                child_content_box,
1
                child_inner_size,
1
                calculated_positions,
1
                reflow_needed_for_scrollbars,
1
                float_cache,
            )?;
        }
    } else {
        // For Block/Inline/Table children, their descendants need proper layout calculation
        // Use the output.positions from their own layout
7
        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
7
        let children: Vec<usize> = tree.children(node_index).to_vec();
11
        for &child_index in &children {
4
            let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
4
            let child_rel_pos = tree.warm(child_index)
4
                .and_then(|w| w.relative_position)
4
                .unwrap_or_default();
4
            let child_abs_pos = LogicalPosition::new(
4
                content_box_pos.x + child_rel_pos.x,
4
                content_box_pos.y + child_rel_pos.y,
            );
            // Insert position
4
            super::pos_set(calculated_positions, child_index, child_abs_pos);
            // Get child's content box for recursion
4
            let cbp = child_node.box_props.unpack();
4
            let child_content_box = LogicalPosition::new(
4
                child_abs_pos.x
4
                    + cbp.border.left
4
                    + cbp.padding.left,
4
                child_abs_pos.y
4
                    + cbp.border.top
4
                    + cbp.padding.top,
            );
4
            let child_inner_size = cbp.inner_size(
4
                child_node.used_size.unwrap_or_default(),
4
                LayoutWritingMode::HorizontalTb,
            );
            // Recurse
4
            position_flex_child_descendants(
4
                ctx,
4
                tree,
4
                text_cache,
4
                child_index,
4
                child_content_box,
4
                child_inner_size,
4
                calculated_positions,
4
                reflow_needed_for_scrollbars,
4
                float_cache,
            )?;
        }
    }
8
    Ok(())
8
}
/// Checks if the given CSS height value should use content-based sizing
28525
fn should_use_content_height(css_height: &MultiValue<LayoutHeight>) -> bool {
28525
    match css_height {
        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
            // Auto/Initial/Inherit height should use content-based sizing
21840
            true
        }
6685
        MultiValue::Exact(height) => match height {
            LayoutHeight::Auto => {
                // Auto height should use content-based sizing
                true
            }
6685
            LayoutHeight::Px(px) => {
                // Check if it's zero or if it has explicit value
                // If it's a percentage or em, it's not auto
                use azul_css::props::basic::{pixel::PixelValue, SizeMetric};
6685
                px == &PixelValue::zero()
6685
                    || (px.metric != SizeMetric::Px
2100
                        && px.metric != SizeMetric::Percent
                        && px.metric != SizeMetric::Em
                        && px.metric != SizeMetric::Rem)
            }
            LayoutHeight::MinContent | LayoutHeight::MaxContent | LayoutHeight::FitContent(_) => {
                // These are content-based, so they should use the content size
                true
            }
            LayoutHeight::Calc(_) => {
                // Calc expressions are not auto, they compute to a specific value
                false
            }
        },
    }
28525
}
/// Applies content-based height sizing to a node
///
/// **Note**: This function respects min-height/max-height constraints from Phase 1.
///
/// According to CSS 2.2 § 10.7, when height is 'auto', the final height must be
/// max(min_height, min(content_height, max_height)).
///
/// The `used_size` parameter already contains the size constrained by
/// min-height/max-height from the initial sizing pass. We must take the
/// maximum of this constrained size and the new content-based size to ensure
/// min-height is not lost.
21840
fn apply_content_based_height(
21840
    mut used_size: LogicalSize,
21840
    content_size: LogicalSize,
21840
    tree: &LayoutTree,
21840
    node_index: usize,
21840
    writing_mode: LayoutWritingMode,
21840
) -> LogicalSize {
21840
    let node_props = tree.get(node_index).unwrap().box_props.unpack();
21840
    let main_axis_padding_border =
21840
        node_props.padding.main_sum(writing_mode) + node_props.border.main_sum(writing_mode);
    // CRITICAL: 'old_main_size' holds the size constrained by min-height/max-height from Phase 1
21840
    let old_main_size = used_size.main(writing_mode);
21840
    let new_main_size = content_size.main(writing_mode) + main_axis_padding_border;
    // Final size = max(min_height_constrained_size, content_size)
    // This ensures that min-height is respected even when content is smaller
21840
    let final_main_size = old_main_size.max(new_main_size);
21840
    used_size = used_size.with_main(writing_mode, final_main_size);
21840
    used_size
21840
}
// hash_styled_node_data() removed — replaced by NodeDataFingerprint::compute()
15890
fn calculate_subtree_hash(node_self_hash: u64, child_hashes: &[u64]) -> SubtreeHash {
15890
    let mut hasher = DefaultHasher::new();
15890
    node_self_hash.hash(&mut hasher);
15890
    child_hashes.hash(&mut hasher);
15890
    SubtreeHash(hasher.finish())
15890
}
/// Computes CSS counter values for all nodes in the layout tree.
///
/// This function traverses the tree in document order and processes counter-reset
/// and counter-increment properties. The computed values are stored in cache.counters.
///
/// CSS counters work with a stack-based scoping model:
/// - `counter-reset` creates a new scope and sets the counter to a value
/// - `counter-increment` increments the counter in the current scope
/// - When leaving a subtree, counter scopes are popped
4655
pub fn compute_counters(
4655
    styled_dom: &StyledDom,
4655
    tree: &LayoutTree,
4655
    counters: &mut HashMap<(usize, String), i32>,
4655
) {
    // Track counter stacks: counter_name -> Vec<value>
    // Each entry in the Vec represents a nested scope
4655
    let mut counter_stacks: HashMap<String, Vec<i32>> = HashMap::new();
    // Stack to track which counters were reset at each tree level
    // When we pop back up the tree, we need to pop these counter scopes
4655
    let mut scope_stack: Vec<Vec<String>> = Vec::new();
4655
    compute_counters_recursive(
4655
        styled_dom,
4655
        tree,
4655
        tree.root,
4655
        counters,
4655
        &mut counter_stacks,
4655
        &mut scope_stack,
    );
4655
}
25445
fn compute_counters_recursive(
25445
    styled_dom: &StyledDom,
25445
    tree: &LayoutTree,
25445
    node_idx: usize,
25445
    counters: &mut HashMap<(usize, String), i32>,
25445
    counter_stacks: &mut std::collections::HashMap<String, Vec<i32>>,
25445
    scope_stack: &mut Vec<Vec<String>>,
25445
) {
25445
    let node = match tree.get(node_idx) {
25445
        Some(n) => n,
        None => return,
    };
    // Skip pseudo-elements (::marker, ::before, ::after) for counter processing
    // Pseudo-elements inherit counter values from their parent element
    // but don't participate in counter-reset or counter-increment themselves
25445
    if tree.warm(node_idx).and_then(|w| w.pseudo_element.as_ref()).is_some() {
        // Store the parent's counter values for this pseudo-element
        // so it can be looked up during marker text generation
        if let Some(parent_idx) = node.parent {
            // Copy all counter values from parent to this pseudo-element
            let parent_counters: Vec<_> = counters
                .iter()
                .filter(|((idx, _), _)| *idx == parent_idx)
                .map(|((_, name), &value)| (name.clone(), value))
                .collect();
            for (counter_name, value) in parent_counters {
                counters.insert((node_idx, counter_name), value);
            }
        }
        // Don't recurse to children of pseudo-elements
        // (pseudo-elements shouldn't have children in normal circumstances)
        return;
25445
    }
    // Only process real DOM nodes, not anonymous boxes
25445
    let dom_id = match node.dom_node_id {
25270
        Some(id) => id,
        None => {
            // For anonymous boxes, just recurse to children
175
            for &child_idx in tree.children(node_idx) {
175
                compute_counters_recursive(
175
                    styled_dom,
175
                    tree,
175
                    child_idx,
175
                    counters,
175
                    counter_stacks,
175
                    scope_stack,
175
                );
175
            }
175
            return;
        }
    };
25270
    let node_data = &styled_dom.node_data.as_container()[dom_id];
25270
    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
25270
    let cache = &styled_dom.css_property_cache.ptr;
    // Track which counters we reset at this level (for cleanup later)
25270
    let mut reset_counters_at_this_level = Vec::new();
    // CSS Lists §3: display: list-item automatically increments the "list-item" counter
    // Check if this is a list-item
25270
    let display = {
        use crate::solver3::getters::get_display_property;
25270
        get_display_property(styled_dom, Some(dom_id)).exact()
    };
25270
    let is_list_item = matches!(display, Some(LayoutDisplay::ListItem));
    // FAST PATH: almost no nodes declare counter-reset/counter-increment.
    // Single-bit check in compact cache lets us skip two cascade walks per node.
25270
    let has_counter_css = node_state.is_normal()
25270
        && cache.compact_cache.as_ref().map_or(true, |cc| cc.has_counter(dom_id.index()));
    // Process counter-reset (now properly typed)
25270
    let counter_reset = if has_counter_css {
        cache
            .get_counter_reset(node_data, &dom_id, node_state)
            .and_then(|v| v.get_property())
    } else {
25270
        None
    };
25270
    if let Some(counter_reset) = counter_reset {
        let counter_name_str = counter_reset.counter_name.as_str();
        if counter_name_str != "none" {
            let counter_name = counter_name_str.to_string();
            let reset_value = counter_reset.value;
            // Reset the counter by pushing a new scope
            counter_stacks
                .entry(counter_name.clone())
                .or_default()
                .push(reset_value);
            reset_counters_at_this_level.push(counter_name);
        }
25270
    }
    // Process counter-increment (now properly typed)
25270
    let counter_inc = if has_counter_css {
        cache
            .get_counter_increment(node_data, &dom_id, node_state)
            .and_then(|v| v.get_property())
    } else {
25270
        None
    };
25270
    if let Some(counter_inc) = counter_inc {
        let counter_name_str = counter_inc.counter_name.as_str();
        if counter_name_str != "none" {
            let counter_name = counter_name_str.to_string();
            let inc_value = counter_inc.value;
            // Increment the counter in the current scope
            let stack = counter_stacks.entry(counter_name.clone()).or_default();
            if stack.is_empty() {
                // Auto-initialize if counter doesn't exist
                stack.push(inc_value);
            } else if let Some(current) = stack.last_mut() {
                *current += inc_value;
            }
        }
25270
    }
    // CSS Lists §3: display: list-item automatically increments "list-item" counter
25270
    if is_list_item {
        let counter_name = "list-item".to_string();
        let stack = counter_stacks.entry(counter_name.clone()).or_default();
        if stack.is_empty() {
            // Auto-initialize if counter doesn't exist
            stack.push(1);
        } else {
            if let Some(current) = stack.last_mut() {
                *current += 1;
            }
        }
25270
    }
    // Store the current counter values for this node
25270
    for (counter_name, stack) in counter_stacks.iter() {
        if let Some(&value) = stack.last() {
            counters.insert((node_idx, counter_name.clone()), value);
        }
    }
    // Push scope tracking for cleanup
25270
    scope_stack.push(reset_counters_at_this_level.clone());
    // Recurse to children
25270
    for &child_idx in tree.children(node_idx) {
20615
        compute_counters_recursive(
20615
            styled_dom,
20615
            tree,
20615
            child_idx,
20615
            counters,
20615
            counter_stacks,
20615
            scope_stack,
20615
        );
20615
    }
    // Pop counter scopes that were created at this level
25270
    if let Some(reset_counters) = scope_stack.pop() {
25270
        for counter_name in reset_counters {
            if let Some(stack) = counter_stacks.get_mut(&counter_name) {
                stack.pop();
            }
        }
    }
25445
}