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
60060
    fn default() -> Self {
180
60060
        Self {
181
60060
            measure_entries: [None, None, None, None, None, None, None, None, None],
182
60060
            layout_entry: None,
183
60060
            is_empty: true, // fresh cache is empty/dirty
184
60060
        }
185
60060
    }
186
}
187

            
188
impl NodeCache {
189
    /// Clear all cache entries, marking this node as dirty.
190
23980
    pub fn clear(&mut self) {
191
23980
        self.measure_entries = [None, None, None, None, None, None, None, None, None];
192
23980
        self.layout_entry = None;
193
23980
        self.is_empty = true;
194
23980
    }
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
30932
    pub fn get_size(&self, slot: usize, known_dims: LogicalSize) -> Option<&SizingCacheEntry> {
227
30932
        let entry = self.measure_entries[slot].as_ref()?;
228
        // Exact match on input constraints
229
4180
        if (known_dims.width - entry.available_size.width).abs() < CACHE_SIZE_EPSILON
230
4180
            && (known_dims.height - entry.available_size.height).abs() < CACHE_SIZE_EPSILON
231
        {
232
308
            return Some(entry);
233
3872
        }
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
3872
        if (known_dims.width - entry.result_size.width).abs() < CACHE_SIZE_EPSILON
239
3168
            && (known_dims.height - entry.result_size.height).abs() < CACHE_SIZE_EPSILON
240
        {
241
            return Some(entry);
242
3872
        }
243
3872
        None
244
30932
    }
245

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

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

            
269
    /// Store a full layout result.
270
52844
    pub fn store_layout(&mut self, entry: LayoutCacheEntry) {
271
52844
        self.layout_entry = Some(entry);
272
52844
        self.is_empty = false;
273
52844
    }
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
7128
    pub fn resize_to_tree(&mut self, tree_len: usize) {
295
7128
        self.entries.resize_with(tree_len, NodeCache::default);
296
7128
    }
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
105688
    pub fn get_mut(&mut self, node_index: usize) -> &mut NodeCache {
307
105688
        &mut self.entries[node_index]
308
105688
    }
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
62964
    pub fn mark_dirty(&mut self, node_index: usize, tree: &[LayoutNodeHot]) {
317
62964
        if node_index >= self.entries.len() {
318
            return;
319
62964
        }
320
62964
        let cache = &mut self.entries[node_index];
321
62964
        if cache.is_empty {
322
62744
            return; // Already dirty → ancestors are too
323
220
        }
324
220
        cache.clear();
325

            
326
        // Propagate upward (Taffy's early-stop optimization)
327
220
        let mut current = tree.get(node_index).and_then(|n| n.parent);
328
220
        while let Some(parent_idx) = current {
329
88
            if parent_idx >= self.entries.len() {
330
                break;
331
88
            }
332
88
            let parent_cache = &mut self.entries[parent_idx];
333
88
            if parent_cache.is_empty {
334
88
                break; // Stop early — ancestor already dirty
335
            }
336
            parent_cache.clear();
337
            current = tree.get(parent_idx).and_then(|n| n.parent);
338
        }
339
62964
    }
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
    /// Drop all incremental-reuse state so the next `layout_document` lays the
418
    /// DOM out from scratch (cold path), as if no previous frame existed.
419
    ///
420
    /// Required before laying out a DOM whose NodeIds are NOT a stable evolution
421
    /// of whatever this (shared) cache last held — namely VirtualView / iframe
422
    /// child DOMs, which their callbacks rebuild wholesale on every invocation.
423
    /// Incremental reconciliation matches/reuses subtrees by NodeId + subtree
424
    /// hash; on a wholesale rebuild those NodeIds are reassigned, so reusing the
425
    /// prior tree can graft NodeIds that no longer exist in the new StyledDom
426
    /// (panic: out-of-bounds node_data index when the DOM shrinks — e.g. the map
427
    /// dropping tiles on zoom-out).
428
308
    pub fn reset_incremental(&mut self) {
429
308
        self.tree = None;
430
308
        self.cache_map = LayoutCacheMap::default();
431
308
        self.cached_display_list = None;
432
308
        self.prev_dom_ptr = 0;
433
308
        self.counters.clear();
434
308
        self.float_cache.clear();
435
308
    }
436

            
437
    /// Approximate heap bytes retained by this LayoutCache.
438
    pub fn memory_report(&self) -> Solver3CacheMemoryReport {
439
        let tree_report = self.tree.as_ref().map(|t| t.memory_report());
440
        let tree_bytes = tree_report.as_ref().map(|r| r.total_bytes()).unwrap_or(0);
441
        // cache_map: Vec<NodeCache>; NodeCache has 9 Option<SizingCacheEntry>
442
        // + 1 Option<LayoutCacheEntry>. Count filled layout entries' child_positions.
443
        let mut cache_map_bytes = self.cache_map.entries.capacity()
444
            * core::mem::size_of::<NodeCache>();
445
        for e in &self.cache_map.entries {
446
            if let Some(le) = &e.layout_entry {
447
                cache_map_bytes += le.child_positions.capacity()
448
                    * core::mem::size_of::<(usize, LogicalPosition)>();
449
            }
450
        }
451
        Solver3CacheMemoryReport {
452
            tree_bytes,
453
            tree_report,
454
            calculated_positions_bytes: self.calculated_positions.len()
455
                * core::mem::size_of::<LogicalPosition>(),
456
            previous_positions_bytes: self.previous_positions.len()
457
                * core::mem::size_of::<LogicalPosition>(),
458
            scroll_ids_bytes: self.scroll_ids.len()
459
                * (core::mem::size_of::<usize>() + core::mem::size_of::<u64>()),
460
            scroll_id_to_node_id_bytes: self.scroll_id_to_node_id.len()
461
                * (core::mem::size_of::<u64>() + core::mem::size_of::<NodeId>()),
462
            counters_bytes: self.counters.iter().map(|((_, name), _)| {
463
                core::mem::size_of::<(usize, String)>()
464
                    + core::mem::size_of::<i32>()
465
                    + name.capacity()
466
            }).sum(),
467
            float_cache_bytes: self.float_cache.len() * 256, // conservative per-FC
468
            cache_map_bytes,
469
            cached_display_list_bytes: if self.cached_display_list.is_some() { 2048 } else { 0 },
470
        }
471
    }
472
}
473

            
474
/// The result of a reconciliation pass.
475
#[derive(Debug, Default)]
476
pub struct ReconciliationResult {
477
    /// Set of nodes whose intrinsic size needs to be recalculated (bottom-up pass).
478
    pub intrinsic_dirty: BTreeSet<usize>,
479
    /// Set of layout roots whose subtrees need a new top-down layout pass.
480
    pub layout_roots: BTreeSet<usize>,
481
    /// Set of nodes that only need a paint/display-list update (no relayout).
482
    pub paint_dirty: BTreeSet<usize>,
483
}
484

            
485
impl ReconciliationResult {
486
    /// Checks if any layout or paint work is needed.
487
7260
    pub fn is_clean(&self) -> bool {
488
7260
        self.intrinsic_dirty.is_empty()
489
            && self.layout_roots.is_empty()
490
            && self.paint_dirty.is_empty()
491
7260
    }
492

            
493
    /// Returns true if full layout work is needed for at least one node.
494
    pub fn needs_layout(&self) -> bool {
495
        !self.intrinsic_dirty.is_empty() || !self.layout_roots.is_empty()
496
    }
497

            
498
    /// Returns true if only paint work is needed (no layout).
499
    pub fn needs_paint_only(&self) -> bool {
500
        !self.needs_layout() && !self.paint_dirty.is_empty()
501
    }
502
}
503

            
504
/// After dirty subtrees are laid out, this repositions their clean siblings
505
/// without recalculating their internal layout. This is a critical optimization.
506
///
507
/// This function acts as a dispatcher, inspecting the parent's formatting context
508
/// and calling the appropriate repositioning algorithm. For complex layout modes
509
/// like Flexbox or Grid, this optimization is skipped, as a full relayout is
510
/// often required to correctly recalculate spacing and sizing for all siblings.
511
7260
pub fn reposition_clean_subtrees(
512
7260
    styled_dom: &StyledDom,
513
7260
    tree: &LayoutTree,
514
7260
    layout_roots: &BTreeSet<usize>,
515
7260
    calculated_positions: &mut super::PositionVec,
516
7260
) {
517
    // Find the unique parents of all dirty layout roots. These are the containers
518
    // where sibling positions need to be adjusted.
519
7260
    let mut parents_to_reposition = BTreeSet::new();
520
14520
    for &root_idx in layout_roots {
521
7260
        if let Some(parent_idx) = tree.get(root_idx).and_then(|n| n.parent) {
522
            parents_to_reposition.insert(parent_idx);
523
7260
        }
524
    }
525

            
526
7260
    for parent_idx in parents_to_reposition {
527
        let parent_node = match tree.get(parent_idx) {
528
            Some(n) => n,
529
            None => continue,
530
        };
531

            
532
        // Dispatch to the correct repositioning logic based on the parent's layout mode.
533
        match parent_node.formatting_context {
534
            // Cases that use simple block-flow stacking can be optimized.
535
            FormattingContext::Block { .. } | FormattingContext::TableRowGroup => {
536
                reposition_block_flow_siblings(
537
                    styled_dom,
538
                    parent_idx,
539
                    tree,
540
                    layout_roots,
541
                    calculated_positions,
542
                );
543
            }
544

            
545
            FormattingContext::Flex | FormattingContext::Grid => {
546
                // Taffy handles this, so if a child is dirty, the parent would have
547
                // already been marked as a layout_root and re-laid out by Taffy.
548
                // We do nothing here for Flex or Grid.
549
            }
550

            
551
            FormattingContext::Table | FormattingContext::TableRow => {
552
                // TODO: Table layout is interdependent. A change in one cell's size
553
                // can affect the entire column's width or row's height, requiring a
554
                // full relayout of the table. This optimization is skipped.
555
            }
556

            
557
            // Other contexts either don't contain children in a way that this
558
            // optimization applies (e.g., Inline, TableCell) or are handled by other
559
            // layout mechanisms (e.g., OutOfFlow).
560
            _ => { /* Do nothing */ }
561
        }
562
    }
563
7260
}
564

            
565
/// Convert LayoutOverflow to OverflowBehavior
566
/// CSS Overflow Module Level 3: initial value of `overflow` is `visible`.
567
// +spec:overflow:3a6297 - initial value 'visible', maps hidden/scroll/auto overflow behaviors
568
224664
pub fn to_overflow_behavior(overflow: MultiValue<LayoutOverflow>) -> fc::OverflowBehavior {
569
224664
    match overflow.unwrap_or(LayoutOverflow::Visible) {
570
219912
        LayoutOverflow::Visible => fc::OverflowBehavior::Visible,
571
4752
        LayoutOverflow::Hidden | LayoutOverflow::Clip => fc::OverflowBehavior::Hidden,
572
        LayoutOverflow::Scroll => fc::OverflowBehavior::Scroll,
573
        LayoutOverflow::Auto => fc::OverflowBehavior::Auto,
574
    }
575
224664
}
576

            
577
/// Convert StyleTextAlign to fc::TextAlign
578
// +spec:text-alignment-spacing:43ea0a - text-align-all shorthand: aligns all lines except last (overridden by text-align-last)
579
52844
pub const fn style_text_align_to_fc(text_align: StyleTextAlign) -> fc::TextAlign {
580
52844
    match text_align {
581
52844
        StyleTextAlign::Start | StyleTextAlign::Left => fc::TextAlign::Start,
582
        StyleTextAlign::End | StyleTextAlign::Right => fc::TextAlign::End,
583
        StyleTextAlign::Center => fc::TextAlign::Center,
584
        StyleTextAlign::Justify => fc::TextAlign::Justify,
585
    }
586
52844
}
587

            
588
/// Collects DOM child IDs from the node hierarchy into a Vec.
589
///
590
/// This is a helper function that flattens the sibling iteration into a simple loop.
591
/// Children with `display: none` are filtered out since they generate no boxes.
592
44484
pub fn collect_children_dom_ids(styled_dom: &StyledDom, parent_dom_id: NodeId) -> Vec<NodeId> {
593
44484
    let hierarchy_container = styled_dom.node_hierarchy.as_container();
594
44484
    let mut children = Vec::new();
595

            
596
44484
    let Some(hierarchy_item) = hierarchy_container.get(parent_dom_id) else {
597
        return children;
598
    };
599

            
600
44484
    let Some(mut child_id) = hierarchy_item.first_child_id(parent_dom_id) else {
601
        // DEBUG (2026-06-02 children-None): first_child_id returned None for this
602
        // parent → 0xC0000000 marker @0x40540+parent*4. REVERT before commit.
603
        unsafe {
604
17732
            let pi = parent_dom_id.index();
605
17732
            if pi < 8 { crate::az_mark(((0x40540 + pi * 4)) as u32, (0xC000_0000u32) as u32); }
606
        }
607
17732
        return children;
608
    };
609

            
610
    // +spec:display-property:9f02c6 - display:none elements generate no boxes
611
    // +spec:display-property:3b507e - display:none excludes subtree from box tree
612
26752
    if get_display_type(styled_dom, child_id) != LayoutDisplay::None {
613
26752
        children.push(child_id);
614
26752
    }
615
39820
    while let Some(hierarchy_item) = hierarchy_container.get(child_id) {
616
39820
        let Some(next) = hierarchy_item.next_sibling_id() else {
617
26752
            break;
618
        };
619
13068
        if get_display_type(styled_dom, next) != LayoutDisplay::None {
620
13024
            children.push(next);
621
13024
        }
622
13068
        child_id = next;
623
    }
624

            
625
    // DEBUG (2026-06-02 children-None): record collected child count per parent
626
    // @0x40540+parent*4 (0xCC00_00NN). N=0 with first_child Some ⇒ get_display_type
627
    // mis-lift skipped them; N>0 ⇒ walk works. REVERT before commit.
628
    unsafe {
629
26752
        let pi = parent_dom_id.index();
630
26752
        if pi < 8 {
631
15664
            crate::az_mark(((0x40540 + pi * 4)) as u32, (0xCC00_0000u32 | (children.len() as u32 & 0xffff)) as u32);
632
15664
        }
633
    }
634
26752
    children
635
44484
}
636

            
637
/// Checks if a flex container is simple enough to be treated like a block-stack for
638
/// repositioning.
639
pub fn is_simple_flex_stack(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> bool {
640
    let Some(id) = dom_id else { return false };
641
    let binding = styled_dom.styled_nodes.as_container();
642
    let styled_node = match binding.get(id) {
643
        Some(styled_node) => styled_node,
644
        None => return false,
645
    };
646

            
647
    // Must be a single-line flex container
648
    let wrap = get_wrap(styled_dom, id, &styled_node.styled_node_state);
649

            
650
    if wrap.unwrap_or_default() != LayoutFlexWrap::NoWrap {
651
        return false;
652
    }
653

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

            
657
    if !matches!(
658
        justify.unwrap_or_default(),
659
        LayoutJustifyContent::FlexStart | LayoutJustifyContent::Start
660
    ) {
661
        return false;
662
    }
663

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

            
672
    true
673
}
674

            
675
/// Repositions clean children within a simple block-flow layout (like a BFC or a
676
/// table-row-group). It stacks children along the main axis, preserving their
677
/// previously calculated cross-axis alignment.
678
pub fn reposition_block_flow_siblings(
679
    styled_dom: &StyledDom,
680
    parent_idx: usize,
681
    tree: &LayoutTree,
682
    layout_roots: &BTreeSet<usize>,
683
    calculated_positions: &mut super::PositionVec,
684
) {
685
    let parent_node = match tree.get(parent_idx) {
686
        Some(n) => n,
687
        None => return,
688
    };
689
    let dom_id = parent_node.dom_node_id.unwrap_or(NodeId::ZERO);
690
    let styled_node_state = styled_dom
691
        .styled_nodes
692
        .as_container()
693
        .get(dom_id)
694
        .map(|n| n.styled_node_state.clone())
695
        .unwrap_or_default();
696

            
697
    let writing_mode = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
698

            
699
    let parent_pos = calculated_positions
700
        .get(parent_idx)
701
        .copied()
702
        .unwrap_or_default();
703

            
704
    let parent_bp = parent_node.box_props.unpack();
705
    let content_box_origin = LogicalPosition::new(
706
        parent_pos.x + parent_bp.padding.left,
707
        parent_pos.y + parent_bp.padding.top,
708
    );
709

            
710
    let mut main_pen = 0.0;
711

            
712
    for &child_idx in tree.children(parent_idx) {
713
        let child_node = match tree.get(child_idx) {
714
            Some(n) => n,
715
            None => continue,
716
        };
717

            
718
        let child_size = child_node.used_size.unwrap_or_default();
719
        let child_bp = child_node.box_props.unpack();
720
        let child_main_sum = child_bp.margin.main_sum(writing_mode);
721
        let margin_box_main_size = child_size.main(writing_mode) + child_main_sum;
722

            
723
        if layout_roots.contains(&child_idx) {
724
            // This child was DIRTY and has been correctly repositioned.
725
            // Update the pen to the position immediately after this child.
726
            let new_pos = match calculated_positions.get(child_idx) {
727
                Some(p) => *p,
728
                None => continue,
729
            };
730

            
731
            let main_axis_offset = if writing_mode.is_vertical() {
732
                new_pos.x - content_box_origin.x
733
            } else {
734
                new_pos.y - content_box_origin.y
735
            };
736

            
737
            main_pen = main_axis_offset
738
                + child_size.main(writing_mode)
739
                + child_bp.margin.main_end(writing_mode);
740
        } else {
741
            // This child is *clean*. Calculate its new position and shift its
742
            // entire subtree.
743
            let old_pos = match calculated_positions.get(child_idx) {
744
                Some(p) => *p,
745
                None => continue,
746
            };
747

            
748
            let child_main_start = child_bp.margin.main_start(writing_mode);
749
            let new_main_pos = main_pen + child_main_start;
750
            let old_relative_pos = tree.warm(child_idx)
751
                .and_then(|w| w.relative_position)
752
                .unwrap_or_default();
753
            let cross_pos = if writing_mode.is_vertical() {
754
                old_relative_pos.y
755
            } else {
756
                old_relative_pos.x
757
            };
758
            let new_relative_pos =
759
                LogicalPosition::from_main_cross(new_main_pos, cross_pos, writing_mode);
760

            
761
            let new_absolute_pos = LogicalPosition::new(
762
                content_box_origin.x + new_relative_pos.x,
763
                content_box_origin.y + new_relative_pos.y,
764
            );
765

            
766
            if old_pos != new_absolute_pos {
767
                let delta = LogicalPosition::new(
768
                    new_absolute_pos.x - old_pos.x,
769
                    new_absolute_pos.y - old_pos.y,
770
                );
771
                shift_subtree_position(child_idx, delta, tree, calculated_positions);
772
            }
773

            
774
            main_pen += margin_box_main_size;
775
        }
776
    }
777
}
778

            
779
/// Helper to recursively shift the absolute position of a node and all its descendants.
780
pub fn shift_subtree_position(
781
    node_idx: usize,
782
    delta: LogicalPosition,
783
    tree: &LayoutTree,
784
    calculated_positions: &mut super::PositionVec,
785
) {
786
    if let Some(pos) = calculated_positions.get_mut(node_idx) {
787
        pos.x += delta.x;
788
        pos.y += delta.y;
789
    }
790

            
791
    if let Some(node) = tree.get(node_idx) {
792
        let children = tree.children(node_idx).to_vec();
793
        for &child_idx in &children {
794
            shift_subtree_position(child_idx, delta, tree, calculated_positions);
795
        }
796
    }
797
}
798

            
799
/// Compares the new DOM against the cached tree, creating a new tree
800
/// and identifying which parts need to be re-laid out.
801
/// Count how many of the supplied DOM children would actually end up
802
/// in the layout tree. Mirrors the filters applied by
803
/// `LayoutTreeBuilder::build_recursive` so reconciliation can compare
804
/// like-for-like:
805
///
806
/// - `display: none` nodes are skipped entirely.
807
/// - In table structural contexts (table, row-group, row) whitespace
808
///   text nodes are skipped (CSS 2.2 §17.2.1, matches
809
///   `should_skip_for_table_structure`).
810
/// - Whitespace-only inline runs that sit between block siblings
811
///   collapse to zero boxes (CSS 2.2 §9.2.2.1).
812
///
813
/// The first two rules drop children unconditionally; the third only
814
/// fires on siblings surrounding a block-level child, so we detect it
815
/// by walking the run pairs. We do not build the runs — just count
816
/// survivors.
817
44484
fn layout_relevant_child_count(
818
44484
    styled_dom: &azul_core::styled_dom::StyledDom,
819
44484
    children: &[NodeId],
820
44484
    parent_id: NodeId,
821
44484
) -> usize {
822
    use super::getters::{get_display_property, MultiValue};
823
    use super::layout_tree::{is_block_level, is_whitespace_only_text};
824

            
825
44484
    let parent_display = match get_display_property(styled_dom, Some(parent_id)) {
826
44484
        MultiValue::Exact(d) => d,
827
        _ => azul_css::props::layout::display::LayoutDisplay::Block,
828
    };
829
44484
    let is_table_structural = matches!(
830
44484
        parent_display,
831
        azul_css::props::layout::display::LayoutDisplay::Table
832
            | azul_css::props::layout::display::LayoutDisplay::InlineTable
833
            | azul_css::props::layout::display::LayoutDisplay::TableRowGroup
834
            | azul_css::props::layout::display::LayoutDisplay::TableHeaderGroup
835
            | azul_css::props::layout::display::LayoutDisplay::TableFooterGroup
836
            | azul_css::props::layout::display::LayoutDisplay::TableRow
837
    );
838

            
839
44484
    let has_any_block_child = children
840
44484
        .iter()
841
44484
        .any(|&id| is_block_level(styled_dom, id));
842

            
843
44484
    let mut count = 0usize;
844
    // When parent has any block child, whitespace-only inline runs
845
    // surrounding blocks collapse. We approximate that by skipping
846
    // whitespace text whenever any block sibling exists.
847
44484
    let collapse_inline_whitespace = has_any_block_child;
848
84260
    for &id in children {
849
        // display:none drops
850
39776
        let display = match get_display_property(styled_dom, Some(id)) {
851
39776
            MultiValue::Exact(d) => d,
852
            _ => azul_css::props::layout::display::LayoutDisplay::Block,
853
        };
854
39776
        if matches!(display, azul_css::props::layout::display::LayoutDisplay::None) {
855
            continue;
856
39776
        }
857
        // Table-structural whitespace drops.
858
39776
        if is_table_structural && is_whitespace_only_text(styled_dom, id) {
859
            continue;
860
39776
        }
861
        // Whitespace-only inline run collapse when mixed with blocks.
862
39776
        if collapse_inline_whitespace
863
20372
            && !is_block_level(styled_dom, id)
864
440
            && is_whitespace_only_text(styled_dom, id)
865
        {
866
            continue;
867
39776
        }
868
39776
        count += 1;
869
    }
870
44484
    count
871
44484
}
872

            
873
4708
pub fn reconcile_and_invalidate<T: ParsedFontTrait>(
874
4708
    ctx: &mut LayoutContext<'_, T>,
875
4708
    cache: &LayoutCache,
876
4708
    viewport: LogicalRect,
877
4708
) -> Result<(LayoutTree, ReconciliationResult)> {
878
4708
    let _probe_outer = crate::probe::Probe::span("reconcile_and_invalidate");
879
4708
    let mut new_tree_builder = LayoutTreeBuilder::new(ctx.viewport_size);
880
4708
    let mut recon_result = ReconciliationResult::default();
881
    // A viewport SIZE change invalidates every computed size: percentage, flex,
882
    // and absolute insets (top/right/bottom/left) all resolve against the
883
    // viewport / containing block. Incrementally reusing the cached layout tree
884
    // left out-of-flow and VirtualView nodes sized against the OLD viewport — e.g.
885
    // the map's absolutely-positioned container kept its old size, so a maximized
886
    // window showed tiles only in the original rect and grey everywhere else
887
    // (#9 "grey on resize"). On a size change, drop the cached tree so the whole
888
    // tree is laid out fresh against the new viewport. (Position-only moves keep
889
    // the incremental path.)
890
4708
    let viewport_resized = cache.viewport.map_or(true, |v| v.size != viewport.size);
891
4708
    let old_tree = if viewport_resized {
892
4576
        None
893
    } else {
894
132
        cache.tree.as_ref()
895
    };
896

            
897
4708
    if viewport_resized {
898
4576
        recon_result.layout_roots.insert(0); // Root is always index 0
899
4576
    }
900

            
901
4708
    let root_dom_id = ctx
902
4708
        .styled_dom
903
4708
        .root
904
4708
        .into_crate_internal()
905
4708
        .unwrap_or(NodeId::ZERO);
906
4708
    let root_idx = reconcile_recursive(
907
4708
        ctx.styled_dom,
908
4708
        root_dom_id,
909
4708
        old_tree.map(|t| t.root),
910
4708
        None,
911
4708
        old_tree,
912
4708
        &mut new_tree_builder,
913
4708
        &mut recon_result,
914
4708
        &mut ctx.debug_messages,
915
    )?;
916

            
917
    // Clean up layout roots: if a parent is a layout root, its children don't need to be.
918
4708
    let final_layout_roots = recon_result
919
4708
        .layout_roots
920
4708
        .iter()
921
42460
        .filter(|&&idx| {
922
42460
            let mut current = new_tree_builder.get(idx).and_then(|n| n.parent);
923
42548
            while let Some(p_idx) = current {
924
37884
                if recon_result.layout_roots.contains(&p_idx) {
925
37796
                    return false;
926
88
                }
927
88
                current = new_tree_builder.get(p_idx).and_then(|n| n.parent);
928
            }
929
4664
            true
930
42460
        })
931
4708
        .copied()
932
4708
        .collect();
933
4708
    recon_result.layout_roots = final_layout_roots;
934

            
935
4708
    let new_tree = new_tree_builder.build(root_idx);
936
    // layout_document's step marker is stuck at 1 (post-`?` not reached), the
937
    // lifted `?` mis-discriminated this Ok as Err (niche-Result mis-lift).
938
4708
    { let _ = (0xCC00_0001u32); }
939
4708
    Ok((new_tree, recon_result))
940
4708
}
941

            
942
/// CSS 2.2 § 9.2.2.1: Checks whether an inline run consists entirely of
943
/// whitespace-only text nodes, in which case it should NOT generate an
944
/// anonymous IFC wrapper in a BFC mixed-content context.
945
///
946
/// This prevents whitespace between block elements from creating empty
947
/// anonymous blocks that take up vertical space (regression c33e94b0).
948
///
949
/// Exception: if the parent (or any ancestor) has `white-space: pre`,
950
/// `pre-wrap`, or `pre-line`, whitespace IS significant and the wrapper
951
/// must still be created.
952
88
fn is_whitespace_only_inline_run(
953
88
    styled_dom: &StyledDom,
954
88
    inline_run: &[(usize, NodeId)],
955
88
    parent_dom_id: NodeId,
956
88
) -> bool {
957
    use azul_css::props::style::text::StyleWhiteSpace;
958

            
959
88
    if inline_run.is_empty() {
960
        return true;
961
88
    }
962

            
963
    // Check if the parent preserves whitespace
964
88
    let parent_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
965
88
    let white_space = match get_white_space_property(styled_dom, parent_dom_id, parent_state) {
966
88
        MultiValue::Exact(ws) => Some(ws),
967
        _ => None,
968
    };
969

            
970
    // If white-space preserves whitespace, don't strip
971
88
    if matches!(
972
88
        white_space,
973
        Some(StyleWhiteSpace::Pre) | Some(StyleWhiteSpace::PreWrap) | Some(StyleWhiteSpace::PreLine)
974
    ) {
975
        return false;
976
88
    }
977

            
978
    // Check that every node in the run is a whitespace-only text node
979
88
    let binding = styled_dom.node_data.as_container();
980
88
    for &(_, dom_id) in inline_run {
981
88
        if let Some(data) = binding.get(dom_id) {
982
88
            match data.get_node_type() {
983
88
                NodeType::Text(text) => {
984
88
                    let s = text.as_str();
985
88
                    if !s.chars().all(|c| matches!(c, ' ' | '\t' | '\n' | '\r' | '\x0C')) {
986
88
                        return false; // Non-whitespace text → must create wrapper
987
                    }
988
                }
989
                _ => {
990
                    return false; // Non-text inline element → must create wrapper
991
                }
992
            }
993
        }
994
    }
995

            
996
    true // All nodes are whitespace-only text
997
88
}
998

            
999
/// Recursively traverses the new DOM and old tree, building a new tree and marking dirty nodes.
44484
pub fn reconcile_recursive(
44484
    styled_dom: &StyledDom,
44484
    new_dom_id: NodeId,
44484
    old_tree_idx: Option<usize>,
44484
    new_parent_idx: Option<usize>,
44484
    old_tree: Option<&LayoutTree>,
44484
    new_tree_builder: &mut LayoutTreeBuilder,
44484
    recon: &mut ReconciliationResult,
44484
    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
44484
) -> Result<usize> {
44484
    let node_data = &styled_dom.node_data.as_container()[new_dom_id];
44484
    let old_cold = old_tree.and_then(|t| old_tree_idx.and_then(|idx| t.cold(idx)));
44484
    match (old_tree.is_some(), old_tree_idx.is_some(), old_cold.is_some()) {
42196
        (false, _, _) => drop(crate::probe::Probe::span("recon_old_tree_none")),
        (true, false, _) => drop(crate::probe::Probe::span("recon_old_idx_none")),
        (true, true, false) => drop(crate::probe::Probe::span("recon_cold_none")),
2288
        (true, true, true) => drop(crate::probe::Probe::span("recon_cold_some")),
    }
    // Compute the new multi-field fingerprint instead of a single hash.
44484
    let new_fingerprint = {
44484
        let _p = crate::probe::Probe::span("fingerprint_compute");
44484
        NodeDataFingerprint::compute(
44484
            node_data,
44484
            styled_dom.styled_nodes.as_container().get(new_dom_id).map(|n| &n.styled_node_state),
        )
    };
    // Compare fingerprints to determine what changed (Layout, Paint, or Nothing).
44484
    let dirty_flag = match old_cold {
        None => {
42196
            drop(crate::probe::Probe::span("fp_new_node"));
42196
            DirtyFlag::Layout // new node → full layout
        },
2288
        Some(old_c) => {
2288
            let change_set = old_c.node_data_fingerprint.diff(&new_fingerprint);
2288
            if change_set.needs_layout() {
88
                drop(crate::probe::Probe::span("fp_needs_layout"));
                // Cache the env check in a `OnceLock<bool>`: this branch
                // fires once per dirty node (hundreds on cold layout),
                // and a direct `env::var` is a mutex + hashmap lookup
                // on macOS (~100 ns/call) even when the env var is unset.
                static FP_DUMP_ENABLED: std::sync::OnceLock<bool> =
                    std::sync::OnceLock::new();
88
                let enabled = *FP_DUMP_ENABLED.get_or_init(|| {
44
                    std::env::var_os("AZ_FP_DUMP").is_some()
44
                });
88
                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,
                        );
                    }
88
                }
88
                DirtyFlag::Layout
2200
            } else if change_set.needs_paint() {
                drop(crate::probe::Probe::span("fp_needs_paint"));
                DirtyFlag::Paint
            } else {
2200
                drop(crate::probe::Probe::span("fp_clean"));
2200
                DirtyFlag::None
            }
        }
    };
