Skip to main content

azul_layout/solver3/
cache.rs

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
11//!    all(`calculate_layout_for_subtree` starting from the root). This correctly recalculates all
12//!    percentage-based sizes and repositions all elements according to the new viewport dimensions.
13//! 4. The intrinsic size calculation (bottom-up) can often be skipped, as it's independent of the
14//!    container size, which is a significant optimization.
15
16use std::{
17    collections::{BTreeMap, BTreeSet},
18    hash::{DefaultHasher, Hash, Hasher},
19};
20
21use azul_core::{
22    dom::{FormattingContext, NodeId, NodeType},
23    geom::{LogicalPosition, LogicalRect, LogicalSize},
24    styled_dom::{StyledDom, StyledNode},
25};
26use azul_css::{
27    css::CssPropertyValue,
28    props::{
29        layout::{
30            LayoutDisplay, LayoutFlexWrap, LayoutHeight, LayoutJustifyContent, LayoutOverflow,
31            LayoutPosition, LayoutWrap, LayoutWritingMode,
32        },
33        property::{CssProperty, CssPropertyType},
34        style::StyleTextAlign,
35    },
36    LayoutDebugMessage, LayoutDebugMessageType,
37};
38
39use crate::{
40    font_traits::{FontLoaderTrait, ParsedFontTrait, TextLayoutCache},
41    solver3::{
42        fc::{self, layout_formatting_context, LayoutConstraints, OverflowBehavior},
43        geometry::PositionedRectangle,
44        getters::{
45            get_css_height, get_display_property, get_justify_content, get_overflow_x,
46            get_overflow_y, get_text_align, get_white_space_property, get_wrap, get_writing_mode,
47            MultiValue,
48        },
49        layout_tree::{
50            is_block_level, AnonymousBoxType, LayoutNode, LayoutTreeBuilder, SubtreeHash,
51        },
52        positioning::get_position_type,
53        scrollbar::ScrollbarRequirements,
54        sizing::calculate_used_size_for_node,
55        LayoutContext, LayoutError, LayoutTree, Result,
56    },
57    text3::cache::AvailableSpace as Text3AvailableSpace,
58};
59
60// ============================================================================
61// Per-Node Multi-Slot Cache (inspired by Taffy's 9+1 slot cache architecture)
62//
63// Instead of a global BTreeMap keyed by (node_index, available_size), each node
64// gets its own deterministic cache with 9 measurement slots + 1 full layout slot.
65// This eliminates O(log n) lookups, prevents slot collisions between MinContent/
66// MaxContent/Definite measurements, and cleanly separates sizing from positioning.
67//
68// Reference: https://github.com/DioxusLabs/taffy — Cache struct in src/tree/cache.rs
69// Azul improvement: cache is EXTERNAL (Vec<NodeCache> parallel to LayoutTree.nodes)
70// rather than stored on the node, keeping LayoutNode slim and avoiding &mut tree
71// for cache operations.
72// ============================================================================
73
74/// Determines whether `calculate_layout_for_subtree` should only compute
75/// the node's size (for parent's sizing pass) or perform full layout
76/// including child positioning.
77///
78/// Inspired by Taffy's `RunMode` enum. The two-mode approach enables the
79/// classic CSS two-pass layout: Pass 1 (ComputeSize) measures all children,
80/// Pass 2 (PerformLayout) positions them using the measured sizes.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub enum ComputeMode {
83    /// Only compute the node's border-box size and baseline.
84    /// Does NOT store child positions. Used in BFC Pass 1 (sizing).
85    ComputeSize,
86    /// Compute size AND position all children.
87    /// Stores the full layout result including child positions.
88    /// Used in BFC Pass 2 (positioning) and as the final layout step.
89    PerformLayout,
90}
91
92/// Constraint classification for deterministic cache slot selection.
93///
94/// Inspired by Taffy's `AvailableSpace` enum. Each constraint type maps to a
95/// different cache slot, preventing collisions between e.g. MinContent and
96/// Definite measurements of the same node.
97#[derive(Debug, Clone, Copy, PartialEq, Eq)]
98pub enum AvailableWidthType {
99    /// A definite pixel value (or percentage resolved to pixels).
100    Definite,
101    /// Shrink-to-fit: the smallest size that doesn't cause overflow.
102    MinContent,
103    /// Use all available space: the largest size the content can use.
104    MaxContent,
105}
106
107/// Cache entry for sizing (ComputeSize mode) — stores NO positions.
108///
109/// This is the lightweight entry stored in the 9 measurement slots.
110/// It records what constraints were provided and what size resulted,
111/// enabling Taffy's "result matches request" optimization.
112#[derive(Debug, Clone)]
113pub struct SizingCacheEntry {
114    /// The available size that was provided as input.
115    pub available_size: LogicalSize,
116    /// The computed border-box size (output).
117    pub result_size: LogicalSize,
118    /// Baseline for inline alignment (if applicable).
119    pub baseline: Option<f32>,
120    /// First child's escaped top margin (CSS 2.2 § 8.3.1).
121    pub escaped_top_margin: Option<f32>,
122    /// Last child's escaped bottom margin (CSS 2.2 § 8.3.1).
123    pub escaped_bottom_margin: Option<f32>,
124}
125
126/// Cache entry for full layout (PerformLayout mode).
127///
128/// This is the single "final layout" slot. It includes child positions
129/// (relative to parent's content-box) and overflow/scrollbar info.
130#[derive(Debug, Clone)]
131pub struct LayoutCacheEntry {
132    /// The available size that was provided as input.
133    pub available_size: LogicalSize,
134    /// The computed border-box size (output).
135    pub result_size: LogicalSize,
136    /// Content overflow size (for scrolling).
137    pub content_size: LogicalSize,
138    /// Child positions relative to parent's content-box (NOT absolute).
139    pub child_positions: Vec<(usize, LogicalPosition)>,
140    /// First child's escaped top margin.
141    pub escaped_top_margin: Option<f32>,
142    /// Last child's escaped bottom margin.
143    pub escaped_bottom_margin: Option<f32>,
144    /// Scrollbar requirements for this node.
145    pub scrollbar_info: ScrollbarRequirements,
146}
147
148/// Per-node cache entry with 9 measurement slots + 1 full layout slot.
149///
150/// Inspired by Taffy's `Cache` struct (9+1 slots per node). The deterministic
151/// slot index is computed from the constraint combination, so entries never
152/// clobber each other (unlike the old global BTreeMap where fixed-point
153/// collisions were possible).
154///
155/// NOT stored on LayoutNode — lives in the external `LayoutCacheMap`.
156#[derive(Debug, Clone)]
157pub struct NodeCache {
158    /// 9 measurement slots (Taffy's deterministic scheme):
159    /// - Slot 0: both dimensions known
160    /// - Slots 1-2: only width known (MaxContent/Definite vs MinContent)
161    /// - Slots 3-4: only height known (MaxContent/Definite vs MinContent)
162    /// - Slots 5-8: neither known (2×2 combos of width/height constraint types)
163    pub measure_entries: [Option<SizingCacheEntry>; 9],
164
165    /// 1 full layout slot (with child positions, overflow, baseline).
166    /// Only populated after PerformLayout, not after ComputeSize.
167    pub layout_entry: Option<LayoutCacheEntry>,
168
169    /// Fast check for dirty propagation (Taffy optimization).
170    /// When true, all slots are empty — ancestors are also dirty.
171    pub is_empty: bool,
172}
173
174impl Default for NodeCache {
175    fn default() -> Self {
176        Self {
177            measure_entries: [None, None, None, None, None, None, None, None, None],
178            layout_entry: None,
179            is_empty: true, // fresh cache is empty/dirty
180        }
181    }
182}
183
184impl NodeCache {
185    /// Clear all cache entries, marking this node as dirty.
186    pub fn clear(&mut self) {
187        self.measure_entries = [None, None, None, None, None, None, None, None, None];
188        self.layout_entry = None;
189        self.is_empty = true;
190    }
191
192    /// Compute the deterministic slot index from constraint dimensions.
193    ///
194    /// This is Taffy's slot selection scheme: given whether width/height are
195    /// "known" (definite constraint provided by parent) and what type of
196    /// constraint applies to the unknown dimension(s), we get a unique slot 0–8.
197    pub fn slot_index(
198        width_known: bool,
199        height_known: bool,
200        width_type: AvailableWidthType,
201        height_type: AvailableWidthType,
202    ) -> usize {
203        match (width_known, height_known) {
204            (true, true) => 0,
205            (true, false) => {
206                if width_type == AvailableWidthType::MinContent { 2 } else { 1 }
207            }
208            (false, true) => {
209                if height_type == AvailableWidthType::MinContent { 4 } else { 3 }
210            }
211            (false, false) => {
212                let w = if width_type == AvailableWidthType::MinContent { 1 } else { 0 };
213                let h = if height_type == AvailableWidthType::MinContent { 1 } else { 0 };
214                5 + w * 2 + h
215            }
216        }
217    }
218
219    /// Look up a sizing cache entry, implementing Taffy's "result matches request"
220    /// optimization: if the caller provides the result size as a known dimension
221    /// (common in Pass1→Pass2 transitions), it's still a cache hit.
222    pub fn get_size(&self, slot: usize, known_dims: LogicalSize) -> Option<&SizingCacheEntry> {
223        let entry = self.measure_entries[slot].as_ref()?;
224        // Exact match on input constraints
225        if (known_dims.width - entry.available_size.width).abs() < 0.1
226            && (known_dims.height - entry.available_size.height).abs() < 0.1
227        {
228            return Some(entry);
229        }
230        // "Result matches request" — if the caller provides the result size
231        // as a known dimension, it's still a hit. This is the key optimization
232        // that makes two-pass layout O(n): Pass 1 measures a node, Pass 2
233        // provides the measured size as a constraint → automatic cache hit.
234        if (known_dims.width - entry.result_size.width).abs() < 0.1
235            && (known_dims.height - entry.result_size.height).abs() < 0.1
236        {
237            return Some(entry);
238        }
239        None
240    }
241
242    /// Store a sizing result in the given slot.
243    pub fn store_size(&mut self, slot: usize, entry: SizingCacheEntry) {
244        self.measure_entries[slot] = Some(entry);
245        self.is_empty = false;
246    }
247
248    /// Look up the full layout cache entry.
249    pub fn get_layout(&self, known_dims: LogicalSize) -> Option<&LayoutCacheEntry> {
250        let entry = self.layout_entry.as_ref()?;
251        if (known_dims.width - entry.available_size.width).abs() < 0.1
252            && (known_dims.height - entry.available_size.height).abs() < 0.1
253        {
254            return Some(entry);
255        }
256        // "Result matches request" for layout too
257        if (known_dims.width - entry.result_size.width).abs() < 0.1
258            && (known_dims.height - entry.result_size.height).abs() < 0.1
259        {
260            return Some(entry);
261        }
262        None
263    }
264
265    /// Store a full layout result.
266    pub fn store_layout(&mut self, entry: LayoutCacheEntry) {
267        self.layout_entry = Some(entry);
268        self.is_empty = false;
269    }
270}
271
272/// External layout cache, parallel to `LayoutTree.nodes`.
273///
274/// `cache_map.entries[i]` holds the cache for `LayoutTree.nodes[i]`.
275/// Stored on `LayoutCache` (persists across frames).
276///
277/// This is Azul's improvement over Taffy's on-node cache:
278/// - `LayoutNode` stays slim (0 bytes overhead)
279/// - No `&mut tree` needed to read/write cache entries
280/// - Cache can be resized independently after reconciliation
281/// - O(1) indexed lookup (Vec) instead of O(log n) (BTreeMap)
282#[derive(Debug, Clone, Default)]
283pub struct LayoutCacheMap {
284    pub entries: Vec<NodeCache>,
285}
286
287impl LayoutCacheMap {
288    /// Resize to match tree length after reconciliation.
289    /// New nodes get empty (dirty) caches. Removed nodes' caches are dropped.
290    pub fn resize_to_tree(&mut self, tree_len: usize) {
291        self.entries.resize_with(tree_len, NodeCache::default);
292    }
293
294    /// O(1) lookup by layout tree index.
295    #[inline]
296    pub fn get(&self, node_index: usize) -> &NodeCache {
297        &self.entries[node_index]
298    }
299
300    /// O(1) mutable lookup by layout tree index.
301    #[inline]
302    pub fn get_mut(&mut self, node_index: usize) -> &mut NodeCache {
303        &mut self.entries[node_index]
304    }
305
306    /// Invalidate a node and propagate dirty flags upward through ancestors.
307    ///
308    /// Implements Taffy's early-stop optimization: propagation halts at the
309    /// first ancestor whose cache is already empty (i.e., already dirty).
310    /// This prevents redundant O(depth) propagation when multiple children
311    /// of the same parent are dirtied.
312    pub fn mark_dirty(&mut self, node_index: usize, tree: &[LayoutNode]) {
313        if node_index >= self.entries.len() {
314            return;
315        }
316        let cache = &mut self.entries[node_index];
317        if cache.is_empty {
318            return; // Already dirty → ancestors are too
319        }
320        cache.clear();
321
322        // Propagate upward (Taffy's early-stop optimization)
323        let mut current = tree.get(node_index).and_then(|n| n.parent);
324        while let Some(parent_idx) = current {
325            if parent_idx >= self.entries.len() {
326                break;
327            }
328            let parent_cache = &mut self.entries[parent_idx];
329            if parent_cache.is_empty {
330                break; // Stop early — ancestor already dirty
331            }
332            parent_cache.clear();
333            current = tree.get(parent_idx).and_then(|n| n.parent);
334        }
335    }
336}
337
338/// The persistent cache that holds the layout state between frames.
339#[derive(Debug, Clone, Default)]
340pub struct LayoutCache {
341    /// The fully laid-out tree from the previous frame. This is our primary cache.
342    pub tree: Option<LayoutTree>,
343    /// The final, absolute positions of all nodes from the previous frame.
344    pub calculated_positions: super::PositionVec,
345    /// The viewport size from the last layout pass, used to detect resizes.
346    pub viewport: Option<LogicalRect>,
347    /// Stable scroll IDs computed from node_data_hash (layout index -> scroll ID)
348    pub scroll_ids: BTreeMap<usize, u64>,
349    /// Mapping from scroll ID to DOM NodeId for hit testing
350    pub scroll_id_to_node_id: BTreeMap<u64, NodeId>,
351    /// CSS counter values for each node and counter name.
352    /// Key: (layout_index, counter_name), Value: counter value
353    /// This stores the computed counter values after processing counter-reset and
354    /// counter-increment.
355    pub counters: BTreeMap<(usize, String), i32>,
356    /// Cache of positioned floats for each BFC node (layout_index -> FloatingContext).
357    /// This persists float positions across multiple layout passes, ensuring IFC
358    /// children always have access to correct float exclusions even when layout is
359    /// recalculated.
360    pub float_cache: BTreeMap<usize, fc::FloatingContext>,
361    /// Per-node multi-slot cache (inspired by Taffy's 9+1 architecture).
362    /// External to LayoutTree — indexed by node index for O(1) lookup.
363    /// Persists across frames; resized after reconciliation.
364    pub cache_map: LayoutCacheMap,
365}
366
367/// The result of a reconciliation pass.
368#[derive(Debug, Default)]
369pub struct ReconciliationResult {
370    /// Set of nodes whose intrinsic size needs to be recalculated (bottom-up pass).
371    pub intrinsic_dirty: BTreeSet<usize>,
372    /// Set of layout roots whose subtrees need a new top-down layout pass.
373    pub layout_roots: BTreeSet<usize>,
374}
375
376impl ReconciliationResult {
377    /// Checks if any layout or paint work is needed.
378    pub fn is_clean(&self) -> bool {
379        self.intrinsic_dirty.is_empty() && self.layout_roots.is_empty()
380    }
381}
382
383/// After dirty subtrees are laid out, this repositions their clean siblings
384/// without recalculating their internal layout. This is a critical optimization.
385///
386/// This function acts as a dispatcher, inspecting the parent's formatting context
387/// and calling the appropriate repositioning algorithm. For complex layout modes
388/// like Flexbox or Grid, this optimization is skipped, as a full relayout is
389/// often required to correctly recalculate spacing and sizing for all siblings.
390pub fn reposition_clean_subtrees(
391    styled_dom: &StyledDom,
392    tree: &LayoutTree,
393    layout_roots: &BTreeSet<usize>,
394    calculated_positions: &mut super::PositionVec,
395) {
396    // Find the unique parents of all dirty layout roots. These are the containers
397    // where sibling positions need to be adjusted.
398    let mut parents_to_reposition = BTreeSet::new();
399    for &root_idx in layout_roots {
400        if let Some(parent_idx) = tree.get(root_idx).and_then(|n| n.parent) {
401            parents_to_reposition.insert(parent_idx);
402        }
403    }
404
405    for parent_idx in parents_to_reposition {
406        let parent_node = match tree.get(parent_idx) {
407            Some(n) => n,
408            None => continue,
409        };
410
411        // Dispatch to the correct repositioning logic based on the parent's layout mode.
412        match parent_node.formatting_context {
413            // Cases that use simple block-flow stacking can be optimized.
414            FormattingContext::Block { .. } | FormattingContext::TableRowGroup => {
415                reposition_block_flow_siblings(
416                    styled_dom,
417                    parent_idx,
418                    parent_node,
419                    tree,
420                    layout_roots,
421                    calculated_positions,
422                );
423            }
424
425            FormattingContext::Flex | FormattingContext::Grid => {
426                // Taffy handles this, so if a child is dirty, the parent would have
427                // already been marked as a layout_root and re-laid out by Taffy.
428                // We do nothing here for Flex or Grid.
429            }
430
431            FormattingContext::Table | FormattingContext::TableRow => {
432                // STUB: Table layout is interdependent. A change in one cell's size
433                // can affect the entire column's width or row's height, requiring a
434                // full relayout of the table. This optimization is skipped.
435            }
436
437            // Other contexts either don't contain children in a way that this
438            // optimization applies (e.g., Inline, TableCell) or are handled by other
439            // layout mechanisms (e.g., OutOfFlow).
440            _ => { /* Do nothing */ }
441        }
442    }
443}
444
445/// Convert LayoutOverflow to OverflowBehavior
446/// CSS Overflow Module Level 3: initial value of `overflow` is `visible`.
447pub fn to_overflow_behavior(overflow: MultiValue<LayoutOverflow>) -> fc::OverflowBehavior {
448    match overflow.unwrap_or(LayoutOverflow::Visible) {
449        LayoutOverflow::Visible => fc::OverflowBehavior::Visible,
450        LayoutOverflow::Hidden | LayoutOverflow::Clip => fc::OverflowBehavior::Hidden,
451        LayoutOverflow::Scroll => fc::OverflowBehavior::Scroll,
452        LayoutOverflow::Auto => fc::OverflowBehavior::Auto,
453    }
454}
455
456/// Convert StyleTextAlign to fc::TextAlign
457pub const fn style_text_align_to_fc(text_align: StyleTextAlign) -> fc::TextAlign {
458    match text_align {
459        StyleTextAlign::Start | StyleTextAlign::Left => fc::TextAlign::Start,
460        StyleTextAlign::End | StyleTextAlign::Right => fc::TextAlign::End,
461        StyleTextAlign::Center => fc::TextAlign::Center,
462        StyleTextAlign::Justify => fc::TextAlign::Justify,
463    }
464}
465
466/// Collects DOM child IDs from the node hierarchy into a Vec.
467///
468/// This is a helper function that flattens the sibling iteration into a simple loop.
469pub fn collect_children_dom_ids(styled_dom: &StyledDom, parent_dom_id: NodeId) -> Vec<NodeId> {
470    let hierarchy_container = styled_dom.node_hierarchy.as_container();
471    let mut children = Vec::new();
472
473    let Some(hierarchy_item) = hierarchy_container.get(parent_dom_id) else {
474        return children;
475    };
476
477    let Some(mut child_id) = hierarchy_item.first_child_id(parent_dom_id) else {
478        return children;
479    };
480
481    children.push(child_id);
482    while let Some(hierarchy_item) = hierarchy_container.get(child_id) {
483        let Some(next) = hierarchy_item.next_sibling_id() else {
484            break;
485        };
486        children.push(next);
487        child_id = next;
488    }
489
490    children
491}
492
493/// Checks if a flex container is simple enough to be treated like a block-stack for
494/// repositioning.
495pub fn is_simple_flex_stack(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> bool {
496    let Some(id) = dom_id else { return false };
497    let binding = styled_dom.styled_nodes.as_container();
498    let styled_node = match binding.get(id) {
499        Some(styled_node) => styled_node,
500        None => return false,
501    };
502
503    // Must be a single-line flex container
504    let wrap = get_wrap(styled_dom, id, &styled_node.styled_node_state);
505
506    if wrap.unwrap_or_default() != LayoutFlexWrap::NoWrap {
507        return false;
508    }
509
510    // Must be start-aligned, so there's no space distribution to recalculate.
511    let justify = get_justify_content(styled_dom, id, &styled_node.styled_node_state);
512
513    if !matches!(
514        justify.unwrap_or_default(),
515        LayoutJustifyContent::FlexStart | LayoutJustifyContent::Start
516    ) {
517        return false;
518    }
519
520    // Crucially, no clean siblings can have flexible sizes, otherwise a dirty
521    // sibling's size change could affect their resolved size.
522    // NOTE: This check is expensive and incomplete. A more robust solution might
523    // store flags on the LayoutNode indicating if flex factors are present.
524    // For now, we assume that if a container *could* have complex flex behavior,
525    // we play it safe and require a full relayout. This heuristic is a compromise.
526    // To be truly safe, we'd have to check all children for flex-grow/shrink > 0.
527
528    true
529}
530
531/// Repositions clean children within a simple block-flow layout (like a BFC or a
532/// table-row-group). It stacks children along the main axis, preserving their
533/// previously calculated cross-axis alignment.
534pub fn reposition_block_flow_siblings(
535    styled_dom: &StyledDom,
536    parent_idx: usize,
537    parent_node: &LayoutNode,
538    tree: &LayoutTree,
539    layout_roots: &BTreeSet<usize>,
540    calculated_positions: &mut super::PositionVec,
541) {
542    let dom_id = parent_node.dom_node_id.unwrap_or(NodeId::ZERO);
543    let styled_node_state = styled_dom
544        .styled_nodes
545        .as_container()
546        .get(dom_id)
547        .map(|n| n.styled_node_state.clone())
548        .unwrap_or_default();
549
550    let writing_mode = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
551
552    let parent_pos = calculated_positions
553        .get(parent_idx)
554        .copied()
555        .unwrap_or_default();
556
557    let content_box_origin = LogicalPosition::new(
558        parent_pos.x + parent_node.box_props.padding.left,
559        parent_pos.y + parent_node.box_props.padding.top,
560    );
561
562    let mut main_pen = 0.0;
563
564    for &child_idx in &parent_node.children {
565        let child_node = match tree.get(child_idx) {
566            Some(n) => n,
567            None => continue,
568        };
569
570        let child_size = child_node.used_size.unwrap_or_default();
571        let child_main_sum = child_node.box_props.margin.main_sum(writing_mode);
572        let margin_box_main_size = child_size.main(writing_mode) + child_main_sum;
573
574        if layout_roots.contains(&child_idx) {
575            // This child was DIRTY and has been correctly repositioned.
576            // Update the pen to the position immediately after this child.
577            let new_pos = match calculated_positions.get(child_idx) {
578                Some(p) => *p,
579                None => continue,
580            };
581
582            let main_axis_offset = if writing_mode.is_vertical() {
583                new_pos.x - content_box_origin.x
584            } else {
585                new_pos.y - content_box_origin.y
586            };
587
588            main_pen = main_axis_offset
589                + child_size.main(writing_mode)
590                + child_node.box_props.margin.main_end(writing_mode);
591        } else {
592            // This child is *clean*. Calculate its new position and shift its
593            // entire subtree.
594            let old_pos = match calculated_positions.get(child_idx) {
595                Some(p) => *p,
596                None => continue,
597            };
598
599            let child_main_start = child_node.box_props.margin.main_start(writing_mode);
600            let new_main_pos = main_pen + child_main_start;
601            let old_relative_pos = child_node.relative_position.unwrap_or_default();
602            let cross_pos = if writing_mode.is_vertical() {
603                old_relative_pos.y
604            } else {
605                old_relative_pos.x
606            };
607            let new_relative_pos =
608                LogicalPosition::from_main_cross(new_main_pos, cross_pos, writing_mode);
609
610            let new_absolute_pos = LogicalPosition::new(
611                content_box_origin.x + new_relative_pos.x,
612                content_box_origin.y + new_relative_pos.y,
613            );
614
615            if old_pos != new_absolute_pos {
616                let delta = LogicalPosition::new(
617                    new_absolute_pos.x - old_pos.x,
618                    new_absolute_pos.y - old_pos.y,
619                );
620                shift_subtree_position(child_idx, delta, tree, calculated_positions);
621            }
622
623            main_pen += margin_box_main_size;
624        }
625    }
626}
627
628/// Helper to recursively shift the absolute position of a node and all its descendants.
629pub fn shift_subtree_position(
630    node_idx: usize,
631    delta: LogicalPosition,
632    tree: &LayoutTree,
633    calculated_positions: &mut super::PositionVec,
634) {
635    if let Some(pos) = calculated_positions.get_mut(node_idx) {
636        pos.x += delta.x;
637        pos.y += delta.y;
638    }
639
640    if let Some(node) = tree.get(node_idx) {
641        for &child_idx in &node.children {
642            shift_subtree_position(child_idx, delta, tree, calculated_positions);
643        }
644    }
645}
646
647/// Compares the new DOM against the cached tree, creating a new tree
648/// and identifying which parts need to be re-laid out.
649pub fn reconcile_and_invalidate<T: ParsedFontTrait>(
650    ctx: &mut LayoutContext<'_, T>,
651    cache: &LayoutCache,
652    viewport: LogicalRect,
653) -> Result<(LayoutTree, ReconciliationResult)> {
654    let mut new_tree_builder = LayoutTreeBuilder::new(ctx.viewport_size);
655    let mut recon_result = ReconciliationResult::default();
656    let old_tree = cache.tree.as_ref();
657
658    // Check for viewport resize, which dirties the root for a top-down pass.
659    if cache.viewport.map_or(true, |v| v.size != viewport.size) {
660        recon_result.layout_roots.insert(0); // Root is always index 0
661    }
662
663    let root_dom_id = ctx
664        .styled_dom
665        .root
666        .into_crate_internal()
667        .unwrap_or(NodeId::ZERO);
668    let root_idx = reconcile_recursive(
669        ctx.styled_dom,
670        root_dom_id,
671        old_tree.map(|t| t.root),
672        None,
673        old_tree,
674        &mut new_tree_builder,
675        &mut recon_result,
676        &mut ctx.debug_messages,
677    )?;
678
679    // Clean up layout roots: if a parent is a layout root, its children don't need to be.
680    let final_layout_roots = recon_result
681        .layout_roots
682        .iter()
683        .filter(|&&idx| {
684            let mut current = new_tree_builder.get(idx).and_then(|n| n.parent);
685            while let Some(p_idx) = current {
686                if recon_result.layout_roots.contains(&p_idx) {
687                    return false;
688                }
689                current = new_tree_builder.get(p_idx).and_then(|n| n.parent);
690            }
691            true
692        })
693        .copied()
694        .collect();
695    recon_result.layout_roots = final_layout_roots;
696
697    let new_tree = new_tree_builder.build(root_idx);
698    Ok((new_tree, recon_result))
699}
700
701/// CSS 2.2 § 9.2.2.1: Checks whether an inline run consists entirely of
702/// whitespace-only text nodes, in which case it should NOT generate an
703/// anonymous IFC wrapper in a BFC mixed-content context.
704///
705/// This prevents whitespace between block elements from creating empty
706/// anonymous blocks that take up vertical space (regression c33e94b0).
707///
708/// Exception: if the parent (or any ancestor) has `white-space: pre`,
709/// `pre-wrap`, or `pre-line`, whitespace IS significant and the wrapper
710/// must still be created.
711fn is_whitespace_only_inline_run(
712    styled_dom: &StyledDom,
713    inline_run: &[(usize, NodeId)],
714    parent_dom_id: NodeId,
715) -> bool {
716    use azul_css::props::style::text::StyleWhiteSpace;
717
718    if inline_run.is_empty() {
719        return true;
720    }
721
722    // Check if the parent preserves whitespace
723    let parent_state = &styled_dom.styled_nodes.as_container()[parent_dom_id].styled_node_state;
724    let white_space = match get_white_space_property(styled_dom, parent_dom_id, parent_state) {
725        MultiValue::Exact(ws) => Some(ws),
726        _ => None,
727    };
728
729    // If white-space preserves whitespace, don't strip
730    if matches!(
731        white_space,
732        Some(StyleWhiteSpace::Pre) | Some(StyleWhiteSpace::PreWrap) | Some(StyleWhiteSpace::PreLine)
733    ) {
734        return false;
735    }
736
737    // Check that every node in the run is a whitespace-only text node
738    let binding = styled_dom.node_data.as_container();
739    for &(_, dom_id) in inline_run {
740        if let Some(data) = binding.get(dom_id) {
741            match data.get_node_type() {
742                NodeType::Text(text) => {
743                    let s = text.as_str();
744                    if !s.chars().all(|c| c.is_whitespace()) {
745                        return false; // Non-whitespace text → must create wrapper
746                    }
747                }
748                _ => {
749                    return false; // Non-text inline element → must create wrapper
750                }
751            }
752        }
753    }
754
755    true // All nodes are whitespace-only text
756}
757
758/// Recursively traverses the new DOM and old tree, building a new tree and marking dirty nodes.
759pub fn reconcile_recursive(
760    styled_dom: &StyledDom,
761    new_dom_id: NodeId,
762    old_tree_idx: Option<usize>,
763    new_parent_idx: Option<usize>,
764    old_tree: Option<&LayoutTree>,
765    new_tree_builder: &mut LayoutTreeBuilder,
766    recon: &mut ReconciliationResult,
767    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
768) -> Result<usize> {
769    let node_data = &styled_dom.node_data.as_container()[new_dom_id];
770
771    let old_node = old_tree.and_then(|t| old_tree_idx.and_then(|idx| t.get(idx)));
772    let new_node_data_hash = hash_styled_node_data(styled_dom, new_dom_id);
773
774    // A node is dirty if it's new, or if its data/style hash has changed.
775
776    let is_dirty = old_node.map_or(true, |n| new_node_data_hash != n.node_data_hash);
777
778    let new_node_idx = if is_dirty {
779        new_tree_builder.create_node_from_dom(
780            styled_dom,
781            new_dom_id,
782            new_parent_idx,
783            debug_messages,
784        )?
785    } else {
786        new_tree_builder.clone_node_from_old(old_node.unwrap(), new_parent_idx)
787    };
788
789    // CRITICAL: For list-items, create a ::marker pseudo-element as the first child
790    // This must be done after the node is created but before processing children
791    // Per CSS Lists Module Level 3, ::marker is generated as the first child of list-items
792    {
793        use crate::solver3::getters::get_display_property;
794        let display = get_display_property(styled_dom, Some(new_dom_id))
795            .exact();
796
797        if matches!(display, Some(LayoutDisplay::ListItem)) {
798            // Create ::marker pseudo-element for this list-item
799            new_tree_builder.create_marker_pseudo_element(styled_dom, new_dom_id, new_node_idx);
800        }
801    }
802
803    // Reconcile children to check for structural changes and build the new tree structure.
804    let new_children_dom_ids: Vec<_> = collect_children_dom_ids(styled_dom, new_dom_id);
805    let old_children_indices: Vec<_> = old_node.map(|n| n.children.clone()).unwrap_or_default();
806
807    let mut children_are_different = new_children_dom_ids.len() != old_children_indices.len();
808    let mut new_child_hashes = Vec::new();
809
810    // CSS 2.2 Section 9.2.1.1: Anonymous Block Boxes
811    // "When an inline box contains an in-flow block-level box, the inline box
812    // (and its inline ancestors within the same line box) are broken around
813    // the block-level box [...], splitting the inline box into two boxes"
814    //
815    // When a block container has mixed block/inline children, we must:
816    // 1. Wrap consecutive inline children in anonymous block boxes
817    // 2. Leave block-level children as direct children
818
819    let has_block_child = new_children_dom_ids
820        .iter()
821        .any(|&id| is_block_level(styled_dom, id));
822
823    if !has_block_child {
824        // All children are inline - no anonymous boxes needed
825        // Simple case: process each child directly
826        for (i, &new_child_dom_id) in new_children_dom_ids.iter().enumerate() {
827            let old_child_idx = old_children_indices.get(i).copied();
828
829            let reconciled_child_idx = reconcile_recursive(
830                styled_dom,
831                new_child_dom_id,
832                old_child_idx,
833                Some(new_node_idx),
834                old_tree,
835                new_tree_builder,
836                recon,
837                debug_messages,
838            )?;
839            if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
840                new_child_hashes.push(child_node.subtree_hash.0);
841            }
842
843            if old_tree.and_then(|t| t.get(old_child_idx?).map(|n| n.subtree_hash))
844                != new_tree_builder
845                    .get(reconciled_child_idx)
846                    .map(|n| n.subtree_hash)
847            {
848                children_are_different = true;
849            }
850        }
851    } else {
852        // Mixed content: block and inline children
853        // We must create anonymous block boxes around consecutive inline runs
854
855        if let Some(msgs) = debug_messages.as_mut() {
856            msgs.push(LayoutDebugMessage::info(format!(
857                "[reconcile_recursive] Mixed content in node {}: creating anonymous IFC wrappers",
858                new_dom_id.index()
859            )));
860        }
861
862        let mut inline_run: Vec<(usize, NodeId)> = Vec::new(); // (dom_child_index, dom_id)
863
864        for (i, &new_child_dom_id) in new_children_dom_ids.iter().enumerate() {
865            if is_block_level(styled_dom, new_child_dom_id) {
866                // End current inline run if any
867                if !inline_run.is_empty() {
868                    // CSS 2.2 § 9.2.2.1: If the inline run consists entirely of
869                    // whitespace-only text nodes (and white-space doesn't preserve it),
870                    // skip creating the anonymous IFC wrapper. This prevents inter-block
871                    // whitespace from creating empty blocks that take up vertical space.
872                    if is_whitespace_only_inline_run(styled_dom, &inline_run, new_dom_id) {
873                        if let Some(msgs) = debug_messages.as_mut() {
874                            msgs.push(LayoutDebugMessage::info(format!(
875                                "[reconcile_recursive] Skipping whitespace-only inline run ({} nodes) between blocks in node {}",
876                                inline_run.len(),
877                                new_dom_id.index()
878                            )));
879                        }
880                        inline_run.clear();
881                    } else {
882                    // Create anonymous IFC wrapper for the inline run
883                    // This wrapper establishes an Inline Formatting Context
884                    let anon_idx = new_tree_builder.create_anonymous_node(
885                        new_node_idx,
886                        AnonymousBoxType::InlineWrapper,
887                        FormattingContext::Inline, // IFC for inline content
888                    );
889
890                    if let Some(msgs) = debug_messages.as_mut() {
891                        msgs.push(LayoutDebugMessage::info(format!(
892                            "[reconcile_recursive] Created anonymous IFC wrapper (layout_idx={}) for {} inline children: {:?}",
893                            anon_idx,
894                            inline_run.len(),
895                            inline_run.iter().map(|(_, id)| id.index()).collect::<Vec<_>>()
896                        )));
897                    }
898
899                    // Process each inline child under the anonymous wrapper
900                    for (pos, inline_dom_id) in inline_run.drain(..) {
901                        let old_child_idx = old_children_indices.get(pos).copied();
902                        let reconciled_child_idx = reconcile_recursive(
903                            styled_dom,
904                            inline_dom_id,
905                            old_child_idx,
906                            Some(anon_idx), // Parent is the anonymous wrapper
907                            old_tree,
908                            new_tree_builder,
909                            recon,
910                            debug_messages,
911                        )?;
912                        if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
913                            new_child_hashes.push(child_node.subtree_hash.0);
914                        }
915                    }
916
917                    // Mark anonymous wrapper as dirty for layout
918                    recon.intrinsic_dirty.insert(anon_idx);
919                    children_are_different = true;
920                    } // end else (non-whitespace run)
921                }
922
923                // Process block-level child directly under parent
924                let old_child_idx = old_children_indices.get(i).copied();
925                let reconciled_child_idx = reconcile_recursive(
926                    styled_dom,
927                    new_child_dom_id,
928                    old_child_idx,
929                    Some(new_node_idx),
930                    old_tree,
931                    new_tree_builder,
932                    recon,
933                    debug_messages,
934                )?;
935                if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
936                    new_child_hashes.push(child_node.subtree_hash.0);
937                }
938
939                if old_tree.and_then(|t| t.get(old_child_idx?).map(|n| n.subtree_hash))
940                    != new_tree_builder
941                        .get(reconciled_child_idx)
942                        .map(|n| n.subtree_hash)
943                {
944                    children_are_different = true;
945                }
946            } else {
947                // Inline-level child - add to current run
948                inline_run.push((i, new_child_dom_id));
949            }
950        }
951
952        // Process any remaining inline run at the end
953        if !inline_run.is_empty() {
954            // CSS 2.2 § 9.2.2.1: Skip whitespace-only trailing inline runs
955            if is_whitespace_only_inline_run(styled_dom, &inline_run, new_dom_id) {
956                if let Some(msgs) = debug_messages.as_mut() {
957                    msgs.push(LayoutDebugMessage::info(format!(
958                        "[reconcile_recursive] Skipping trailing whitespace-only inline run ({} nodes) in node {}",
959                        inline_run.len(),
960                        new_dom_id.index()
961                    )));
962                }
963                // Don't create a wrapper — just drop the run
964            } else {
965            let anon_idx = new_tree_builder.create_anonymous_node(
966                new_node_idx,
967                AnonymousBoxType::InlineWrapper,
968                FormattingContext::Inline, // IFC for inline content
969            );
970
971            if let Some(msgs) = debug_messages.as_mut() {
972                msgs.push(LayoutDebugMessage::info(format!(
973                    "[reconcile_recursive] Created trailing anonymous IFC wrapper (layout_idx={}) for {} inline children: {:?}",
974                    anon_idx,
975                    inline_run.len(),
976                    inline_run.iter().map(|(_, id)| id.index()).collect::<Vec<_>>()
977                )));
978            }
979
980            for (pos, inline_dom_id) in inline_run.drain(..) {
981                let old_child_idx = old_children_indices.get(pos).copied();
982                let reconciled_child_idx = reconcile_recursive(
983                    styled_dom,
984                    inline_dom_id,
985                    old_child_idx,
986                    Some(anon_idx),
987                    old_tree,
988                    new_tree_builder,
989                    recon,
990                    debug_messages,
991                )?;
992                if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
993                    new_child_hashes.push(child_node.subtree_hash.0);
994                }
995            }
996
997            recon.intrinsic_dirty.insert(anon_idx);
998            children_are_different = true;
999            } // end else (non-whitespace trailing run)
1000        }
1001    }
1002
1003    // After reconciling children, calculate this node's full subtree hash.
1004    let final_subtree_hash = calculate_subtree_hash(new_node_data_hash, &new_child_hashes);
1005    if let Some(current_node) = new_tree_builder.get_mut(new_node_idx) {
1006        current_node.subtree_hash = final_subtree_hash;
1007    }
1008
1009    // If the node itself was dirty, or its children's structure changed, it's a layout boundary.
1010    if is_dirty || children_are_different {
1011        recon.intrinsic_dirty.insert(new_node_idx);
1012        recon.layout_roots.insert(new_node_idx);
1013    }
1014
1015    Ok(new_node_idx)
1016}
1017
1018/// Result of `prepare_layout_context`: contains the layout constraints and
1019/// intermediate values needed for `calculate_layout_for_subtree`.
1020struct PreparedLayoutContext<'a> {
1021    constraints: LayoutConstraints<'a>,
1022    /// DOM ID for the node. None for anonymous boxes.
1023    dom_id: Option<NodeId>,
1024    writing_mode: LayoutWritingMode,
1025    final_used_size: LogicalSize,
1026    box_props: crate::solver3::geometry::BoxProps,
1027}
1028
1029/// Prepares the layout context for a single node by calculating its used size
1030/// and building the layout constraints for its children.
1031///
1032/// For anonymous boxes (no dom_node_id), we use default values and inherit
1033/// from the containing block.
1034fn prepare_layout_context<'a, T: ParsedFontTrait>(
1035    ctx: &LayoutContext<'a, T>,
1036    node: &LayoutNode,
1037    containing_block_size: LogicalSize,
1038) -> Result<PreparedLayoutContext<'a>> {
1039    let dom_id = node.dom_node_id; // Can be None for anonymous boxes
1040
1041    // Phase 1: Calculate this node's provisional used size
1042
1043    // This size is based on the node's CSS properties (width, height, etc.) and
1044    // its containing block. If height is 'auto', this is a temporary value.
1045    let intrinsic = node.intrinsic_sizes.clone().unwrap_or_default();
1046    let final_used_size = calculate_used_size_for_node(
1047        ctx.styled_dom,
1048        dom_id, // Now Option<NodeId>
1049        containing_block_size,
1050        intrinsic,
1051        &node.box_props,
1052        ctx.viewport_size,
1053    )?;
1054
1055    // Phase 2: Layout children using a formatting context
1056    // Use pre-computed styles from LayoutNode instead of repeated lookups
1057    let writing_mode = node.computed_style.writing_mode;
1058    let text_align = node.computed_style.text_align;
1059    let display = node.computed_style.display;
1060    let overflow_y = node.computed_style.overflow_y;
1061
1062    // Check if height is auto (no explicit height set)
1063    let height_is_auto = node.computed_style.height.is_none();
1064
1065    let available_size_for_children = if height_is_auto {
1066        // Height is auto - use containing block size as available size
1067        let inner_size = node.box_props.inner_size(final_used_size, writing_mode);
1068
1069        // For inline elements (display: inline), the available width comes from
1070        // the containing block, not from the element's own intrinsic size.
1071        // CSS 2.2 § 10.3.1: Inline, non-replaced elements use containing block width.
1072        let available_width = match display {
1073            LayoutDisplay::Inline => containing_block_size.width,
1074            _ => inner_size.width,
1075        };
1076
1077        LogicalSize {
1078            width: available_width,
1079            // Use containing block height!
1080            height: containing_block_size.height,
1081        }
1082    } else {
1083        // Height is explicit - use inner size (after padding/border)
1084        node.box_props.inner_size(final_used_size, writing_mode)
1085    };
1086
1087    // Proactively reserve space for scrollbars based on overflow properties.
1088    // Use per-node CSS `scrollbar-width` + OS overlay preference.
1089    let scrollbar_reservation = match overflow_y {
1090        LayoutOverflow::Scroll | LayoutOverflow::Auto => {
1091            dom_id.map(|id| {
1092                let styled_node_state = ctx.styled_dom.styled_nodes.as_container()
1093                    .get(id).map(|s| s.styled_node_state.clone()).unwrap_or_default();
1094                crate::solver3::getters::get_layout_scrollbar_width_px(ctx, id, &styled_node_state)
1095            }).unwrap_or(fc::DEFAULT_SCROLLBAR_WIDTH_PX)
1096        }
1097        _ => 0.0,
1098    };
1099
1100    // Reduce available width by scrollbar reservation (if any)
1101    let available_size_for_children = if scrollbar_reservation > 0.0 {
1102        LogicalSize {
1103            width: (available_size_for_children.width - scrollbar_reservation).max(0.0),
1104            height: available_size_for_children.height,
1105        }
1106    } else {
1107        available_size_for_children
1108    };
1109
1110    let constraints = LayoutConstraints {
1111        available_size: available_size_for_children,
1112        bfc_state: None,
1113        writing_mode,
1114        text_align: style_text_align_to_fc(text_align),
1115        containing_block_size,
1116        available_width_type: Text3AvailableSpace::Definite(available_size_for_children.width),
1117    };
1118
1119    Ok(PreparedLayoutContext {
1120        constraints,
1121        dom_id,
1122        writing_mode,
1123        final_used_size,
1124        box_props: node.box_props.clone(),
1125    })
1126}
1127
1128/// Determines scrollbar requirements for a node based on content overflow.
1129///
1130/// Checks if scrollbars are needed by comparing content size against available space.
1131/// For paged media (PDF), scrollbars are never added since they don't exist in print.
1132/// Returns the computed ScrollbarRequirements with horizontal/vertical needs and dimensions.
1133fn compute_scrollbar_info<T: ParsedFontTrait>(
1134    ctx: &LayoutContext<'_, T>,
1135    dom_id: NodeId,
1136    styled_node_state: &azul_core::styled_dom::StyledNodeState,
1137    content_size: LogicalSize,
1138    box_props: &crate::solver3::geometry::BoxProps,
1139    final_used_size: LogicalSize,
1140    writing_mode: LayoutWritingMode,
1141) -> ScrollbarRequirements {
1142    // Skip scrollbar handling for paged media (PDF)
1143    if ctx.fragmentation_context.is_some() {
1144        return ScrollbarRequirements::default();
1145    }
1146
1147    let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, styled_node_state);
1148    let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, styled_node_state);
1149
1150    let container_size = box_props.inner_size(final_used_size, writing_mode);
1151
1152    // Resolve per-node scrollbar width from CSS + OS overlay preference
1153    let scrollbar_width_px =
1154        crate::solver3::getters::get_layout_scrollbar_width_px(ctx, dom_id, styled_node_state);
1155
1156    fc::check_scrollbar_necessity(
1157        content_size,
1158        container_size,
1159        to_overflow_behavior(overflow_x),
1160        to_overflow_behavior(overflow_y),
1161        scrollbar_width_px,
1162    )
1163}
1164
1165/// Checks if scrollbars changed compared to previous layout and if reflow is needed.
1166///
1167/// To prevent oscillation, we only trigger reflow when scrollbars are *added*,
1168/// never when they would be *removed*. This is because:
1169/// 1. Adding scrollbars reduces available space → content reflows → may fit
1170/// 2. Removing scrollbars increases space → content reflows → may overflow again
1171/// This creates an infinite loop. By only allowing transitions *to* scrollbars,
1172/// we reach a stable state where scrollbars are present if ever needed.
1173fn check_scrollbar_change(
1174    tree: &LayoutTree,
1175    node_index: usize,
1176    scrollbar_info: &ScrollbarRequirements,
1177    skip_scrollbar_check: bool,
1178) -> bool {
1179    if skip_scrollbar_check {
1180        return false;
1181    }
1182
1183    let Some(current_node) = tree.get(node_index) else {
1184        return false;
1185    };
1186
1187    match &current_node.scrollbar_info {
1188        None => scrollbar_info.needs_reflow(),
1189        Some(old_info) => {
1190            // Only trigger reflow if scrollbars are being ADDED, not removed
1191            let adding_horizontal = !old_info.needs_horizontal && scrollbar_info.needs_horizontal;
1192            let adding_vertical = !old_info.needs_vertical && scrollbar_info.needs_vertical;
1193            adding_horizontal || adding_vertical
1194        }
1195    }
1196}
1197
1198/// Merges new scrollbar info with existing info, keeping scrollbars once needed.
1199///
1200/// This prevents the oscillation problem where content reflows to fit without
1201/// scrollbars, but then overflows again when scrollbars are removed.
1202fn merge_scrollbar_info(
1203    tree: &LayoutTree,
1204    node_index: usize,
1205    new_info: &ScrollbarRequirements,
1206) -> ScrollbarRequirements {
1207    let Some(current_node) = tree.get(node_index) else {
1208        return new_info.clone();
1209    };
1210
1211    match &current_node.scrollbar_info {
1212        Some(old) => {
1213            // Use whichever width is non-zero (prefer new_info since it has the
1214            // most up-to-date per-node CSS resolution)
1215            let effective_width = if new_info.scrollbar_width > 0.0 {
1216                new_info.scrollbar_width
1217            } else {
1218                old.scrollbar_width
1219            };
1220            let effective_height = if new_info.scrollbar_height > 0.0 {
1221                new_info.scrollbar_height
1222            } else {
1223                old.scrollbar_height
1224            };
1225
1226            ScrollbarRequirements {
1227                needs_horizontal: old.needs_horizontal || new_info.needs_horizontal,
1228                needs_vertical: old.needs_vertical || new_info.needs_vertical,
1229                scrollbar_width: if old.needs_vertical || new_info.needs_vertical {
1230                    effective_width
1231                } else {
1232                    0.0
1233                },
1234                scrollbar_height: if old.needs_horizontal || new_info.needs_horizontal {
1235                    effective_height
1236                } else {
1237                    0.0
1238                },
1239            }
1240        }
1241        None => new_info.clone(),
1242    }
1243}
1244
1245/// Calculates the content-box position from a margin-box position.
1246///
1247/// The content-box is offset from the margin-box by border + padding.
1248/// Margin is NOT added here because containing_block_pos already accounts for it.
1249fn calculate_content_box_pos(
1250    containing_block_pos: LogicalPosition,
1251    box_props: &crate::solver3::geometry::BoxProps,
1252) -> LogicalPosition {
1253    LogicalPosition::new(
1254        containing_block_pos.x + box_props.border.left + box_props.padding.left,
1255        containing_block_pos.y + box_props.border.top + box_props.padding.top,
1256    )
1257}
1258
1259/// Emits debug logging for content-box calculation if debug messages are enabled.
1260fn log_content_box_calculation<T: ParsedFontTrait>(
1261    ctx: &mut LayoutContext<'_, T>,
1262    node_index: usize,
1263    current_node: &LayoutNode,
1264    containing_block_pos: LogicalPosition,
1265    self_content_box_pos: LogicalPosition,
1266) {
1267    let Some(debug_msgs) = ctx.debug_messages.as_mut() else {
1268        return;
1269    };
1270
1271    let dom_name = current_node
1272        .dom_node_id
1273        .and_then(|id| {
1274            ctx.styled_dom
1275                .node_data
1276                .as_container()
1277                .internal
1278                .get(id.index())
1279        })
1280        .map(|n| format!("{:?}", n.node_type))
1281        .unwrap_or_else(|| "Unknown".to_string());
1282
1283    debug_msgs.push(LayoutDebugMessage::new(
1284        LayoutDebugMessageType::PositionCalculation,
1285        format!(
1286            "[CONTENT BOX {}] {} - margin-box pos=({:.2}, {:.2}) + border=({:.2},{:.2}) + \
1287             padding=({:.2},{:.2}) = content-box pos=({:.2}, {:.2})",
1288            node_index,
1289            dom_name,
1290            containing_block_pos.x,
1291            containing_block_pos.y,
1292            current_node.box_props.border.left,
1293            current_node.box_props.border.top,
1294            current_node.box_props.padding.left,
1295            current_node.box_props.padding.top,
1296            self_content_box_pos.x,
1297            self_content_box_pos.y
1298        ),
1299    ));
1300}
1301
1302/// Emits debug logging for child positioning if debug messages are enabled.
1303fn log_child_positioning<T: ParsedFontTrait>(
1304    ctx: &mut LayoutContext<'_, T>,
1305    child_index: usize,
1306    child_node: &LayoutNode,
1307    self_content_box_pos: LogicalPosition,
1308    child_relative_pos: LogicalPosition,
1309    child_absolute_pos: LogicalPosition,
1310) {
1311    // Always print positioning info for debugging
1312    let child_dom_name = child_node
1313        .dom_node_id
1314        .and_then(|id| {
1315            ctx.styled_dom
1316                .node_data
1317                .as_container()
1318                .internal
1319                .get(id.index())
1320        })
1321        .map(|n| format!("{:?}", n.node_type))
1322        .unwrap_or_else(|| "Unknown".to_string());
1323
1324    let Some(debug_msgs) = ctx.debug_messages.as_mut() else {
1325        return;
1326    };
1327
1328    debug_msgs.push(LayoutDebugMessage::new(
1329        LayoutDebugMessageType::PositionCalculation,
1330        format!(
1331            "[CHILD POS {}] {} - parent content-box=({:.2}, {:.2}) + relative=({:.2}, {:.2}) + \
1332             margin=({:.2}, {:.2}) = absolute=({:.2}, {:.2})",
1333            child_index,
1334            child_dom_name,
1335            self_content_box_pos.x,
1336            self_content_box_pos.y,
1337            child_relative_pos.x,
1338            child_relative_pos.y,
1339            child_node.box_props.margin.left,
1340            child_node.box_props.margin.top,
1341            child_absolute_pos.x,
1342            child_absolute_pos.y
1343        ),
1344    ));
1345}
1346
1347/// Processes a single in-flow child: sets position and recurses.
1348///
1349/// For Flex/Grid containers, Taffy has already laid out the children completely.
1350/// We only recurse to position their grandchildren.
1351/// For Block/Inline/Table, layout_bfc/layout_ifc already laid out children in Pass 1.
1352/// We only need to set absolute positions and recurse for positioning grandchildren.
1353fn process_inflow_child<T: ParsedFontTrait>(
1354    ctx: &mut LayoutContext<'_, T>,
1355    tree: &mut LayoutTree,
1356    text_cache: &mut TextLayoutCache,
1357    child_index: usize,
1358    child_relative_pos: LogicalPosition,
1359    self_content_box_pos: LogicalPosition,
1360    inner_size_after_scrollbars: LogicalSize,
1361    writing_mode: LayoutWritingMode,
1362    is_flex_or_grid: bool,
1363    calculated_positions: &mut super::PositionVec,
1364    reflow_needed_for_scrollbars: &mut bool,
1365    float_cache: &mut BTreeMap<usize, fc::FloatingContext>,
1366) -> Result<()> {
1367    // Set relative position on child
1368    // child_relative_pos is [CoordinateSpace::Parent] - relative to parent's content-box
1369    let child_node = tree.get_mut(child_index).ok_or(LayoutError::InvalidTree)?;
1370    child_node.relative_position = Some(child_relative_pos);
1371
1372    // Calculate absolute position
1373    // self_content_box_pos is [CoordinateSpace::Window] - absolute position of parent's content-box
1374    // child_absolute_pos becomes [CoordinateSpace::Window] - absolute window position of child
1375    let child_absolute_pos = LogicalPosition::new(
1376        self_content_box_pos.x + child_relative_pos.x,
1377        self_content_box_pos.y + child_relative_pos.y,
1378    );
1379
1380    // Debug logging
1381    {
1382        let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1383        log_child_positioning(
1384            ctx,
1385            child_index,
1386            child_node,
1387            self_content_box_pos,
1388            child_relative_pos,
1389            child_absolute_pos,
1390        );
1391    }
1392
1393    // calculated_positions stores [CoordinateSpace::Window] - absolute positions
1394    super::pos_set(calculated_positions, child_index, child_absolute_pos);
1395
1396    // Get child's properties for recursion
1397    let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1398    let child_content_box_pos =
1399        calculate_content_box_pos(child_absolute_pos, &child_node.box_props);
1400    let child_inner_size = child_node
1401        .box_props
1402        .inner_size(child_node.used_size.unwrap_or_default(), writing_mode);
1403    let child_children: Vec<usize> = child_node.children.clone();
1404    let child_fc = child_node.formatting_context.clone();
1405
1406    // Recurse to position grandchildren
1407    // OPTIMIZATION: For BFC/IFC children, layout_bfc/layout_ifc already computed their layout.
1408    // We just need to set absolute positions for descendants.
1409    // Only recurse if child has children to position.
1410    if !child_children.is_empty() {
1411        if is_flex_or_grid {
1412            // For Flex/Grid: Taffy already set used_size. Only recurse for grandchildren.
1413            position_flex_child_descendants(
1414                ctx,
1415                tree,
1416                text_cache,
1417                child_index,
1418                child_content_box_pos,
1419                child_inner_size,
1420                calculated_positions,
1421                reflow_needed_for_scrollbars,
1422                float_cache,
1423            )?;
1424        } else {
1425            // For Block/Inline/Table: The formatting context already laid out children.
1426            // Recursively position grandchildren using their cached layout data.
1427            position_bfc_child_descendants(
1428                tree,
1429                child_index,
1430                child_content_box_pos,
1431                calculated_positions,
1432            );
1433        }
1434    }
1435
1436    Ok(())
1437}
1438
1439/// Recursively positions descendants of a BFC/IFC child without re-computing layout.
1440/// The layout was already computed by layout_bfc/layout_ifc.
1441/// We only need to convert relative positions to absolute positions.
1442fn position_bfc_child_descendants(
1443    tree: &LayoutTree,
1444    node_index: usize,
1445    content_box_pos: LogicalPosition,
1446    calculated_positions: &mut super::PositionVec,
1447) {
1448    let Some(node) = tree.get(node_index) else { return };
1449    
1450    for &child_index in &node.children {
1451        let Some(child_node) = tree.get(child_index) else { continue };
1452        
1453        // Use the relative_position that was set during formatting context layout
1454        let child_rel_pos = child_node.relative_position.unwrap_or_default();
1455        let child_abs_pos = LogicalPosition::new(
1456            content_box_pos.x + child_rel_pos.x,
1457            content_box_pos.y + child_rel_pos.y,
1458        );
1459        
1460        super::pos_set(calculated_positions, child_index, child_abs_pos);
1461        
1462        // Calculate child's content-box position for recursion
1463        let child_content_box_pos = LogicalPosition::new(
1464            child_abs_pos.x + child_node.box_props.border.left + child_node.box_props.padding.left,
1465            child_abs_pos.y + child_node.box_props.border.top + child_node.box_props.padding.top,
1466        );
1467        
1468        // Recurse to grandchildren
1469        position_bfc_child_descendants(tree, child_index, child_content_box_pos, calculated_positions);
1470    }
1471}
1472
1473/// Processes out-of-flow children (absolute/fixed positioned elements).
1474///
1475/// Out-of-flow elements don't appear in layout_output.positions but still need
1476/// a static position for when no explicit offsets are specified. This sets their
1477/// static position to the parent's content-box origin.
1478fn process_out_of_flow_children<T: ParsedFontTrait>(
1479    ctx: &mut LayoutContext<'_, T>,
1480    tree: &mut LayoutTree,
1481    text_cache: &mut TextLayoutCache,
1482    node_index: usize,
1483    self_content_box_pos: LogicalPosition,
1484    calculated_positions: &mut super::PositionVec,
1485) -> Result<()> {
1486    // Collect out-of-flow children (those not already positioned)
1487    let out_of_flow_children: Vec<(usize, Option<NodeId>)> = {
1488        let current_node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1489        current_node
1490            .children
1491            .iter()
1492            .filter_map(|&child_index| {
1493                if super::pos_contains(calculated_positions, child_index) {
1494                    return None;
1495                }
1496                let child = tree.get(child_index)?;
1497                Some((child_index, child.dom_node_id))
1498            })
1499            .collect()
1500    };
1501
1502    for (child_index, child_dom_id_opt) in out_of_flow_children {
1503        let Some(child_dom_id) = child_dom_id_opt else {
1504            continue;
1505        };
1506
1507        let position_type = get_position_type(ctx.styled_dom, Some(child_dom_id));
1508        if position_type != LayoutPosition::Absolute && position_type != LayoutPosition::Fixed {
1509            continue;
1510        }
1511
1512        // Set static position to parent's content-box origin
1513        super::pos_set(calculated_positions, child_index, self_content_box_pos);
1514
1515        // Recursively set static positions for nested out-of-flow descendants
1516        set_static_positions_recursive(
1517            ctx,
1518            tree,
1519            text_cache,
1520            child_index,
1521            self_content_box_pos,
1522            calculated_positions,
1523        )?;
1524    }
1525
1526    Ok(())
1527}
1528
1529/// Recursive, top-down pass to calculate used sizes and positions for a given subtree.
1530/// This is the single, authoritative function for in-flow layout.
1531///
1532/// Uses the per-node multi-slot cache (inspired by Taffy's 9+1 architecture) to
1533/// avoid O(n²) complexity. Each node has 9 measurement slots + 1 full layout slot.
1534///
1535/// ## Two-Mode Architecture (CSS Two-Pass Layout)
1536///
1537/// `compute_mode` determines behavior:
1538///
1539/// - **`ComputeSize`** (BFC Pass 1 — sizing):
1540///   Computes only the node's border-box size. On cache hit from measurement slots,
1541///   sets `used_size` and returns immediately — no child positioning. This is the
1542///   key to O(n) two-pass BFC: Pass 1 fills measurement caches cheaply.
1543///
1544/// - **`PerformLayout`** (BFC Pass 2 — positioning):
1545///   Computes size AND positions all children. On cache hit from layout slot,
1546///   applies cached child positions recursively. When Pass 2 provides the same
1547///   constraints as Pass 1, the "result matches request" optimization triggers
1548///   automatic cache hits.
1549///
1550/// ## Cache Hit Rates (Taffy's "result matches request" optimization)
1551///
1552/// When Pass 1 measures a node with available_size A and gets result_size R,
1553/// then Pass 2 provides R as a known_dimension, `get_size()` / `get_layout()`
1554/// recognize R == cached.result_size as a cache hit. This is the fundamental
1555/// mechanism ensuring O(n) total complexity across both passes.
1556pub fn calculate_layout_for_subtree<T: ParsedFontTrait>(
1557    ctx: &mut LayoutContext<'_, T>,
1558    tree: &mut LayoutTree,
1559    text_cache: &mut TextLayoutCache,
1560    node_index: usize,
1561    containing_block_pos: LogicalPosition,
1562    containing_block_size: LogicalSize,
1563    calculated_positions: &mut super::PositionVec,
1564    reflow_needed_for_scrollbars: &mut bool,
1565    float_cache: &mut BTreeMap<usize, fc::FloatingContext>,
1566    compute_mode: ComputeMode,
1567) -> Result<()> {
1568    // === PER-NODE CACHE CHECK (Taffy-inspired 9+1 slot cache) ===
1569    //
1570    // Two-mode cache lookup (CSS two-pass architecture):
1571    //
1572    // ComputeSize (Pass 1 — sizing):
1573    //   1. Check measurement slots (get_size) → if hit, set used_size and return.
1574    //      No child positioning needed — we only need the node's border-box size.
1575    //   2. Fall back to layout slot → if hit, extract size from full layout result.
1576    //
1577    // PerformLayout (Pass 2 — positioning):
1578    //   1. Check layout slot (get_layout) → if hit, apply cached child positions.
1579    //   2. No fallback to measurement slots (we need full positions, not just size).
1580    //
1581    // This split is critical for O(n) two-pass BFC:
1582    // - Pass 1 populates measurement slots (cheap: no absolute positioning)
1583    // - Pass 2 hits layout slot or re-computes with positions
1584    if node_index < ctx.cache_map.entries.len() {
1585        match compute_mode {
1586            ComputeMode::ComputeSize => {
1587                // ComputeSize: check measurement slot first (Taffy's 9-slot scheme)
1588                let sizing_hit = ctx.cache_map.entries[node_index]
1589                    .get_size(0, containing_block_size)
1590                    .cloned();
1591                if let Some(cached_sizing) = sizing_hit {
1592                    // SIZING CACHE HIT — set used_size and return immediately.
1593                    // No child positioning needed in ComputeSize mode.
1594                    if let Some(node) = tree.get_mut(node_index) {
1595                        node.used_size = Some(cached_sizing.result_size);
1596                        node.escaped_top_margin = cached_sizing.escaped_top_margin;
1597                        node.escaped_bottom_margin = cached_sizing.escaped_bottom_margin;
1598                        node.baseline = cached_sizing.baseline;
1599                    }
1600                    return Ok(());
1601                }
1602                // Fall through to layout slot check
1603                let layout_hit = ctx.cache_map.entries[node_index]
1604                    .get_layout(containing_block_size)
1605                    .cloned();
1606                if let Some(cached_layout) = layout_hit {
1607                    // Layout slot hit in ComputeSize mode — extract size only
1608                    if let Some(node) = tree.get_mut(node_index) {
1609                        node.used_size = Some(cached_layout.result_size);
1610                        node.overflow_content_size = Some(cached_layout.content_size);
1611                        node.scrollbar_info = Some(cached_layout.scrollbar_info.clone());
1612                    }
1613                    return Ok(());
1614                }
1615            }
1616            ComputeMode::PerformLayout => {
1617                // PerformLayout: check layout slot (the single "full layout" slot)
1618                let layout_hit = ctx.cache_map.entries[node_index]
1619                    .get_layout(containing_block_size)
1620                    .cloned();
1621                if let Some(cached_layout) = layout_hit {
1622                    // LAYOUT CACHE HIT — apply cached results with child positions
1623                    if let Some(node) = tree.get_mut(node_index) {
1624                        node.used_size = Some(cached_layout.result_size);
1625                        node.overflow_content_size = Some(cached_layout.content_size);
1626                        node.scrollbar_info = Some(cached_layout.scrollbar_info.clone());
1627                    }
1628
1629                    let box_props = tree.get(node_index)
1630                        .map(|n| n.box_props.clone())
1631                        .unwrap_or_default();
1632                    let self_content_box_pos = calculate_content_box_pos(containing_block_pos, &box_props);
1633
1634                    // Apply cached child positions and recurse
1635                    let result_size = cached_layout.result_size;
1636                    for (child_index, child_relative_pos) in &cached_layout.child_positions {
1637                        let child_abs_pos = LogicalPosition::new(
1638                            self_content_box_pos.x + child_relative_pos.x,
1639                            self_content_box_pos.y + child_relative_pos.y,
1640                        );
1641                        super::pos_set(calculated_positions, *child_index, child_abs_pos);
1642
1643                        let child_available_size = box_props.inner_size(
1644                            result_size,
1645                            LayoutWritingMode::HorizontalTb,
1646                        );
1647                        calculate_layout_for_subtree(
1648                            ctx,
1649                            tree,
1650                            text_cache,
1651                            *child_index,
1652                            child_abs_pos,
1653                            child_available_size,
1654                            calculated_positions,
1655                            reflow_needed_for_scrollbars,
1656                            float_cache,
1657                            compute_mode,
1658                        )?;
1659                    }
1660
1661                    return Ok(());
1662                }
1663            }
1664        }
1665    }
1666    
1667    // === CACHE MISS — compute layout ===
1668    
1669    // Phase 1: Prepare layout context (calculate used size, constraints)
1670    let PreparedLayoutContext {
1671        constraints,
1672        dom_id,
1673        writing_mode,
1674        mut final_used_size,
1675        box_props,
1676    } = {
1677        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1678        prepare_layout_context(ctx, node, containing_block_size)?
1679    };
1680
1681    // Phase 2: Layout children using the formatting context
1682    let layout_result =
1683        layout_formatting_context(ctx, tree, text_cache, node_index, &constraints, float_cache)?;
1684    let content_size = layout_result.output.overflow_size;
1685
1686    // Phase 2.5: Resolve 'auto' main-axis size based on content
1687    // For anonymous boxes, use default styled node state
1688    let styled_node_state = dom_id
1689        .and_then(|id| ctx.styled_dom.styled_nodes.as_container().get(id).cloned())
1690        .map(|n| n.styled_node_state)
1691        .unwrap_or_default();
1692
1693    let css_height: MultiValue<LayoutHeight> = match dom_id {
1694        Some(id) => get_css_height(ctx.styled_dom, id, &styled_node_state),
1695        None => MultiValue::Auto, // Anonymous boxes have auto height
1696    };
1697
1698    // Check if this node is a scroll container (overflow: scroll/auto).
1699    // Scroll containers must NOT expand to fit content — their height is
1700    // determined by the containing block, and overflow is scrollable.
1701    //
1702    // Exception: if the containing block height is infinite (unconstrained),
1703    // we must still grow, since you can't scroll inside an infinitely tall box.
1704    let is_scroll_container = dom_id.map_or(false, |id| {
1705        let ov_x = get_overflow_x(ctx.styled_dom, id, &styled_node_state);
1706        let ov_y = get_overflow_y(ctx.styled_dom, id, &styled_node_state);
1707        matches!(ov_x, MultiValue::Exact(LayoutOverflow::Scroll) | MultiValue::Exact(LayoutOverflow::Auto))
1708            || matches!(ov_y, MultiValue::Exact(LayoutOverflow::Scroll) | MultiValue::Exact(LayoutOverflow::Auto))
1709    });
1710
1711    if should_use_content_height(&css_height) {
1712        let skip_expansion = is_scroll_container
1713            && containing_block_size.height.is_finite()
1714            && containing_block_size.height > 0.0;
1715
1716        if !skip_expansion {
1717            final_used_size = apply_content_based_height(
1718                final_used_size,
1719                content_size,
1720                tree,
1721                node_index,
1722                writing_mode,
1723            );
1724        }
1725    }
1726
1727    // Phase 3: Scrollbar handling
1728    // Anonymous boxes don't have scrollbars
1729    let skip_scrollbar_check = ctx.fragmentation_context.is_some();
1730    let scrollbar_info = match dom_id {
1731        Some(id) => compute_scrollbar_info(
1732            ctx,
1733            id,
1734            &styled_node_state,
1735            content_size,
1736            &box_props,
1737            final_used_size,
1738            writing_mode,
1739        ),
1740        None => ScrollbarRequirements::default(),
1741    };
1742
1743    if check_scrollbar_change(tree, node_index, &scrollbar_info, skip_scrollbar_check) {
1744        *reflow_needed_for_scrollbars = true;
1745    }
1746
1747    let merged_scrollbar_info = merge_scrollbar_info(tree, node_index, &scrollbar_info);
1748    let content_box_size = box_props.inner_size(final_used_size, writing_mode);
1749    let inner_size_after_scrollbars = merged_scrollbar_info.shrink_size(content_box_size);
1750
1751    // Phase 4: Update this node's state
1752    let self_content_box_pos = {
1753        let current_node = tree.get_mut(node_index).ok_or(LayoutError::InvalidTree)?;
1754
1755        // Table cells get their size from the table layout algorithm, don't overwrite
1756        let is_table_cell = matches!(
1757            current_node.formatting_context,
1758            FormattingContext::TableCell
1759        );
1760        if !is_table_cell || current_node.used_size.is_none() {
1761            current_node.used_size = Some(final_used_size);
1762        }
1763        current_node.scrollbar_info = Some(merged_scrollbar_info.clone());
1764        // Store overflow content size for scroll frame calculation
1765        current_node.overflow_content_size = Some(content_size);
1766
1767        // self_content_box_pos is [CoordinateSpace::Window] - the absolute position of this node's content-box
1768        let pos = calculate_content_box_pos(containing_block_pos, &current_node.box_props);
1769        log_content_box_calculation(ctx, node_index, current_node, containing_block_pos, pos);
1770        pos
1771    };
1772
1773    // Phase 5: Determine formatting context type
1774    let is_flex_or_grid = {
1775        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1776        matches!(
1777            node.formatting_context,
1778            FormattingContext::Flex | FormattingContext::Grid
1779        )
1780    };
1781
1782    // Phase 6: Process in-flow children
1783    // Positions in layout_result.output.positions are [CoordinateSpace::Parent] - relative to this node's content-box
1784    let positions: Vec<_> = layout_result
1785        .output
1786        .positions
1787        .iter()
1788        .map(|(&idx, &pos)| (idx, pos))
1789        .collect();
1790
1791    // Store child positions for cache
1792    let child_positions_for_cache: Vec<(usize, LogicalPosition)> = positions.clone();
1793
1794    for (child_index, child_relative_pos) in positions {
1795        process_inflow_child(
1796            ctx,
1797            tree,
1798            text_cache,
1799            child_index,
1800            child_relative_pos,
1801            self_content_box_pos,
1802            inner_size_after_scrollbars,
1803            writing_mode,
1804            is_flex_or_grid,
1805            calculated_positions,
1806            reflow_needed_for_scrollbars,
1807            float_cache,
1808        )?;
1809    }
1810
1811    // Phase 7: Process out-of-flow children (absolute/fixed)
1812    process_out_of_flow_children(
1813        ctx,
1814        tree,
1815        text_cache,
1816        node_index,
1817        self_content_box_pos,
1818        calculated_positions,
1819    )?;
1820
1821    // === STORE RESULT IN PER-NODE CACHE (Taffy-inspired 9+1 slot cache) ===
1822    // Store both the full layout entry and a sizing measurement entry.
1823    // This enables O(n) two-pass BFC: Pass 1 populates cache, Pass 2 reads it.
1824    if node_index < ctx.cache_map.entries.len() {
1825        let node_ref = tree.get(node_index);
1826        let baseline = node_ref.and_then(|n| n.baseline);
1827        let escaped_top = node_ref.and_then(|n| n.escaped_top_margin);
1828        let escaped_bottom = node_ref.and_then(|n| n.escaped_bottom_margin);
1829
1830        // Store in the layout slot (PerformLayout result)
1831        ctx.cache_map.get_mut(node_index).store_layout(LayoutCacheEntry {
1832            available_size: containing_block_size,
1833            result_size: final_used_size,
1834            content_size,
1835            child_positions: child_positions_for_cache.clone(),
1836            escaped_top_margin: escaped_top,
1837            escaped_bottom_margin: escaped_bottom,
1838            scrollbar_info: merged_scrollbar_info.clone(),
1839        });
1840
1841        // Also store in a measurement slot (slot 0: both dimensions known)
1842        // This enables the "result matches request" optimization (Taffy pattern):
1843        // when Pass 2 provides the same size as Pass 1 measured, it's a cache hit.
1844        ctx.cache_map.get_mut(node_index).store_size(0, SizingCacheEntry {
1845            available_size: containing_block_size,
1846            result_size: final_used_size,
1847            baseline,
1848            escaped_top_margin: escaped_top,
1849            escaped_bottom_margin: escaped_bottom,
1850        });
1851    }
1852
1853    Ok(())
1854}
1855
1856/// Recursively set static positions for out-of-flow descendants without doing layout
1857/// Recursively positions descendants of Flex/Grid children.
1858///
1859/// When a Flex container lays out its children via Taffy, the children have their
1860/// used_size and relative_position set, but their GRANDCHILDREN don't have positions
1861/// in calculated_positions yet. This function traverses down the tree and positions
1862/// all descendants properly.
1863fn position_flex_child_descendants<T: ParsedFontTrait>(
1864    ctx: &mut LayoutContext<'_, T>,
1865    tree: &mut LayoutTree,
1866    text_cache: &mut TextLayoutCache,
1867    node_index: usize,
1868    content_box_pos: LogicalPosition,
1869    available_size: LogicalSize,
1870    calculated_positions: &mut super::PositionVec,
1871    reflow_needed_for_scrollbars: &mut bool,
1872    float_cache: &mut BTreeMap<usize, fc::FloatingContext>,
1873) -> Result<()> {
1874    let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1875    let children: Vec<usize> = node.children.clone();
1876    let fc = node.formatting_context.clone();
1877
1878    // If this node is itself a Flex/Grid container, its children were laid out by Taffy
1879    // and already have relative_position set. We just need to convert to absolute and recurse.
1880    if matches!(fc, FormattingContext::Flex | FormattingContext::Grid) {
1881        for &child_index in &children {
1882            let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1883            let child_rel_pos = child_node.relative_position.unwrap_or_default();
1884            let child_abs_pos = LogicalPosition::new(
1885                content_box_pos.x + child_rel_pos.x,
1886                content_box_pos.y + child_rel_pos.y,
1887            );
1888
1889            // Insert position
1890            super::pos_set(calculated_positions, child_index, child_abs_pos);
1891
1892            // Get child's content box for recursion
1893            let child_content_box = LogicalPosition::new(
1894                child_abs_pos.x
1895                    + child_node.box_props.border.left
1896                    + child_node.box_props.padding.left,
1897                child_abs_pos.y
1898                    + child_node.box_props.border.top
1899                    + child_node.box_props.padding.top,
1900            );
1901            let child_inner_size = child_node.box_props.inner_size(
1902                child_node.used_size.unwrap_or_default(),
1903                LayoutWritingMode::HorizontalTb,
1904            );
1905
1906            // Recurse
1907            position_flex_child_descendants(
1908                ctx,
1909                tree,
1910                text_cache,
1911                child_index,
1912                child_content_box,
1913                child_inner_size,
1914                calculated_positions,
1915                reflow_needed_for_scrollbars,
1916                float_cache,
1917            )?;
1918        }
1919    } else {
1920        // For Block/Inline/Table children, their descendants need proper layout calculation
1921        // Use the output.positions from their own layout
1922        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1923        let children: Vec<usize> = node.children.clone();
1924
1925        for &child_index in &children {
1926            let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1927            let child_rel_pos = child_node.relative_position.unwrap_or_default();
1928            let child_abs_pos = LogicalPosition::new(
1929                content_box_pos.x + child_rel_pos.x,
1930                content_box_pos.y + child_rel_pos.y,
1931            );
1932
1933            // Insert position
1934            super::pos_set(calculated_positions, child_index, child_abs_pos);
1935
1936            // Get child's content box for recursion
1937            let child_content_box = LogicalPosition::new(
1938                child_abs_pos.x
1939                    + child_node.box_props.border.left
1940                    + child_node.box_props.padding.left,
1941                child_abs_pos.y
1942                    + child_node.box_props.border.top
1943                    + child_node.box_props.padding.top,
1944            );
1945            let child_inner_size = child_node.box_props.inner_size(
1946                child_node.used_size.unwrap_or_default(),
1947                LayoutWritingMode::HorizontalTb,
1948            );
1949
1950            // Recurse
1951            position_flex_child_descendants(
1952                ctx,
1953                tree,
1954                text_cache,
1955                child_index,
1956                child_content_box,
1957                child_inner_size,
1958                calculated_positions,
1959                reflow_needed_for_scrollbars,
1960                float_cache,
1961            )?;
1962        }
1963    }
1964
1965    Ok(())
1966}
1967
1968fn set_static_positions_recursive<T: ParsedFontTrait>(
1969    ctx: &mut LayoutContext<'_, T>,
1970    tree: &mut LayoutTree,
1971    _text_cache: &mut TextLayoutCache,
1972    node_index: usize,
1973    parent_content_box_pos: LogicalPosition,
1974    calculated_positions: &mut super::PositionVec,
1975) -> Result<()> {
1976    let out_of_flow_children: Vec<(usize, Option<NodeId>)> = {
1977        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1978        node.children
1979            .iter()
1980            .filter_map(|&child_index| {
1981                if super::pos_contains(calculated_positions, child_index) {
1982                    None
1983                } else {
1984                    let child = tree.get(child_index)?;
1985                    Some((child_index, child.dom_node_id))
1986                }
1987            })
1988            .collect()
1989    };
1990
1991    for (child_index, child_dom_id_opt) in out_of_flow_children {
1992        if let Some(child_dom_id) = child_dom_id_opt {
1993            let position_type = get_position_type(ctx.styled_dom, Some(child_dom_id));
1994            if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
1995                super::pos_set(calculated_positions, child_index, parent_content_box_pos);
1996
1997                // Continue recursively
1998                set_static_positions_recursive(
1999                    ctx,
2000                    tree,
2001                    _text_cache,
2002                    child_index,
2003                    parent_content_box_pos,
2004                    calculated_positions,
2005                )?;
2006            }
2007        }
2008    }
2009
2010    Ok(())
2011}
2012
2013/// Checks if the given CSS height value should use content-based sizing
2014fn should_use_content_height(css_height: &MultiValue<LayoutHeight>) -> bool {
2015    match css_height {
2016        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
2017            // Auto/Initial/Inherit height should use content-based sizing
2018            true
2019        }
2020        MultiValue::Exact(height) => match height {
2021            LayoutHeight::Auto => {
2022                // Auto height should use content-based sizing
2023                true
2024            }
2025            LayoutHeight::Px(px) => {
2026                // Check if it's zero or if it has explicit value
2027                // If it's a percentage or em, it's not auto
2028                use azul_css::props::basic::{pixel::PixelValue, SizeMetric};
2029                px == &PixelValue::zero()
2030                    || (px.metric != SizeMetric::Px
2031                        && px.metric != SizeMetric::Percent
2032                        && px.metric != SizeMetric::Em
2033                        && px.metric != SizeMetric::Rem)
2034            }
2035            LayoutHeight::MinContent | LayoutHeight::MaxContent => {
2036                // These are content-based, so they should use the content size
2037                true
2038            }
2039            LayoutHeight::Calc(_) => {
2040                // Calc expressions are not auto, they compute to a specific value
2041                false
2042            }
2043        },
2044    }
2045}
2046
2047/// Applies content-based height sizing to a node
2048///
2049/// **Note**: This function respects min-height/max-height constraints from Phase 1.
2050///
2051/// According to CSS 2.2 § 10.7, when height is 'auto', the final height must be
2052/// max(min_height, min(content_height, max_height)).
2053///
2054/// The `used_size` parameter already contains the size constrained by
2055/// min-height/max-height from the initial sizing pass. We must take the
2056/// maximum of this constrained size and the new content-based size to ensure
2057/// min-height is not lost.
2058fn apply_content_based_height(
2059    mut used_size: LogicalSize,
2060    content_size: LogicalSize,
2061    tree: &LayoutTree,
2062    node_index: usize,
2063    writing_mode: LayoutWritingMode,
2064) -> LogicalSize {
2065    let node_props = &tree.get(node_index).unwrap().box_props;
2066    let main_axis_padding_border =
2067        node_props.padding.main_sum(writing_mode) + node_props.border.main_sum(writing_mode);
2068
2069    // CRITICAL: 'old_main_size' holds the size constrained by min-height/max-height from Phase 1
2070    let old_main_size = used_size.main(writing_mode);
2071    let new_main_size = content_size.main(writing_mode) + main_axis_padding_border;
2072
2073    // Final size = max(min_height_constrained_size, content_size)
2074    // This ensures that min-height is respected even when content is smaller
2075    let final_main_size = old_main_size.max(new_main_size);
2076
2077    used_size = used_size.with_main(writing_mode, final_main_size);
2078
2079    used_size
2080}
2081
2082fn hash_styled_node_data(dom: &StyledDom, node_id: NodeId) -> u64 {
2083    let mut hasher = DefaultHasher::new();
2084    if let Some(styled_node) = dom.styled_nodes.as_container().get(node_id) {
2085        styled_node.styled_node_state.hash(&mut hasher);
2086    }
2087    if let Some(node_data) = dom.node_data.as_container().get(node_id) {
2088        node_data.get_node_type().hash(&mut hasher);
2089    }
2090    hasher.finish()
2091}
2092
2093fn calculate_subtree_hash(node_self_hash: u64, child_hashes: &[u64]) -> SubtreeHash {
2094    let mut hasher = DefaultHasher::new();
2095    node_self_hash.hash(&mut hasher);
2096    child_hashes.hash(&mut hasher);
2097    SubtreeHash(hasher.finish())
2098}
2099
2100/// Computes CSS counter values for all nodes in the layout tree.
2101///
2102/// This function traverses the tree in document order and processes counter-reset
2103/// and counter-increment properties. The computed values are stored in cache.counters.
2104///
2105/// CSS counters work with a stack-based scoping model:
2106/// - `counter-reset` creates a new scope and sets the counter to a value
2107/// - `counter-increment` increments the counter in the current scope
2108/// - When leaving a subtree, counter scopes are popped
2109pub fn compute_counters(
2110    styled_dom: &StyledDom,
2111    tree: &LayoutTree,
2112    counters: &mut BTreeMap<(usize, String), i32>,
2113) {
2114    use std::collections::HashMap;
2115
2116    // Track counter stacks: counter_name -> Vec<value>
2117    // Each entry in the Vec represents a nested scope
2118    let mut counter_stacks: HashMap<String, Vec<i32>> = HashMap::new();
2119
2120    // Stack to track which counters were reset at each tree level
2121    // When we pop back up the tree, we need to pop these counter scopes
2122    let mut scope_stack: Vec<Vec<String>> = Vec::new();
2123
2124    compute_counters_recursive(
2125        styled_dom,
2126        tree,
2127        tree.root,
2128        counters,
2129        &mut counter_stacks,
2130        &mut scope_stack,
2131    );
2132}
2133
2134fn compute_counters_recursive(
2135    styled_dom: &StyledDom,
2136    tree: &LayoutTree,
2137    node_idx: usize,
2138    counters: &mut BTreeMap<(usize, String), i32>,
2139    counter_stacks: &mut std::collections::HashMap<String, Vec<i32>>,
2140    scope_stack: &mut Vec<Vec<String>>,
2141) {
2142    let node = match tree.get(node_idx) {
2143        Some(n) => n,
2144        None => return,
2145    };
2146
2147    // Skip pseudo-elements (::marker, ::before, ::after) for counter processing
2148    // Pseudo-elements inherit counter values from their parent element
2149    // but don't participate in counter-reset or counter-increment themselves
2150    if node.pseudo_element.is_some() {
2151        // Store the parent's counter values for this pseudo-element
2152        // so it can be looked up during marker text generation
2153        if let Some(parent_idx) = node.parent {
2154            // Copy all counter values from parent to this pseudo-element
2155            let parent_counters: Vec<_> = counters
2156                .iter()
2157                .filter(|((idx, _), _)| *idx == parent_idx)
2158                .map(|((_, name), &value)| (name.clone(), value))
2159                .collect();
2160
2161            for (counter_name, value) in parent_counters {
2162                counters.insert((node_idx, counter_name), value);
2163            }
2164        }
2165
2166        // Don't recurse to children of pseudo-elements
2167        // (pseudo-elements shouldn't have children in normal circumstances)
2168        return;
2169    }
2170
2171    // Only process real DOM nodes, not anonymous boxes
2172    let dom_id = match node.dom_node_id {
2173        Some(id) => id,
2174        None => {
2175            // For anonymous boxes, just recurse to children
2176            for &child_idx in &node.children {
2177                compute_counters_recursive(
2178                    styled_dom,
2179                    tree,
2180                    child_idx,
2181                    counters,
2182                    counter_stacks,
2183                    scope_stack,
2184                );
2185            }
2186            return;
2187        }
2188    };
2189
2190    let node_data = &styled_dom.node_data.as_container()[dom_id];
2191    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2192    let cache = &styled_dom.css_property_cache.ptr;
2193
2194    // Track which counters we reset at this level (for cleanup later)
2195    let mut reset_counters_at_this_level = Vec::new();
2196
2197    // CSS Lists §3: display: list-item automatically increments the "list-item" counter
2198    // Check if this is a list-item
2199    let display = {
2200        use crate::solver3::getters::get_display_property;
2201        get_display_property(styled_dom, Some(dom_id)).exact()
2202    };
2203    let is_list_item = matches!(display, Some(LayoutDisplay::ListItem));
2204
2205    // Process counter-reset (now properly typed)
2206    let counter_reset = cache
2207        .get_counter_reset(node_data, &dom_id, node_state)
2208        .and_then(|v| v.get_property());
2209
2210    if let Some(counter_reset) = counter_reset {
2211        let counter_name_str = counter_reset.counter_name.as_str();
2212        if counter_name_str != "none" {
2213            let counter_name = counter_name_str.to_string();
2214            let reset_value = counter_reset.value;
2215
2216            // Reset the counter by pushing a new scope
2217            counter_stacks
2218                .entry(counter_name.clone())
2219                .or_default()
2220                .push(reset_value);
2221            reset_counters_at_this_level.push(counter_name);
2222        }
2223    }
2224
2225    // Process counter-increment (now properly typed)
2226    let counter_inc = cache
2227        .get_counter_increment(node_data, &dom_id, node_state)
2228        .and_then(|v| v.get_property());
2229
2230    if let Some(counter_inc) = counter_inc {
2231        let counter_name_str = counter_inc.counter_name.as_str();
2232        if counter_name_str != "none" {
2233            let counter_name = counter_name_str.to_string();
2234            let inc_value = counter_inc.value;
2235
2236            // Increment the counter in the current scope
2237            let stack = counter_stacks.entry(counter_name.clone()).or_default();
2238            if stack.is_empty() {
2239                // Auto-initialize if counter doesn't exist
2240                stack.push(inc_value);
2241            } else if let Some(current) = stack.last_mut() {
2242                *current += inc_value;
2243            }
2244        }
2245    }
2246
2247    // CSS Lists §3: display: list-item automatically increments "list-item" counter
2248    if is_list_item {
2249        let counter_name = "list-item".to_string();
2250        let stack = counter_stacks.entry(counter_name.clone()).or_default();
2251        if stack.is_empty() {
2252            // Auto-initialize if counter doesn't exist
2253            stack.push(1);
2254        } else {
2255            if let Some(current) = stack.last_mut() {
2256                *current += 1;
2257            }
2258        }
2259    }
2260
2261    // Store the current counter values for this node
2262    for (counter_name, stack) in counter_stacks.iter() {
2263        if let Some(&value) = stack.last() {
2264            counters.insert((node_idx, counter_name.clone()), value);
2265        }
2266    }
2267
2268    // Push scope tracking for cleanup
2269    scope_stack.push(reset_counters_at_this_level.clone());
2270
2271    // Recurse to children
2272    for &child_idx in &node.children {
2273        compute_counters_recursive(
2274            styled_dom,
2275            tree,
2276            child_idx,
2277            counters,
2278            counter_stacks,
2279            scope_stack,
2280        );
2281    }
2282
2283    // Pop counter scopes that were created at this level
2284    if let Some(reset_counters) = scope_stack.pop() {
2285        for counter_name in reset_counters {
2286            if let Some(stack) = counter_stacks.get_mut(&counter_name) {
2287                stack.pop();
2288            }
2289        }
2290    }
2291}