44484
    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.
44484
    let new_node_idx = if dirty_flag >= DirtyFlag::Layout || old_tree.is_none() {
42284
        { let _ = (0xBB00_0001u32); }
42284
        let idx = new_tree_builder.create_node_from_dom(
42284
            styled_dom,
42284
            new_dom_id,
42284
            new_parent_idx,
42284
            debug_messages,
        );
        // Blockify replaced/inline flex-or-grid items (CSS Display 3 §2.7). The
        // full `process_node` build does this; this incremental path called
        // `create_node_from_dom` directly and skipped it, so a flex-item <img>
        // (e.g. the AzulPaint canvas) stayed inline and ignored flex-grow.
42284
        new_tree_builder.blockify_node_display(styled_dom, new_dom_id, idx, new_parent_idx);
42284
        idx
    } else {
2200
        { let _ = (0xBB00_0002u32); }
        // Paint-only or clean: clone the old node (preserving layout cache)
2200
        let old_full_node = old_tree
2200
            .and_then(|t| old_tree_idx.and_then(|idx| t.get_full_node(idx)))
2200
            .ok_or(LayoutError::InvalidTree)?;
2200
        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
2200
        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;
            }
2200
        }
2200
        idx
    };
    // reconcile_recursive sees it. 0 = correct (the first node); 64 (matching the
    // build-marker root_idx) = the usize return mis-reads here.
44484
    { 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;
44484
        let display = get_display_property(styled_dom, Some(new_dom_id))
44484
            .exact();
44484
        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);
44484
        }
    }
    // Reconcile children to check for structural changes and build the new tree structure.
44484
    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};
44484
        let parent_display = match get_display_property(styled_dom, Some(new_dom_id)) {
44484
            MultiValue::Exact(d) => d,
            _ => azul_css::props::layout::display::LayoutDisplay::Block,
        };
44484
        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
        ) {
5236
            new_children_dom_ids.retain(|&id| {
5236
                !super::layout_tree::is_whitespace_only_text(styled_dom, id)
5236
            });
41536
        }
    }
    // 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).
44484
    let old_children_indices: Vec<usize> = old_tree
44484
        .and_then(|t| old_tree_idx.map(|idx| t.children(idx).to_vec()))
44484
        .unwrap_or_default();
44484
    let old_children_by_dom: alloc::collections::BTreeMap<NodeId, usize> = old_tree
44484
        .and_then(|t| old_tree_idx.map(|idx| {
2288
            t.children(idx).iter()
2288
                .filter_map(|&cidx| t.get(cidx).and_then(|n| n.dom_node_id).map(|did| (did, cidx)))
2288
                .collect()
2288
        }))
44484
        .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.
44484
    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.
44484
    let new_layout_relevant_count = layout_relevant_child_count(styled_dom, &new_children_dom_ids, new_dom_id);
44484
    let mut children_are_different = new_layout_relevant_count != old_layout_relevant_count;
44484
    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
44484
    let has_block_child = new_children_dom_ids
44484
        .iter()
44484
        .any(|&id| is_block_level(styled_dom, id));
    // CSS Flexbox §4 / Grid §6: every in-flow child of a flex/grid container
    // becomes a (blockified) flex/grid item. Anonymous-block wrapping of inline
    // runs is a BLOCK-container concept and must NOT apply here — otherwise an
    // inline-level child (e.g. an <img> with flex-grow, default display
    // inline-block) gets wrapped in an anonymous IFC block, so it's no longer a
    // direct flex item and its flex-grow is ignored (laid out 300×0). Processing
    // each child directly lets `blockify_node_display` (in create_node_from_dom)
    // see the flex/grid parent and blockify the child into a real flex item.
44484
    let parent_is_flex_or_grid = matches!(
44484
        get_display_type(styled_dom, new_dom_id),
        LayoutDisplay::Flex
            | LayoutDisplay::InlineFlex
            | LayoutDisplay::Grid
            | LayoutDisplay::InlineGrid
    );
44484
    if !has_block_child || parent_is_flex_or_grid {
        // All children are inline (block container) OR the parent is a flex/grid
        // container (all children are direct items) — no anonymous boxes needed.
        // Process each child directly.
38588
        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).
24420
            let old_child_idx = old_children_by_dom.get(&new_child_dom_id).copied();
24420
            let reconciled_child_idx = reconcile_recursive(
24420
                styled_dom,
24420
                new_child_dom_id,
24420
                old_child_idx,
24420
                Some(new_node_idx),
24420
                old_tree,
24420
                new_tree_builder,
24420
                recon,
24420
                debug_messages,
            )?;
24420
            if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
24420
                new_child_hashes.push(child_node.subtree_hash.0);
24420
            }
24420
            if old_tree.and_then(|t| t.cold(old_child_idx?).map(|n| n.subtree_hash))
24420
                != new_tree_builder
24420
                    .get(reconciled_child_idx)
24420
                    .map(|n| n.subtree_hash)
22572
            {
22572
                children_are_different = true;
22572
            }
        }
    } else {
        // Mixed content: block and inline children
        // We must create anonymous block boxes around consecutive inline runs
5896
        if let Some(msgs) = debug_messages.as_mut() {
5852
            msgs.push(LayoutDebugMessage::info(format!(
5852
                "[reconcile_recursive] Mixed content in node {}: creating anonymous IFC wrappers",
5852
                new_dom_id.index()
5852
            )));
5852
        }
5896
        let mut inline_run: Vec<(usize, NodeId)> = Vec::new(); // (dom_child_index, dom_id)
15356
        for (i, &new_child_dom_id) in new_children_dom_ids.iter().enumerate() {
15356
            if is_block_level(styled_dom, new_child_dom_id) {
                // End current inline run if any
15268
                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
88
                    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
88
                    let anon_idx = new_tree_builder.create_anonymous_node(
88
                        new_node_idx,
88
                        AnonymousBoxType::InlineWrapper,
88
                        FormattingContext::Inline, // IFC for inline content
                    );
88
                    if let Some(msgs) = debug_messages.as_mut() {
88
                        msgs.push(LayoutDebugMessage::info(format!(
88
                            "[reconcile_recursive] Created anonymous IFC wrapper (layout_idx={}) for {} inline children: {:?}",
                            anon_idx,
88
                            inline_run.len(),
88
                            inline_run.iter().map(|(_, id)| id.index()).collect::<Vec<_>>()
                        )));
                    }
                    // Process each inline child under the anonymous wrapper
88
                    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.
88
                        let old_child_idx = old_children_by_dom.get(&inline_dom_id).copied()
88
                            .or_else(|| old_tree
88
                                .and_then(|t| t.dom_to_layout.get(&inline_dom_id))
88
                                .and_then(|v| v.first().copied()));
88
                        let reconciled_child_idx = reconcile_recursive(
88
                            styled_dom,
88
                            inline_dom_id,
88
                            old_child_idx,
88
                            Some(anon_idx), // Parent is the anonymous wrapper
88
                            old_tree,
88
                            new_tree_builder,
88
                            recon,
88
                            debug_messages,
                        )?;
88
                        if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
88
                            new_child_hashes.push(child_node.subtree_hash.0);
88
                        }
                    }
                    // 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.
88
                    children_are_different = true;
                    } // end else (non-whitespace run)
15180
                }
                // Process block-level child directly under parent
15268
                let old_child_idx = old_children_by_dom.get(&new_child_dom_id).copied()
15268
                    .or_else(|| old_children_indices.get(i).copied());
15268
                let reconciled_child_idx = reconcile_recursive(
15268
                    styled_dom,
15268
                    new_child_dom_id,
15268
                    old_child_idx,
15268
                    Some(new_node_idx),
15268
                    old_tree,
15268
                    new_tree_builder,
15268
                    recon,
15268
                    debug_messages,
                )?;
15268
                if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
15268
                    new_child_hashes.push(child_node.subtree_hash.0);
15268
                }
15268
                if old_tree.and_then(|t| t.cold(old_child_idx?).map(|n| n.subtree_hash))
15268
                    != new_tree_builder
15268
                        .get(reconciled_child_idx)
15268
                        .map(|n| n.subtree_hash)
15136
                {
15136
                    children_are_different = true;
15136
                }
88
            } else {
88
                // Inline-level child - add to current run
88
                inline_run.push((i, new_child_dom_id));
88
            }
        }
        // Process any remaining inline run at the end
5896
        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)
5896
        }
    }
    // After reconciling children, calculate this node's full subtree hash.
    // Use a combined hash of the fingerprint fields for the subtree hash.
44484
    let node_self_hash = {
        use std::hash::{DefaultHasher, Hash, Hasher};
44484
        let mut h = DefaultHasher::new();
44484
        new_fingerprint.hash(&mut h);
44484
        h.finish()
    };
44484
    let final_subtree_hash = calculate_subtree_hash(node_self_hash, &new_child_hashes);
44484
    if let Some(current_node) = new_tree_builder.get_mut(new_node_idx) {
44484
        current_node.subtree_hash = final_subtree_hash;
44484
    }
    // Classify this node into the appropriate dirty set based on what changed.
44484
    if dirty_flag >= DirtyFlag::Layout || children_are_different {
42460
        recon.intrinsic_dirty.insert(new_node_idx);
42460
        recon.layout_roots.insert(new_node_idx);
42460
    } else if dirty_flag == DirtyFlag::Paint {
        recon.paint_dirty.insert(new_node_idx);
2024
    }
44484
    Ok(new_node_idx)
44484
}
/// 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.
42137
fn prepare_layout_context<'a, T: ParsedFontTrait>(
42137
    ctx: &LayoutContext<'a, T>,
42137
    tree: &LayoutTree,
42137
    node_index: usize,
42137
    containing_block_size: LogicalSize,
42137
) -> Result<PreparedLayoutContext<'a>> {
42137
    let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
42137
    let warm = tree.warm(node_index).ok_or(LayoutError::InvalidTree)?;
42137
    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.
42137
    let intrinsic = warm.intrinsic_sizes.clone().unwrap_or_default();
42137
    let final_used_size = calculate_used_size_for_node(
42137
        ctx.styled_dom,
42137
        dom_id, // Now Option<NodeId>
42137
        &containing_block_size,
42137
        intrinsic,
42137
        &node.box_props.unpack(),
42137
        &ctx.viewport_size,
    )?;
    // Phase 2: Layout children using a formatting context
    // Use pre-computed styles from LayoutNodeWarm instead of repeated lookups
42137
    let writing_mode = warm.computed_style.writing_mode;
42137
    let text_align = warm.computed_style.text_align;
42137
    let display = warm.computed_style.display;
42137
    let overflow_y = warm.computed_style.overflow_y;
    // Check if height is auto (no explicit height set)
42137
    let height_is_auto = warm.computed_style.height.is_none();
42137
    let available_size_for_children = if height_is_auto {
        // Height is auto - use containing block size as available size
31810
        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.
31810
        let available_width = match display {
9552
            LayoutDisplay::Inline => containing_block_size.width,
22258
            _ => inner_size.width,
        };
31810
        LogicalSize {
31810
            width: available_width,
31810
            // Use containing block height!
31810
            height: containing_block_size.height,
31810
        }
    } else {
        // Height is explicit - use inner size (after padding/border)
10327
        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).
42137
    let wm_ctx = crate::solver3::geometry::WritingModeContext::new(
42137
        writing_mode,
42137
        warm.computed_style.direction,
42137
        warm.computed_style.text_orientation,
    );
42137
    let constraints = LayoutConstraints {
42137
        available_size: available_size_for_children,
42137
        bfc_state: None,
42137
        writing_mode,
42137
        writing_mode_ctx: wm_ctx,
42137
        text_align: style_text_align_to_fc(text_align),
42137
        containing_block_size,
42137
        available_width_type: Text3AvailableSpace::Definite(available_size_for_children.width),
42137
    };
42137
    Ok(PreparedLayoutContext {
42137
        constraints,
42137
        dom_id,
42137
        writing_mode,
42137
        final_used_size,
42137
        box_props: node.box_props.unpack(),
42137
    })
42137
}
/// 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.
75195
pub fn compute_scrollbar_info_core<T: ParsedFontTrait>(
75195
    ctx: &LayoutContext<'_, T>,
75195
    dom_id: NodeId,
75195
    styled_node_state: &azul_core::styled_dom::StyledNodeState,
75195
    content_size: LogicalSize,
75195
    container_size: LogicalSize,
75195
) -> ScrollbarRequirements {
    // +spec:overflow:08b60d - non-interactive media: UA may show scroll indicators but we skip them for print
75195
    if ctx.fragmentation_context.is_some() {
307
        return ScrollbarRequirements::default();
74888
    }
74888
    let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, styled_node_state);
74888
    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.
74888
    let scrollbar_style = crate::solver3::getters::get_scrollbar_style_cached(
74888
        ctx, dom_id, styled_node_state,
    );
74888
    let scrollbar_width_px = scrollbar_style.reserve_width_px;
74888
    let mut reqs = fc::check_scrollbar_necessity(
74888
        content_size,
74888
        container_size,
74888
        to_overflow_behavior(overflow_x),
74888
        to_overflow_behavior(overflow_y),
74888
        scrollbar_width_px,
    );
74888
    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.
74888
    let scrollbar_gutter = get_scrollbar_gutter_property(ctx.styled_dom, dom_id, styled_node_state)
74888
        .unwrap_or(azul_css::props::layout::overflow::StyleScrollbarGutter::Auto);
74888
    let ob_y = to_overflow_behavior(overflow_y);
74888
    let is_scroll_container = matches!(ob_y, fc::OverflowBehavior::Scroll | fc::OverflowBehavior::Auto);
74888
    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)
            }
        }
74888
    }
74888
    reqs
75195
}
/// 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, …)`.
42045
fn compute_scrollbar_info<T: ParsedFontTrait>(
42045
    ctx: &LayoutContext<'_, T>,
42045
    dom_id: NodeId,
42045
    styled_node_state: &azul_core::styled_dom::StyledNodeState,
42045
    content_size: LogicalSize,
42045
    box_props: &crate::solver3::geometry::BoxProps,
42045
    final_used_size: LogicalSize,
42045
    writing_mode: LayoutWritingMode,
42045
) -> ScrollbarRequirements {
42045
    let container_size = box_props.inner_size(final_used_size, writing_mode);
42045
    compute_scrollbar_info_core(ctx, dom_id, styled_node_state, content_size, container_size)
42045
}
/// 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.
52844
fn check_scrollbar_change(
52844
    tree: &LayoutTree,
52844
    node_index: usize,
52844
    scrollbar_info: &ScrollbarRequirements,
52844
    skip_scrollbar_check: bool,
52844
) -> bool {
52844
    if skip_scrollbar_check {
10956
        return false;
41888
    }
41888
    let Some(warm_node) = tree.warm(node_index) else {
        return false;
    };
41888
    match &warm_node.scrollbar_info {
27984
        None => scrollbar_info.needs_reflow(),
13904
        Some(old_info) => {
            // Trigger reflow if scrollbar state changed in either direction
13904
            let horizontal_changed = old_info.needs_horizontal != scrollbar_info.needs_horizontal;
13904
            let vertical_changed = old_info.needs_vertical != scrollbar_info.needs_vertical;
13904
            horizontal_changed || vertical_changed
        }
    }
52844
}
/// 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.
52844
fn merge_scrollbar_info(
52844
    _tree: &LayoutTree,
52844
    _node_index: usize,
52844
    new_info: &ScrollbarRequirements,
52844
) -> ScrollbarRequirements {
52844
    new_info.clone()
52844
}
/// 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.
80432
fn calculate_content_box_pos(
80432
    containing_block_pos: LogicalPosition,
80432
    box_props: &crate::solver3::geometry::BoxProps,
80432
) -> LogicalPosition {
80432
    LogicalPosition::new(
80432
        containing_block_pos.x + box_props.border.left + box_props.padding.left,
80432
        containing_block_pos.y + box_props.border.top + box_props.padding.top,
    )
80432
}
/// Emits debug logging for content-box calculation if debug messages are enabled.
42137
fn log_content_box_calculation<T: ParsedFontTrait>(
42137
    ctx: &mut LayoutContext<'_, T>,
42137
    node_index: usize,
42137
    current_node: &LayoutNodeHot,
42137
    containing_block_pos: LogicalPosition,
42137
    self_content_box_pos: LogicalPosition,
42137
) {
42137
    let Some(debug_msgs) = ctx.debug_messages.as_mut() else {
836
        return;
    };
41301
    let dom_name = current_node
41301
        .dom_node_id
41301
        .and_then(|id| {
41209
            ctx.styled_dom
41209
                .node_data
41209
                .as_container()
41209
                .internal
41209
                .get(id.index())
41209
        })
41301
        .map(|n| format!("{:?}", n.node_type))
41301
        .unwrap_or_else(|| "Unknown".to_string());
41301
    let cbp = current_node.box_props.unpack();
41301
    debug_msgs.push(LayoutDebugMessage::new(
41301
        LayoutDebugMessageType::PositionCalculation,
41301
        format!(
41301
            "[CONTENT BOX {}] {} - margin-box pos=({:.2}, {:.2}) + border=({:.2},{:.2}) + \
41301
             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
        ),
    ));
42137
}
/// Emits debug logging for child positioning if debug messages are enabled.
19332
fn log_child_positioning<T: ParsedFontTrait>(
19332
    ctx: &mut LayoutContext<'_, T>,
19332
    child_index: usize,
19332
    child_node: &LayoutNodeHot,
19332
    self_content_box_pos: LogicalPosition,
19332
    child_relative_pos: LogicalPosition,
19332
    child_absolute_pos: LogicalPosition,
19332
) {
    // Always print positioning info for debugging
19332
    let child_dom_name = child_node
19332
        .dom_node_id
19332
        .and_then(|id| {
19240
            ctx.styled_dom
19240
                .node_data
19240
                .as_container()
19240
                .internal
19240
                .get(id.index())
19240
        })
19332
        .map(|n| format!("{:?}", n.node_type))
19332
        .unwrap_or_else(|| "Unknown".to_string());
19332
    let Some(debug_msgs) = ctx.debug_messages.as_mut() else {
352
        return;
    };
18980
    debug_msgs.push(LayoutDebugMessage::new(
18980
        LayoutDebugMessageType::PositionCalculation,
18980
        format!(
18980
            "[CHILD POS {}] {} - parent content-box=({:.2}, {:.2}) + relative=({:.2}, {:.2}) + \
18980
             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,
18980
            child_node.box_props.unpack().margin.left,
18980
            child_node.box_props.unpack().margin.top,
            child_absolute_pos.x,
            child_absolute_pos.y
        ),
    ));
19332
}
/// 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.
19332
fn process_inflow_child<T: ParsedFontTrait>(
19332
    ctx: &mut LayoutContext<'_, T>,
19332
    tree: &mut LayoutTree,
19332
    text_cache: &mut TextLayoutCache,
19332
    child_index: usize,
19332
    child_relative_pos: LogicalPosition,
19332
    self_content_box_pos: LogicalPosition,
19332
    inner_size_after_scrollbars: LogicalSize,
19332
    writing_mode: LayoutWritingMode,
19332
    is_flex_or_grid: bool,
19332
    calculated_positions: &mut super::PositionVec,
19332
    reflow_needed_for_scrollbars: &mut bool,
19332
    float_cache: &mut HashMap<usize, fc::FloatingContext>,
19332
) -> Result<()> {
    // Set relative position on child
    // child_relative_pos is [CoordinateSpace::Parent] - relative to parent's content-box
19332
    let child_warm = tree.warm_mut(child_index).ok_or(LayoutError::InvalidTree)?;
19332
    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
19332
    let child_absolute_pos = LogicalPosition::new(
19332
        self_content_box_pos.x + child_relative_pos.x,
19332
        self_content_box_pos.y + child_relative_pos.y,
    );
    // Debug logging
    {
19332
        let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
19332
        log_child_positioning(
19332
            ctx,
19332
            child_index,
19332
            child_node,
19332
            self_content_box_pos,
19332
            child_relative_pos,
19332
            child_absolute_pos,
        );
    }
    // calculated_positions stores [CoordinateSpace::Window] - absolute positions
19332
    super::pos_set(calculated_positions, child_index, child_absolute_pos);
    // Get child's properties for recursion
19332
    let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
19332
    let child_bp = child_node.box_props.unpack();
19332
    let child_content_box_pos =
19332
        calculate_content_box_pos(child_absolute_pos, &child_bp);
19332
    let child_inner_size = child_bp
19332
        .inner_size(child_node.used_size.unwrap_or_default(), writing_mode);
19332
    let child_children: Vec<usize> = tree.children(child_index).to_vec();
19332
    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.
19332
    if !child_children.is_empty() {
14732
        if is_flex_or_grid {
            // For Flex/Grid: Taffy already set used_size. Only recurse for grandchildren.
889
            position_flex_child_descendants(
889
                ctx,
889
                tree,
889
                text_cache,
889
                child_index,
889
                child_content_box_pos,
889
                child_inner_size,
889
                calculated_positions,
889
                reflow_needed_for_scrollbars,
889
                float_cache,
            )?;
13843
        } else {
13843
            // For Block/Inline/Table: The formatting context already laid out children.
13843
            // Recursively position grandchildren using their cached layout data.
13843
            position_bfc_child_descendants(
13843
                tree,
13843
                child_index,
13843
                child_content_box_pos,
13843
                calculated_positions,
13843
            );
13843
        }
4600
    }
19332
    Ok(())
19332
}
/// 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.
58256
fn position_bfc_child_descendants(
58256
    tree: &LayoutTree,
58256
    node_index: usize,
58256
    content_box_pos: LogicalPosition,
58256
    calculated_positions: &mut super::PositionVec,
58256
) {
58256
    let Some(node) = tree.get(node_index) else { return };
58256
    for &child_index in tree.children(node_index) {
39468
        let Some(child_node) = tree.get(child_index) else { continue };
        // Use the relative_position that was set during formatting context layout
39468
        let child_rel_pos = tree.warm(child_index)
39468
            .and_then(|w| w.relative_position)
39468
            .unwrap_or_default();
39468
        let child_abs_pos = LogicalPosition::new(
39468
            content_box_pos.x + child_rel_pos.x,
39468
            content_box_pos.y + child_rel_pos.y,
        );
39468
        super::pos_set(calculated_positions, child_index, child_abs_pos);
        // Calculate child's content-box position for recursion
39468
        let cbp = child_node.box_props.unpack();
39468
        let child_content_box_pos = LogicalPosition::new(
39468
            child_abs_pos.x + cbp.border.left + cbp.padding.left,
39468
            child_abs_pos.y + cbp.border.top + cbp.padding.top,
        );
        // Recurse to grandchildren
39468
        position_bfc_child_descendants(tree, child_index, child_content_box_pos, calculated_positions);
    }
58256
}
/// 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.
42137
fn process_out_of_flow_children<T: ParsedFontTrait>(
42137
    ctx: &mut LayoutContext<'_, T>,
42137
    tree: &mut LayoutTree,
42137
    text_cache: &mut TextLayoutCache,
42137
    node_index: usize,
42137
    self_content_box_pos: LogicalPosition,
42137
    containing_block_size: LogicalSize,
42137
    calculated_positions: &mut super::PositionVec,
42137
    reflow_needed_for_scrollbars: &mut bool,
42137
    float_cache: &mut HashMap<usize, fc::FloatingContext>,
42137
) -> Result<()> {
    // Collect out-of-flow children (those not already positioned)
42137
    let out_of_flow_children: Vec<(usize, Option<NodeId>)> = {
42137
        let current_node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
42137
        tree.children(node_index)
42137
            .iter()
42137
            .filter_map(|&child_index| {
38813
                if super::pos_contains(calculated_positions, child_index) {
15592
                    return None;
23221
                }
23221
                let child = tree.get(child_index)?;
23221
                Some((child_index, child.dom_node_id))
38813
            })
42137
            .collect()
    };
65358
    for (child_index, child_dom_id_opt) in out_of_flow_children {
23221
        let Some(child_dom_id) = child_dom_id_opt else {
            continue;
        };
23221
        let position_type = get_position_type(ctx.styled_dom, Some(child_dom_id));
23221
        if position_type != LayoutPosition::Absolute && position_type != LayoutPosition::Fixed {
10856
            continue;
12365
        }
        // Set static position to parent's content-box origin
12365
        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.
12365
        calculate_layout_for_subtree(
12365
            ctx,
12365
            tree,
12365
            text_cache,
12365
            child_index,
12365
            self_content_box_pos,
12365
            containing_block_size,
12365
            calculated_positions,
12365
            reflow_needed_for_scrollbars,
12365
            float_cache,
12365
            ComputeMode::PerformLayout,
        )?;
    }
42137
    Ok(())
42137
}
/// 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.
42402
pub fn calculate_layout_for_subtree<T: ParsedFontTrait>(
42402
    ctx: &mut LayoutContext<'_, T>,
42402
    tree: &mut LayoutTree,
42402
    text_cache: &mut TextLayoutCache,
42402
    node_index: usize,
42402
    containing_block_pos: LogicalPosition,
42402
    containing_block_size: LogicalSize,
42402
    calculated_positions: &mut super::PositionVec,
42402
    reflow_needed_for_scrollbars: &mut bool,
42402
    float_cache: &mut HashMap<usize, fc::FloatingContext>,
42402
    compute_mode: ComputeMode,
42402
) -> Result<()> {
    // [g147b az-web-lift DIAG] per-node calculate_layout_for_subtree entry (0x60980+slot): records the
    // last compute_mode that reached this node (PerformLayout=2 wins, runs after ComputeSize=1). If a div
    // shows 0x...0002 here but its layout_formatting_context marker (0x609A0+) is UNSET → positioning
    // reached calculate but short-circuited (cache hit) before dispatching to the formatting context.
    #[cfg(feature = "web_lift")]
    unsafe {
        let m = match compute_mode { ComputeMode::PerformLayout => 0xC0DE0002u32, _ => 0xC0DE0001u32 };
        crate::az_mark(((0x60980 + (node_index & 7) * 4)) as u32, (m) as u32);
    }
42402
    let _probe = match compute_mode {
22762
        ComputeMode::ComputeSize => crate::probe::Probe::span("size_node"),
19640
        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
42402
    if node_index < ctx.cache_map.entries.len() {
42402
        match compute_mode {
            ComputeMode::ComputeSize => {
                // ComputeSize: check measurement slot first (Taffy's 9-slot scheme)
22762
                let sizing_hit = ctx.cache_map.entries[node_index]
22762
                    .get_size(0, containing_block_size)
22762
                    .cloned();
22762
                if let Some(cached_sizing) = sizing_hit {
                    // SIZING CACHE HIT — set used_size and return immediately.
                    // No child positioning needed in ComputeSize mode.
265
                    drop(crate::probe::Probe::span("size_cache_hit_sizing"));
265
                    if let Some(node) = tree.get_mut(node_index) {
265
                        node.used_size = Some(cached_sizing.result_size);
265
                    }
265
                    if let Some(warm) = tree.warm_mut(node_index) {
265
                        warm.escaped_top_margin = cached_sizing.escaped_top_margin;
265
                        warm.escaped_bottom_margin = cached_sizing.escaped_bottom_margin;
265
                        warm.baseline = cached_sizing.baseline;
265
                    }
265
                    return Ok(());
22497
                }
                // Fall through to layout slot check
22497
                let layout_hit = ctx.cache_map.entries[node_index]
22497
                    .get_layout(containing_block_size)
22497
                    .cloned();
22497
                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(());
22497
                }
                // [g147c az-web-lift DIAG] ComputeSize cache MISS for this node (0x60A60+slot): the
                // compute path WILL run → layout_formatting_context should fire. If a div is sized by
                // Pass-1 (0x60A40 set) but this miss-flag is UNSET → calculate(child,ComputeSize) hit
                // the cache instead (so layout_formatting_context/layout_ifc were skipped).
                #[cfg(feature = "web_lift")]
                unsafe { crate::az_mark(((0x60A60 + (node_index & 7) * 4)) as u32, (0xC0DE0001) as u32); }
22497
                drop(crate::probe::Probe::span("size_cache_miss"));
            }
            ComputeMode::PerformLayout => {
                // PerformLayout: check layout slot (the single "full layout" slot)
19640
                let layout_hit = ctx.cache_map.entries[node_index]
19640
                    .get_layout(containing_block_size)
19640
                    .cloned();
19640
                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(());
19640
                }
            }
        }
    }
    // === CACHE MISS — compute layout ===
42137
    if compute_mode == ComputeMode::PerformLayout {
19640
        drop(crate::probe::Probe::span("pos_cache_miss"));
22497
    }
    // 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 {
42137
        constraints,
42137
        dom_id,
42137
        writing_mode,
42137
        mut final_used_size,
42137
        box_props,
    } = {
42137
        let _p = crate::probe::Probe::span("prepare_layout_context");
42137
        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.
    {
42137
        let is_table_cell = tree.get(node_index).map_or(false, |n| {
42137
            matches!(n.formatting_context, FormattingContext::TableCell)
42137
        });
42137
        if !is_table_cell {
32105
            if let Some(node) = tree.get_mut(node_index) {
32105
                node.used_size = Some(final_used_size);
32105
            }
10032
        }
    }
    // Phase 2: Layout children using the formatting context
42137
    let layout_result = {
42137
        let _p = crate::probe::Probe::span("layout_formatting_context");
42137
        layout_formatting_context(ctx, tree, text_cache, node_index, &constraints, float_cache)?
    };
42137
    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.
42137
    if let Some(adjusted) = tree.get(node_index).and_then(|n| n.used_size) {
38397
        final_used_size = adjusted;
38397
    }
    // Phase 2.5: Resolve 'auto' main-axis size based on content
    // For anonymous boxes, use default styled node state
42137
    let styled_node_state = dom_id
42137
        .and_then(|id| ctx.styled_dom.styled_nodes.as_container().get(id).cloned())
42137
        .map(|n| n.styled_node_state)
42137
        .unwrap_or_default();
42137
    let css_height: MultiValue<LayoutHeight> = match dom_id {
42045
        Some(id) => get_css_height(ctx.styled_dom, id, &styled_node_state),
92
        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.
42137
    let is_scroll_container = dom_id.map_or(false, |id| {
42045
        let ov_x = get_overflow_x(ctx.styled_dom, id, &styled_node_state);
42045
        let ov_y = get_overflow_y(ctx.styled_dom, id, &styled_node_state);
42045
        matches!(ov_x, MultiValue::Exact(LayoutOverflow::Scroll) | MultiValue::Exact(LayoutOverflow::Auto))
42045
            || matches!(ov_y, MultiValue::Exact(LayoutOverflow::Scroll) | MultiValue::Exact(LayoutOverflow::Auto))
42045
    });
42137
    if should_use_content_height(&css_height) {
31810
        let skip_expansion = is_scroll_container
            && containing_block_size.height.is_finite()
            && containing_block_size.height > 0.0;
31810
        if !skip_expansion {
31810
            final_used_size = apply_content_based_height(
31810
                final_used_size,
31810
                content_size,
31810
                tree,
31810
                node_index,
31810
                writing_mode,
31810
            );
31810
        }
10327
    }
    // Phase 3: Scrollbar handling
    // Anonymous boxes don't have scrollbars
42137
    let skip_scrollbar_check = ctx.fragmentation_context.is_some();
42137
    let scrollbar_info = match dom_id {
42045
        Some(id) => compute_scrollbar_info(
42045
            ctx,
42045
            id,
42045
            &styled_node_state,
42045
            content_size,
42045
            &box_props,
42045
            final_used_size,
42045
            writing_mode,
        ),
92
        None => ScrollbarRequirements::default(),
    };
42137
    if check_scrollbar_change(tree, node_index, &scrollbar_info, skip_scrollbar_check) {
        *reflow_needed_for_scrollbars = true;
42137
    }
42137
    let merged_scrollbar_info = merge_scrollbar_info(tree, node_index, &scrollbar_info);
42137
    let content_box_size = box_props.inner_size(final_used_size, writing_mode);
42137
    let inner_size_after_scrollbars = merged_scrollbar_info.shrink_size(content_box_size);
    // Phase 4: Update this node's state
42137
    let self_content_box_pos = {
        {
42137
            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
42137
            let is_table_cell = matches!(
42137
                current_node.formatting_context,
                FormattingContext::TableCell
            );
42137
            if !is_table_cell || current_node.used_size.is_none() {
35845
                current_node.used_size = Some(final_used_size);
35845
            }
        }
        // Update warm fields
42137
        if let Some(warm) = tree.warm_mut(node_index) {
42137
            warm.scrollbar_info = Some(merged_scrollbar_info.clone());
42137
            // Store overflow content size for scroll frame calculation
42137
            // +spec:overflow:f28d6a - hanging glyphs should be ink overflow, not scrollable overflow (not yet subtracted from content_size)
42137
            warm.overflow_content_size = Some(content_size);
42137
        }
        // self_content_box_pos is [CoordinateSpace::Window] - the absolute position of this node's content-box
42137
        let current_node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
42137
        let current_bp = current_node.box_props.unpack();
42137
        let pos = calculate_content_box_pos(containing_block_pos, &current_bp);
42137
        log_content_box_calculation(ctx, node_index, current_node, containing_block_pos, pos);
42137
        pos
    };
    // Phase 5: Determine formatting context type
42137
    let is_flex_or_grid = {
42137
        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
41252
        matches!(
42137
            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
42137
    let positions: Vec<_> = layout_result
42137
        .output
42137
        .positions
42137
        .iter()
42137
        .map(|(&idx, &pos)| (idx, pos))
42137
        .collect();
    // Store child positions for cache
42137
    let child_positions_for_cache: Vec<(usize, LogicalPosition)> = positions.clone();
61469
    for (child_index, child_relative_pos) in positions {
19332
        process_inflow_child(
19332
            ctx,
19332
            tree,
19332
            text_cache,
19332
            child_index,
19332
            child_relative_pos,
19332
            self_content_box_pos,
19332
            inner_size_after_scrollbars,
19332
            writing_mode,
19332
            is_flex_or_grid,
19332
            calculated_positions,
19332
            reflow_needed_for_scrollbars,
19332
            float_cache,
        )?;
    }
    // Phase 7: Process out-of-flow children (absolute/fixed)
42137
    process_out_of_flow_children(
42137
        ctx,
42137
        tree,
42137
        text_cache,
42137
        node_index,
42137
        self_content_box_pos,
42137
        inner_size_after_scrollbars,
42137
        calculated_positions,
42137
        reflow_needed_for_scrollbars,
42137
        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.
42137
    if node_index < ctx.cache_map.entries.len() {
42137
        let warm_ref = tree.warm(node_index);
42137
        let baseline = warm_ref.and_then(|n| n.baseline);
42137
        let escaped_top = warm_ref.and_then(|n| n.escaped_top_margin);
42137
        let escaped_bottom = warm_ref.and_then(|n| n.escaped_bottom_margin);
        // Store in the layout slot (PerformLayout result)
42137
        ctx.cache_map.get_mut(node_index).store_layout(LayoutCacheEntry {
42137
            available_size: containing_block_size,
42137
            result_size: final_used_size,
42137
            content_size,
42137
            child_positions: child_positions_for_cache.clone(),
42137
            escaped_top_margin: escaped_top,
42137
            escaped_bottom_margin: escaped_bottom,
42137
            scrollbar_info: merged_scrollbar_info.clone(),
42137
        });
        // 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.
42137
        ctx.cache_map.get_mut(node_index).store_size(0, SizingCacheEntry {
42137
            available_size: containing_block_size,
42137
            result_size: final_used_size,
42137
            baseline,
42137
            escaped_top_margin: escaped_top,
42137
            escaped_bottom_margin: escaped_bottom,
42137
        });
    }
42137
    Ok(())
42402
}
/// 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.
8298
fn position_flex_child_descendants<T: ParsedFontTrait>(
8298
    ctx: &mut LayoutContext<'_, T>,
8298
    tree: &mut LayoutTree,
8298
    text_cache: &mut TextLayoutCache,
8298
    node_index: usize,
8298
    content_box_pos: LogicalPosition,
8298
    available_size: LogicalSize,
8298
    calculated_positions: &mut super::PositionVec,
8298
    reflow_needed_for_scrollbars: &mut bool,
8298
    float_cache: &mut HashMap<usize, fc::FloatingContext>,
8298
) -> Result<()> {
8298
    let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
8298
    let children: Vec<usize> = tree.children(node_index).to_vec();
8298
    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.
8298
    if matches!(fc, FormattingContext::Flex | FormattingContext::Grid) {
5870
        for &child_index in &children {
4410
            let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
4410
            let child_rel_pos = tree.warm(child_index)
4410
                .and_then(|w| w.relative_position)
4410
                .unwrap_or_default();
4410
            let child_abs_pos = LogicalPosition::new(
4410
                content_box_pos.x + child_rel_pos.x,
4410
                content_box_pos.y + child_rel_pos.y,
            );
            // Insert position
4410
            super::pos_set(calculated_positions, child_index, child_abs_pos);
            // Get child's content box for recursion
4410
            let cbp = child_node.box_props.unpack();
4410
            let child_content_box = LogicalPosition::new(
4410
                child_abs_pos.x
4410
                    + cbp.border.left
4410
                    + cbp.padding.left,
4410
                child_abs_pos.y
4410
                    + cbp.border.top
4410
                    + cbp.padding.top,
            );
4410
            let child_inner_size = cbp.inner_size(
4410
                child_node.used_size.unwrap_or_default(),
4410
                LayoutWritingMode::HorizontalTb,
            );
            // Recurse
4410
            position_flex_child_descendants(
4410
                ctx,
4410
                tree,
4410
                text_cache,
4410
                child_index,
4410
                child_content_box,
4410
                child_inner_size,
4410
                calculated_positions,
4410
                reflow_needed_for_scrollbars,
4410
                float_cache,
            )?;
        }
    } else {
        // For Block/Inline/Table children, their descendants need proper layout calculation
        // Use the output.positions from their own layout
6838
        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
6838
        let children: Vec<usize> = tree.children(node_index).to_vec();
9837
        for &child_index in &children {
2999
            let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
2999
            let child_rel_pos = tree.warm(child_index)
2999
                .and_then(|w| w.relative_position)
2999
                .unwrap_or_default();
2999
            let child_abs_pos = LogicalPosition::new(
2999
                content_box_pos.x + child_rel_pos.x,
2999
                content_box_pos.y + child_rel_pos.y,
            );
            // Insert position
2999
            super::pos_set(calculated_positions, child_index, child_abs_pos);
            // Get child's content box for recursion
2999
            let cbp = child_node.box_props.unpack();
2999
            let child_content_box = LogicalPosition::new(
2999
                child_abs_pos.x
2999
                    + cbp.border.left
2999
                    + cbp.padding.left,
2999
                child_abs_pos.y
2999
                    + cbp.border.top
2999
                    + cbp.padding.top,
            );
2999
            let child_inner_size = cbp.inner_size(
2999
                child_node.used_size.unwrap_or_default(),
2999
                LayoutWritingMode::HorizontalTb,
            );
            // Recurse
2999
            position_flex_child_descendants(
2999
                ctx,
2999
                tree,
2999
                text_cache,
2999
                child_index,
2999
                child_content_box,
2999
                child_inner_size,
2999
                calculated_positions,
2999
                reflow_needed_for_scrollbars,
2999
                float_cache,
            )?;
        }
    }
8298
    Ok(())
8298
}
/// Checks if the given CSS height value should use content-based sizing
52844
fn should_use_content_height(css_height: &MultiValue<LayoutHeight>) -> bool {
52844
    match css_height {
        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
            // Auto/Initial/Inherit height should use content-based sizing
39292
            true
        }
13552
        MultiValue::Exact(height) => match height {
            LayoutHeight::Auto => {
                // Auto height should use content-based sizing
                true
            }
13552
            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};
13552
                px == &PixelValue::zero()
13552
                    || (px.metric != SizeMetric::Px
1232
                        && 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
            }
        },
    }
52844
}
/// 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.
39292
fn apply_content_based_height(
39292
    mut used_size: LogicalSize,
39292
    content_size: LogicalSize,
39292
    tree: &LayoutTree,
39292
    node_index: usize,
39292
    writing_mode: LayoutWritingMode,
39292
) -> LogicalSize {
39292
    let node_props = tree.get(node_index).unwrap().box_props.unpack();
39292
    let main_axis_padding_border =
39292
        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
39292
    let old_main_size = used_size.main(writing_mode);
39292
    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
39292
    let final_main_size = old_main_size.max(new_main_size);
39292
    used_size = used_size.with_main(writing_mode, final_main_size);
39292
    used_size
39292
}
// hash_styled_node_data() removed — replaced by NodeDataFingerprint::compute()
44484
fn calculate_subtree_hash(node_self_hash: u64, child_hashes: &[u64]) -> SubtreeHash {
44484
    let mut hasher = DefaultHasher::new();
44484
    node_self_hash.hash(&mut hasher);
44484
    child_hashes.hash(&mut hasher);
44484
    SubtreeHash(hasher.finish())
44484
}
/// 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
7260
pub fn compute_counters(
7260
    styled_dom: &StyledDom,
7260
    tree: &LayoutTree,
7260
    counters: &mut HashMap<(usize, String), i32>,
7260
) {
    // Track counter stacks: counter_name -> Vec<value>
    // Each entry in the Vec represents a nested scope
7260
    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
7260
    let mut scope_stack: Vec<Vec<String>> = Vec::new();
7260
    compute_counters_recursive(
7260
        styled_dom,
7260
        tree,
7260
        tree.root,
7260
        counters,
7260
        &mut counter_stacks,
7260
        &mut scope_stack,
    );
7260
}
57728
fn compute_counters_recursive(
57728
    styled_dom: &StyledDom,
57728
    tree: &LayoutTree,
57728
    node_idx: usize,
57728
    counters: &mut HashMap<(usize, String), i32>,
57728
    counter_stacks: &mut std::collections::HashMap<String, Vec<i32>>,
57728
    scope_stack: &mut Vec<Vec<String>>,
57728
) {
57728
    let node = match tree.get(node_idx) {
57728
        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
57728
    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;
57728
    }
    // Only process real DOM nodes, not anonymous boxes
57728
    let dom_id = match node.dom_node_id {
57464
        Some(id) => id,
        None => {
            // For anonymous boxes, just recurse to children
264
            for &child_idx in tree.children(node_idx) {
264
                compute_counters_recursive(
264
                    styled_dom,
264
                    tree,
264
                    child_idx,
264
                    counters,
264
                    counter_stacks,
264
                    scope_stack,
264
                );
264
            }
264
            return;
        }
    };
57464
    let node_data = &styled_dom.node_data.as_container()[dom_id];
57464
    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
57464
    let cache = &styled_dom.css_property_cache.ptr;
    // Track which counters we reset at this level (for cleanup later)
57464
    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
57464
    let display = {
        use crate::solver3::getters::get_display_property;
57464
        get_display_property(styled_dom, Some(dom_id)).exact()
    };
57464
    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.
57464
    let has_counter_css = node_state.is_normal()
57464
        && cache.compact_cache.as_ref().map_or(true, |cc| cc.has_counter(dom_id.index()));
    // Process counter-reset (now properly typed)
57464
    let counter_reset = if has_counter_css {
        cache
            .get_counter_reset(node_data, &dom_id, node_state)
            .and_then(|v| v.get_property())
    } else {
57464
        None
    };
57464
    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);
        }
57464
    }
    // Process counter-increment (now properly typed)
57464
    let counter_inc = if has_counter_css {
        cache
            .get_counter_increment(node_data, &dom_id, node_state)
            .and_then(|v| v.get_property())
    } else {
57464
        None
    };
57464
    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;
            }
        }
57464
    }
    // CSS Lists §3: display: list-item automatically increments "list-item" counter
57464
    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;
            }
        }
57464
    }
    // Store the current counter values for this node
57464
    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
57464
    scope_stack.push(reset_counters_at_this_level.clone());
    // Recurse to children
57464
    for &child_idx in tree.children(node_idx) {
50204
        compute_counters_recursive(
50204
            styled_dom,
50204
            tree,
50204
            child_idx,
50204
            counters,
50204
            counter_stacks,
50204
            scope_stack,
50204
        );
50204
    }
    // Pop counter scopes that were created at this level
57464
    if let Some(reset_counters) = scope_stack.pop() {
57464
        for counter_name in reset_counters {
            if let Some(stack) = counter_stacks.get_mut(&counter_name) {
                stack.pop();
            }
        }
    }
57728
}