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_wrap, get_writing_mode, MultiValue,
47        },
48        layout_tree::{
49            is_block_level, AnonymousBoxType, LayoutNode, LayoutTreeBuilder, SubtreeHash,
50        },
51        positioning::get_position_type,
52        scrollbar::ScrollbarRequirements,
53        sizing::calculate_used_size_for_node,
54        LayoutContext, LayoutError, LayoutTree, Result,
55    },
56    text3::cache::AvailableSpace as Text3AvailableSpace,
57};
58
59/// The persistent cache that holds the layout state between frames.
60#[derive(Debug, Clone, Default)]
61pub struct LayoutCache {
62    /// The fully laid-out tree from the previous frame. This is our primary cache.
63    pub tree: Option<LayoutTree>,
64    /// The final, absolute positions of all nodes from the previous frame.
65    pub calculated_positions: BTreeMap<usize, LogicalPosition>,
66    /// The viewport size from the last layout pass, used to detect resizes.
67    pub viewport: Option<LogicalRect>,
68    /// Stable scroll IDs computed from node_data_hash (layout index -> scroll ID)
69    pub scroll_ids: BTreeMap<usize, u64>,
70    /// Mapping from scroll ID to DOM NodeId for hit testing
71    pub scroll_id_to_node_id: BTreeMap<u64, NodeId>,
72    /// CSS counter values for each node and counter name.
73    /// Key: (layout_index, counter_name), Value: counter value
74    /// This stores the computed counter values after processing counter-reset and
75    /// counter-increment.
76    pub counters: BTreeMap<(usize, String), i32>,
77    /// Cache of positioned floats for each BFC node (layout_index -> FloatingContext).
78    /// This persists float positions across multiple layout passes, ensuring IFC
79    /// children always have access to correct float exclusions even when layout is
80    /// recalculated.
81    pub float_cache: BTreeMap<usize, fc::FloatingContext>,
82}
83
84/// The result of a reconciliation pass.
85#[derive(Debug, Default)]
86pub struct ReconciliationResult {
87    /// Set of nodes whose intrinsic size needs to be recalculated (bottom-up pass).
88    pub intrinsic_dirty: BTreeSet<usize>,
89    /// Set of layout roots whose subtrees need a new top-down layout pass.
90    pub layout_roots: BTreeSet<usize>,
91}
92
93impl ReconciliationResult {
94    /// Checks if any layout or paint work is needed.
95    pub fn is_clean(&self) -> bool {
96        self.intrinsic_dirty.is_empty() && self.layout_roots.is_empty()
97    }
98}
99
100/// After dirty subtrees are laid out, this repositions their clean siblings
101/// without recalculating their internal layout. This is a critical optimization.
102///
103/// This function acts as a dispatcher, inspecting the parent's formatting context
104/// and calling the appropriate repositioning algorithm. For complex layout modes
105/// like Flexbox or Grid, this optimization is skipped, as a full relayout is
106/// often required to correctly recalculate spacing and sizing for all siblings.
107pub fn reposition_clean_subtrees(
108    styled_dom: &StyledDom,
109    tree: &LayoutTree,
110    layout_roots: &BTreeSet<usize>,
111    calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
112) {
113    // Find the unique parents of all dirty layout roots. These are the containers
114    // where sibling positions need to be adjusted.
115    let mut parents_to_reposition = BTreeSet::new();
116    for &root_idx in layout_roots {
117        if let Some(parent_idx) = tree.get(root_idx).and_then(|n| n.parent) {
118            parents_to_reposition.insert(parent_idx);
119        }
120    }
121
122    for parent_idx in parents_to_reposition {
123        let parent_node = match tree.get(parent_idx) {
124            Some(n) => n,
125            None => continue,
126        };
127
128        // Dispatch to the correct repositioning logic based on the parent's layout mode.
129        match parent_node.formatting_context {
130            // Cases that use simple block-flow stacking can be optimized.
131            FormattingContext::Block { .. } | FormattingContext::TableRowGroup => {
132                reposition_block_flow_siblings(
133                    styled_dom,
134                    parent_idx,
135                    parent_node,
136                    tree,
137                    layout_roots,
138                    calculated_positions,
139                );
140            }
141
142            FormattingContext::Flex | FormattingContext::Grid => {
143                // Taffy handles this, so if a child is dirty, the parent would have
144                // already been marked as a layout_root and re-laid out by Taffy.
145                // We do nothing here for Flex or Grid.
146            }
147
148            FormattingContext::Table | FormattingContext::TableRow => {
149                // STUB: Table layout is interdependent. A change in one cell's size
150                // can affect the entire column's width or row's height, requiring a
151                // full relayout of the table. This optimization is skipped.
152            }
153
154            // Other contexts either don't contain children in a way that this
155            // optimization applies (e.g., Inline, TableCell) or are handled by other
156            // layout mechanisms (e.g., OutOfFlow).
157            _ => { /* Do nothing */ }
158        }
159    }
160}
161
162/// Convert LayoutOverflow to OverflowBehavior
163pub fn to_overflow_behavior(overflow: MultiValue<LayoutOverflow>) -> fc::OverflowBehavior {
164    match overflow.unwrap_or_default() {
165        LayoutOverflow::Visible => fc::OverflowBehavior::Visible,
166        LayoutOverflow::Hidden | LayoutOverflow::Clip => fc::OverflowBehavior::Hidden,
167        LayoutOverflow::Scroll => fc::OverflowBehavior::Scroll,
168        LayoutOverflow::Auto => fc::OverflowBehavior::Auto,
169    }
170}
171
172/// Convert StyleTextAlign to fc::TextAlign
173pub const fn style_text_align_to_fc(text_align: StyleTextAlign) -> fc::TextAlign {
174    match text_align {
175        StyleTextAlign::Start | StyleTextAlign::Left => fc::TextAlign::Start,
176        StyleTextAlign::End | StyleTextAlign::Right => fc::TextAlign::End,
177        StyleTextAlign::Center => fc::TextAlign::Center,
178        StyleTextAlign::Justify => fc::TextAlign::Justify,
179    }
180}
181
182/// Collects DOM child IDs from the node hierarchy into a Vec.
183///
184/// This is a helper function that flattens the sibling iteration into a simple loop.
185pub fn collect_children_dom_ids(styled_dom: &StyledDom, parent_dom_id: NodeId) -> Vec<NodeId> {
186    let hierarchy_container = styled_dom.node_hierarchy.as_container();
187    let mut children = Vec::new();
188
189    let Some(hierarchy_item) = hierarchy_container.get(parent_dom_id) else {
190        return children;
191    };
192
193    let Some(mut child_id) = hierarchy_item.first_child_id(parent_dom_id) else {
194        return children;
195    };
196
197    children.push(child_id);
198    while let Some(hierarchy_item) = hierarchy_container.get(child_id) {
199        let Some(next) = hierarchy_item.next_sibling_id() else {
200            break;
201        };
202        children.push(next);
203        child_id = next;
204    }
205
206    children
207}
208
209/// Checks if a flex container is simple enough to be treated like a block-stack for
210/// repositioning.
211pub fn is_simple_flex_stack(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> bool {
212    let Some(id) = dom_id else { return false };
213    let binding = styled_dom.styled_nodes.as_container();
214    let styled_node = match binding.get(id) {
215        Some(styled_node) => styled_node,
216        None => return false,
217    };
218
219    // Must be a single-line flex container
220    let wrap = get_wrap(styled_dom, id, &styled_node.styled_node_state);
221
222    if wrap.unwrap_or_default() != LayoutFlexWrap::NoWrap {
223        return false;
224    }
225
226    // Must be start-aligned, so there's no space distribution to recalculate.
227    let justify = get_justify_content(styled_dom, id, &styled_node.styled_node_state);
228
229    if !matches!(
230        justify.unwrap_or_default(),
231        LayoutJustifyContent::FlexStart | LayoutJustifyContent::Start
232    ) {
233        return false;
234    }
235
236    // Crucially, no clean siblings can have flexible sizes, otherwise a dirty
237    // sibling's size change could affect their resolved size.
238    // NOTE: This check is expensive and incomplete. A more robust solution might
239    // store flags on the LayoutNode indicating if flex factors are present.
240    // For now, we assume that if a container *could* have complex flex behavior,
241    // we play it safe and require a full relayout. This heuristic is a compromise.
242    // To be truly safe, we'd have to check all children for flex-grow/shrink > 0.
243
244    true
245}
246
247/// Repositions clean children within a simple block-flow layout (like a BFC or a
248/// table-row-group). It stacks children along the main axis, preserving their
249/// previously calculated cross-axis alignment.
250pub fn reposition_block_flow_siblings(
251    styled_dom: &StyledDom,
252    parent_idx: usize,
253    parent_node: &LayoutNode,
254    tree: &LayoutTree,
255    layout_roots: &BTreeSet<usize>,
256    calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
257) {
258    let dom_id = parent_node.dom_node_id.unwrap_or(NodeId::ZERO);
259    let styled_node_state = styled_dom
260        .styled_nodes
261        .as_container()
262        .get(dom_id)
263        .map(|n| n.styled_node_state.clone())
264        .unwrap_or_default();
265
266    let writing_mode = get_writing_mode(styled_dom, dom_id, &styled_node_state).unwrap_or_default();
267
268    let parent_pos = calculated_positions
269        .get(&parent_idx)
270        .copied()
271        .unwrap_or_default();
272
273    let content_box_origin = LogicalPosition::new(
274        parent_pos.x + parent_node.box_props.padding.left,
275        parent_pos.y + parent_node.box_props.padding.top,
276    );
277
278    let mut main_pen = 0.0;
279
280    for &child_idx in &parent_node.children {
281        let child_node = match tree.get(child_idx) {
282            Some(n) => n,
283            None => continue,
284        };
285
286        let child_size = child_node.used_size.unwrap_or_default();
287        let child_main_sum = child_node.box_props.margin.main_sum(writing_mode);
288        let margin_box_main_size = child_size.main(writing_mode) + child_main_sum;
289
290        if layout_roots.contains(&child_idx) {
291            // This child was DIRTY and has been correctly repositioned.
292            // Update the pen to the position immediately after this child.
293            let new_pos = match calculated_positions.get(&child_idx) {
294                Some(p) => *p,
295                None => continue,
296            };
297
298            let main_axis_offset = if writing_mode.is_vertical() {
299                new_pos.x - content_box_origin.x
300            } else {
301                new_pos.y - content_box_origin.y
302            };
303
304            main_pen = main_axis_offset
305                + child_size.main(writing_mode)
306                + child_node.box_props.margin.main_end(writing_mode);
307        } else {
308            // This child is *clean*. Calculate its new position and shift its
309            // entire subtree.
310            let old_pos = match calculated_positions.get(&child_idx) {
311                Some(p) => *p,
312                None => continue,
313            };
314
315            let child_main_start = child_node.box_props.margin.main_start(writing_mode);
316            let new_main_pos = main_pen + child_main_start;
317            let old_relative_pos = child_node.relative_position.unwrap_or_default();
318            let cross_pos = if writing_mode.is_vertical() {
319                old_relative_pos.y
320            } else {
321                old_relative_pos.x
322            };
323            let new_relative_pos =
324                LogicalPosition::from_main_cross(new_main_pos, cross_pos, writing_mode);
325
326            let new_absolute_pos = LogicalPosition::new(
327                content_box_origin.x + new_relative_pos.x,
328                content_box_origin.y + new_relative_pos.y,
329            );
330
331            if old_pos != new_absolute_pos {
332                let delta = LogicalPosition::new(
333                    new_absolute_pos.x - old_pos.x,
334                    new_absolute_pos.y - old_pos.y,
335                );
336                shift_subtree_position(child_idx, delta, tree, calculated_positions);
337            }
338
339            main_pen += margin_box_main_size;
340        }
341    }
342}
343
344/// Helper to recursively shift the absolute position of a node and all its descendants.
345pub fn shift_subtree_position(
346    node_idx: usize,
347    delta: LogicalPosition,
348    tree: &LayoutTree,
349    calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
350) {
351    if let Some(pos) = calculated_positions.get_mut(&node_idx) {
352        pos.x += delta.x;
353        pos.y += delta.y;
354    }
355
356    if let Some(node) = tree.get(node_idx) {
357        for &child_idx in &node.children {
358            shift_subtree_position(child_idx, delta, tree, calculated_positions);
359        }
360    }
361}
362
363/// Compares the new DOM against the cached tree, creating a new tree
364/// and identifying which parts need to be re-laid out.
365pub fn reconcile_and_invalidate<T: ParsedFontTrait>(
366    ctx: &mut LayoutContext<'_, T>,
367    cache: &LayoutCache,
368    viewport: LogicalRect,
369) -> Result<(LayoutTree, ReconciliationResult)> {
370    let mut new_tree_builder = LayoutTreeBuilder::new();
371    let mut recon_result = ReconciliationResult::default();
372    let old_tree = cache.tree.as_ref();
373
374    // Check for viewport resize, which dirties the root for a top-down pass.
375    if cache.viewport.map_or(true, |v| v.size != viewport.size) {
376        recon_result.layout_roots.insert(0); // Root is always index 0
377    }
378
379    let root_dom_id = ctx
380        .styled_dom
381        .root
382        .into_crate_internal()
383        .unwrap_or(NodeId::ZERO);
384    let root_idx = reconcile_recursive(
385        ctx.styled_dom,
386        root_dom_id,
387        old_tree.map(|t| t.root),
388        None,
389        old_tree,
390        &mut new_tree_builder,
391        &mut recon_result,
392        &mut ctx.debug_messages,
393    )?;
394
395    // Clean up layout roots: if a parent is a layout root, its children don't need to be.
396    let final_layout_roots = recon_result
397        .layout_roots
398        .iter()
399        .filter(|&&idx| {
400            let mut current = new_tree_builder.get(idx).and_then(|n| n.parent);
401            while let Some(p_idx) = current {
402                if recon_result.layout_roots.contains(&p_idx) {
403                    return false;
404                }
405                current = new_tree_builder.get(p_idx).and_then(|n| n.parent);
406            }
407            true
408        })
409        .copied()
410        .collect();
411    recon_result.layout_roots = final_layout_roots;
412
413    let new_tree = new_tree_builder.build(root_idx);
414    Ok((new_tree, recon_result))
415}
416
417/// Recursively traverses the new DOM and old tree, building a new tree and marking dirty nodes.
418pub fn reconcile_recursive(
419    styled_dom: &StyledDom,
420    new_dom_id: NodeId,
421    old_tree_idx: Option<usize>,
422    new_parent_idx: Option<usize>,
423    old_tree: Option<&LayoutTree>,
424    new_tree_builder: &mut LayoutTreeBuilder,
425    recon: &mut ReconciliationResult,
426    debug_messages: &mut Option<Vec<LayoutDebugMessage>>,
427) -> Result<usize> {
428    let node_data = &styled_dom.node_data.as_container()[new_dom_id];
429
430    let old_node = old_tree.and_then(|t| old_tree_idx.and_then(|idx| t.get(idx)));
431    let new_node_data_hash = hash_styled_node_data(styled_dom, new_dom_id);
432
433    // A node is dirty if it's new, or if its data/style hash has changed.
434
435    let is_dirty = old_node.map_or(true, |n| new_node_data_hash != n.node_data_hash);
436
437    let new_node_idx = if is_dirty {
438        new_tree_builder.create_node_from_dom(
439            styled_dom,
440            new_dom_id,
441            new_parent_idx,
442            debug_messages,
443        )?
444    } else {
445        new_tree_builder.clone_node_from_old(old_node.unwrap(), new_parent_idx)
446    };
447
448    // CRITICAL: For list-items, create a ::marker pseudo-element as the first child
449    // This must be done after the node is created but before processing children
450    // Per CSS Lists Module Level 3, ::marker is generated as the first child of list-items
451    {
452        let node_data = &styled_dom.node_data.as_container()[new_dom_id];
453        let node_state = &styled_dom.styled_nodes.as_container()[new_dom_id].styled_node_state;
454        let cache = &styled_dom.css_property_cache.ptr;
455
456        let display = cache
457            .get_display(node_data, &new_dom_id, node_state)
458            .and_then(|v| v.get_property().copied());
459
460        if matches!(display, Some(LayoutDisplay::ListItem)) {
461            // Create ::marker pseudo-element for this list-item
462            new_tree_builder.create_marker_pseudo_element(styled_dom, new_dom_id, new_node_idx);
463        }
464    }
465
466    // Reconcile children to check for structural changes and build the new tree structure.
467    let new_children_dom_ids: Vec<_> = collect_children_dom_ids(styled_dom, new_dom_id);
468    let old_children_indices: Vec<_> = old_node.map(|n| n.children.clone()).unwrap_or_default();
469
470    let mut children_are_different = new_children_dom_ids.len() != old_children_indices.len();
471    let mut new_child_hashes = Vec::new();
472
473    // CSS 2.2 Section 9.2.1.1: Anonymous Block Boxes
474    // "When an inline box contains an in-flow block-level box, the inline box
475    // (and its inline ancestors within the same line box) are broken around
476    // the block-level box [...], splitting the inline box into two boxes"
477    //
478    // When a block container has mixed block/inline children, we must:
479    // 1. Wrap consecutive inline children in anonymous block boxes
480    // 2. Leave block-level children as direct children
481
482    let has_block_child = new_children_dom_ids
483        .iter()
484        .any(|&id| is_block_level(styled_dom, id));
485
486    if !has_block_child {
487        // All children are inline - no anonymous boxes needed
488        // Simple case: process each child directly
489        for (i, &new_child_dom_id) in new_children_dom_ids.iter().enumerate() {
490            let old_child_idx = old_children_indices.get(i).copied();
491
492            let reconciled_child_idx = reconcile_recursive(
493                styled_dom,
494                new_child_dom_id,
495                old_child_idx,
496                Some(new_node_idx),
497                old_tree,
498                new_tree_builder,
499                recon,
500                debug_messages,
501            )?;
502            if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
503                new_child_hashes.push(child_node.subtree_hash.0);
504            }
505
506            if old_tree.and_then(|t| t.get(old_child_idx?).map(|n| n.subtree_hash))
507                != new_tree_builder
508                    .get(reconciled_child_idx)
509                    .map(|n| n.subtree_hash)
510            {
511                children_are_different = true;
512            }
513        }
514    } else {
515        // Mixed content: block and inline children
516        // We must create anonymous block boxes around consecutive inline runs
517
518        if let Some(msgs) = debug_messages.as_mut() {
519            msgs.push(LayoutDebugMessage::info(format!(
520                "[reconcile_recursive] Mixed content in node {}: creating anonymous IFC wrappers",
521                new_dom_id.index()
522            )));
523        }
524
525        let mut inline_run: Vec<(usize, NodeId)> = Vec::new(); // (dom_child_index, dom_id)
526
527        for (i, &new_child_dom_id) in new_children_dom_ids.iter().enumerate() {
528            if is_block_level(styled_dom, new_child_dom_id) {
529                // End current inline run if any
530                if !inline_run.is_empty() {
531                    // Create anonymous IFC wrapper for the inline run
532                    // This wrapper establishes an Inline Formatting Context
533                    let anon_idx = new_tree_builder.create_anonymous_node(
534                        new_node_idx,
535                        AnonymousBoxType::InlineWrapper,
536                        FormattingContext::Inline, // IFC for inline content
537                    );
538
539                    if let Some(msgs) = debug_messages.as_mut() {
540                        msgs.push(LayoutDebugMessage::info(format!(
541                            "[reconcile_recursive] Created anonymous IFC wrapper (layout_idx={}) for {} inline children: {:?}",
542                            anon_idx,
543                            inline_run.len(),
544                            inline_run.iter().map(|(_, id)| id.index()).collect::<Vec<_>>()
545                        )));
546                    }
547
548                    // Process each inline child under the anonymous wrapper
549                    for (pos, inline_dom_id) in inline_run.drain(..) {
550                        let old_child_idx = old_children_indices.get(pos).copied();
551                        let reconciled_child_idx = reconcile_recursive(
552                            styled_dom,
553                            inline_dom_id,
554                            old_child_idx,
555                            Some(anon_idx), // Parent is the anonymous wrapper
556                            old_tree,
557                            new_tree_builder,
558                            recon,
559                            debug_messages,
560                        )?;
561                        if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
562                            new_child_hashes.push(child_node.subtree_hash.0);
563                        }
564                    }
565
566                    // Mark anonymous wrapper as dirty for layout
567                    recon.intrinsic_dirty.insert(anon_idx);
568                    children_are_different = true;
569                }
570
571                // Process block-level child directly under parent
572                let old_child_idx = old_children_indices.get(i).copied();
573                let reconciled_child_idx = reconcile_recursive(
574                    styled_dom,
575                    new_child_dom_id,
576                    old_child_idx,
577                    Some(new_node_idx),
578                    old_tree,
579                    new_tree_builder,
580                    recon,
581                    debug_messages,
582                )?;
583                if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
584                    new_child_hashes.push(child_node.subtree_hash.0);
585                }
586
587                if old_tree.and_then(|t| t.get(old_child_idx?).map(|n| n.subtree_hash))
588                    != new_tree_builder
589                        .get(reconciled_child_idx)
590                        .map(|n| n.subtree_hash)
591                {
592                    children_are_different = true;
593                }
594            } else {
595                // Inline-level child - add to current run
596                inline_run.push((i, new_child_dom_id));
597            }
598        }
599
600        // Process any remaining inline run at the end
601        if !inline_run.is_empty() {
602            let anon_idx = new_tree_builder.create_anonymous_node(
603                new_node_idx,
604                AnonymousBoxType::InlineWrapper,
605                FormattingContext::Inline, // IFC for inline content
606            );
607
608            if let Some(msgs) = debug_messages.as_mut() {
609                msgs.push(LayoutDebugMessage::info(format!(
610                    "[reconcile_recursive] Created trailing anonymous IFC wrapper (layout_idx={}) for {} inline children: {:?}",
611                    anon_idx,
612                    inline_run.len(),
613                    inline_run.iter().map(|(_, id)| id.index()).collect::<Vec<_>>()
614                )));
615            }
616
617            for (pos, inline_dom_id) in inline_run.drain(..) {
618                let old_child_idx = old_children_indices.get(pos).copied();
619                let reconciled_child_idx = reconcile_recursive(
620                    styled_dom,
621                    inline_dom_id,
622                    old_child_idx,
623                    Some(anon_idx),
624                    old_tree,
625                    new_tree_builder,
626                    recon,
627                    debug_messages,
628                )?;
629                if let Some(child_node) = new_tree_builder.get(reconciled_child_idx) {
630                    new_child_hashes.push(child_node.subtree_hash.0);
631                }
632            }
633
634            recon.intrinsic_dirty.insert(anon_idx);
635            children_are_different = true;
636        }
637    }
638
639    // After reconciling children, calculate this node's full subtree hash.
640    let final_subtree_hash = calculate_subtree_hash(new_node_data_hash, &new_child_hashes);
641    if let Some(current_node) = new_tree_builder.get_mut(new_node_idx) {
642        current_node.subtree_hash = final_subtree_hash;
643    }
644
645    // If the node itself was dirty, or its children's structure changed, it's a layout boundary.
646    if is_dirty || children_are_different {
647        recon.intrinsic_dirty.insert(new_node_idx);
648        recon.layout_roots.insert(new_node_idx);
649    }
650
651    Ok(new_node_idx)
652}
653
654/// Result of `prepare_layout_context`: contains the layout constraints and
655/// intermediate values needed for `calculate_layout_for_subtree`.
656struct PreparedLayoutContext<'a> {
657    constraints: LayoutConstraints<'a>,
658    /// DOM ID for the node. None for anonymous boxes.
659    dom_id: Option<NodeId>,
660    writing_mode: LayoutWritingMode,
661    final_used_size: LogicalSize,
662    box_props: crate::solver3::geometry::BoxProps,
663}
664
665/// Prepares the layout context for a single node by calculating its used size
666/// and building the layout constraints for its children.
667///
668/// For anonymous boxes (no dom_node_id), we use default values and inherit
669/// from the containing block.
670fn prepare_layout_context<'a, T: ParsedFontTrait>(
671    ctx: &LayoutContext<'a, T>,
672    node: &LayoutNode,
673    containing_block_size: LogicalSize,
674) -> Result<PreparedLayoutContext<'a>> {
675    let dom_id = node.dom_node_id; // Can be None for anonymous boxes
676
677    // Phase 1: Calculate this node's provisional used size
678
679    // This size is based on the node's CSS properties (width, height, etc.) and
680    // its containing block. If height is 'auto', this is a temporary value.
681    let intrinsic = node.intrinsic_sizes.clone().unwrap_or_default();
682    let final_used_size = calculate_used_size_for_node(
683        ctx.styled_dom,
684        dom_id, // Now Option<NodeId>
685        containing_block_size,
686        intrinsic,
687        &node.box_props,
688    )?;
689
690    // Phase 2: Layout children using a formatting context
691
692    // Fetch the writing mode for the current context.
693    // For anonymous boxes, use default values
694    let styled_node_state = dom_id
695        .and_then(|id| ctx.styled_dom.styled_nodes.as_container().get(id).cloned())
696        .map(|n| n.styled_node_state)
697        .unwrap_or_default();
698
699    // This should come from the node's style. For anonymous boxes, use defaults.
700    let writing_mode = match dom_id {
701        Some(id) => get_writing_mode(ctx.styled_dom, id, &styled_node_state).unwrap_or_default(),
702        None => LayoutWritingMode::default(),
703    };
704    let text_align = match dom_id {
705        Some(id) => get_text_align(ctx.styled_dom, id, &styled_node_state).unwrap_or_default(),
706        None => StyleTextAlign::default(),
707    };
708
709    // IMPORTANT: For the available_size that we pass to children, we need to use
710    // the containing_block_size if the current node's height is 'auto'.
711    // Otherwise, we would pass 0 as available height to children, which breaks
712    // table layout and other auto-height containers.
713    // For anonymous boxes, assume 'auto' height behavior.
714    let css_height: MultiValue<LayoutHeight> = match dom_id {
715        Some(id) => get_css_height(ctx.styled_dom, id, &styled_node_state),
716        None => MultiValue::Auto, // Anonymous boxes have auto height
717    };
718
719    // Get display type to determine sizing behavior
720    let display = match dom_id {
721        Some(id) => get_display_property(ctx.styled_dom, Some(id)),
722        None => MultiValue::Auto, // Anonymous boxes behave like blocks
723    };
724
725    let available_size_for_children = if should_use_content_height(&css_height) {
726        // Height is auto - use containing block size as available size
727        let inner_size = node.box_props.inner_size(final_used_size, writing_mode);
728
729        // For inline elements (display: inline), the available width comes from
730        // the containing block, not from the element's own intrinsic size.
731        // CSS 2.2 § 10.3.1: Inline, non-replaced elements use containing block width.
732        // The containing_block_size already has parent's padding subtracted when
733        // passed from the parent's layout (via inner_size calculation).
734        let available_width = match display {
735            MultiValue::Exact(LayoutDisplay::Inline) | MultiValue::Auto => {
736                // Inline elements flow within the containing block
737                containing_block_size.width
738            }
739            _ => {
740                // Block-level elements use their own content-box
741                inner_size.width
742            }
743        };
744
745        LogicalSize {
746            width: available_width,
747            // Use containing block height!
748            height: containing_block_size.height,
749        }
750    } else {
751        // Height is explicit - use inner size (after padding/border)
752        node.box_props.inner_size(final_used_size, writing_mode)
753    };
754
755    // Proactively reserve space for scrollbars based on overflow properties.
756    // If overflow-y is auto/scroll, we must reduce available width for children
757    // to ensure they don't overlap with the scrollbar.
758    // This is done BEFORE layout so children are sized correctly from the start.
759    let scrollbar_reservation = match dom_id {
760        Some(id) => {
761            let styled_node_state = ctx
762                .styled_dom
763                .styled_nodes
764                .as_container()
765                .get(id)
766                .map(|s| s.styled_node_state.clone())
767                .unwrap_or_default();
768            let overflow_y = get_overflow_y(ctx.styled_dom, id, &styled_node_state);
769            use azul_css::props::layout::LayoutOverflow;
770            match overflow_y.unwrap_or_default() {
771                LayoutOverflow::Scroll | LayoutOverflow::Auto => fc::SCROLLBAR_WIDTH_PX,
772                _ => 0.0,
773            }
774        }
775        None => 0.0,
776    };
777
778    // Reduce available width by scrollbar reservation (if any)
779    let available_size_for_children = if scrollbar_reservation > 0.0 {
780        LogicalSize {
781            width: (available_size_for_children.width - scrollbar_reservation).max(0.0),
782            height: available_size_for_children.height,
783        }
784    } else {
785        available_size_for_children
786    };
787
788    let constraints = LayoutConstraints {
789        available_size: available_size_for_children,
790        bfc_state: None,
791        writing_mode,
792        text_align: style_text_align_to_fc(text_align),
793        containing_block_size,
794        available_width_type: Text3AvailableSpace::Definite(available_size_for_children.width),
795    };
796
797    Ok(PreparedLayoutContext {
798        constraints,
799        dom_id,
800        writing_mode,
801        final_used_size,
802        box_props: node.box_props.clone(),
803    })
804}
805
806/// Determines scrollbar requirements for a node based on content overflow.
807///
808/// Checks if scrollbars are needed by comparing content size against available space.
809/// For paged media (PDF), scrollbars are never added since they don't exist in print.
810/// Returns the computed ScrollbarRequirements with horizontal/vertical needs and dimensions.
811fn compute_scrollbar_info<T: ParsedFontTrait>(
812    ctx: &LayoutContext<'_, T>,
813    dom_id: NodeId,
814    styled_node_state: &azul_core::styled_dom::StyledNodeState,
815    content_size: LogicalSize,
816    box_props: &crate::solver3::geometry::BoxProps,
817    final_used_size: LogicalSize,
818    writing_mode: LayoutWritingMode,
819) -> ScrollbarRequirements {
820    // Skip scrollbar handling for paged media (PDF)
821    if ctx.fragmentation_context.is_some() {
822        return ScrollbarRequirements {
823            needs_horizontal: false,
824            needs_vertical: false,
825            scrollbar_width: 0.0,
826            scrollbar_height: 0.0,
827        };
828    }
829
830    let overflow_x = get_overflow_x(ctx.styled_dom, dom_id, styled_node_state);
831    let overflow_y = get_overflow_y(ctx.styled_dom, dom_id, styled_node_state);
832
833    let container_size = box_props.inner_size(final_used_size, writing_mode);
834
835    fc::check_scrollbar_necessity(
836        content_size,
837        container_size,
838        to_overflow_behavior(overflow_x),
839        to_overflow_behavior(overflow_y),
840    )
841}
842
843/// Checks if scrollbars changed compared to previous layout and if reflow is needed.
844///
845/// To prevent oscillation, we only trigger reflow when scrollbars are *added*,
846/// never when they would be *removed*. This is because:
847/// 1. Adding scrollbars reduces available space → content reflows → may fit
848/// 2. Removing scrollbars increases space → content reflows → may overflow again
849/// This creates an infinite loop. By only allowing transitions *to* scrollbars,
850/// we reach a stable state where scrollbars are present if ever needed.
851fn check_scrollbar_change(
852    tree: &LayoutTree,
853    node_index: usize,
854    scrollbar_info: &ScrollbarRequirements,
855    skip_scrollbar_check: bool,
856) -> bool {
857    if skip_scrollbar_check {
858        return false;
859    }
860
861    let Some(current_node) = tree.get(node_index) else {
862        return false;
863    };
864
865    match &current_node.scrollbar_info {
866        None => scrollbar_info.needs_reflow(),
867        Some(old_info) => {
868            // Only trigger reflow if scrollbars are being ADDED, not removed
869            let adding_horizontal = !old_info.needs_horizontal && scrollbar_info.needs_horizontal;
870            let adding_vertical = !old_info.needs_vertical && scrollbar_info.needs_vertical;
871            adding_horizontal || adding_vertical
872        }
873    }
874}
875
876/// Merges new scrollbar info with existing info, keeping scrollbars once needed.
877///
878/// This prevents the oscillation problem where content reflows to fit without
879/// scrollbars, but then overflows again when scrollbars are removed.
880fn merge_scrollbar_info(
881    tree: &LayoutTree,
882    node_index: usize,
883    new_info: &ScrollbarRequirements,
884) -> ScrollbarRequirements {
885    let Some(current_node) = tree.get(node_index) else {
886        return new_info.clone();
887    };
888
889    match &current_node.scrollbar_info {
890        Some(old) => ScrollbarRequirements {
891            needs_horizontal: old.needs_horizontal || new_info.needs_horizontal,
892            needs_vertical: old.needs_vertical || new_info.needs_vertical,
893            scrollbar_width: if old.needs_vertical || new_info.needs_vertical {
894                16.0
895            } else {
896                0.0
897            },
898            scrollbar_height: if old.needs_horizontal || new_info.needs_horizontal {
899                16.0
900            } else {
901                0.0
902            },
903        },
904        None => new_info.clone(),
905    }
906}
907
908/// Calculates the content-box position from a margin-box position.
909///
910/// The content-box is offset from the margin-box by border + padding.
911/// Margin is NOT added here because containing_block_pos already accounts for it.
912fn calculate_content_box_pos(
913    containing_block_pos: LogicalPosition,
914    box_props: &crate::solver3::geometry::BoxProps,
915) -> LogicalPosition {
916    LogicalPosition::new(
917        containing_block_pos.x + box_props.border.left + box_props.padding.left,
918        containing_block_pos.y + box_props.border.top + box_props.padding.top,
919    )
920}
921
922/// Emits debug logging for content-box calculation if debug messages are enabled.
923fn log_content_box_calculation<T: ParsedFontTrait>(
924    ctx: &mut LayoutContext<'_, T>,
925    node_index: usize,
926    current_node: &LayoutNode,
927    containing_block_pos: LogicalPosition,
928    self_content_box_pos: LogicalPosition,
929) {
930    let Some(debug_msgs) = ctx.debug_messages.as_mut() else {
931        return;
932    };
933
934    let dom_name = current_node
935        .dom_node_id
936        .and_then(|id| {
937            ctx.styled_dom
938                .node_data
939                .as_container()
940                .internal
941                .get(id.index())
942        })
943        .map(|n| format!("{:?}", n.node_type))
944        .unwrap_or_else(|| "Unknown".to_string());
945
946    debug_msgs.push(LayoutDebugMessage::new(
947        LayoutDebugMessageType::PositionCalculation,
948        format!(
949            "[CONTENT BOX {}] {} - margin-box pos=({:.2}, {:.2}) + border=({:.2},{:.2}) + \
950             padding=({:.2},{:.2}) = content-box pos=({:.2}, {:.2})",
951            node_index,
952            dom_name,
953            containing_block_pos.x,
954            containing_block_pos.y,
955            current_node.box_props.border.left,
956            current_node.box_props.border.top,
957            current_node.box_props.padding.left,
958            current_node.box_props.padding.top,
959            self_content_box_pos.x,
960            self_content_box_pos.y
961        ),
962    ));
963}
964
965/// Emits debug logging for child positioning if debug messages are enabled.
966fn log_child_positioning<T: ParsedFontTrait>(
967    ctx: &mut LayoutContext<'_, T>,
968    child_index: usize,
969    child_node: &LayoutNode,
970    self_content_box_pos: LogicalPosition,
971    child_relative_pos: LogicalPosition,
972    child_absolute_pos: LogicalPosition,
973) {
974    let Some(debug_msgs) = ctx.debug_messages.as_mut() else {
975        return;
976    };
977
978    let child_dom_name = child_node
979        .dom_node_id
980        .and_then(|id| {
981            ctx.styled_dom
982                .node_data
983                .as_container()
984                .internal
985                .get(id.index())
986        })
987        .map(|n| format!("{:?}", n.node_type))
988        .unwrap_or_else(|| "Unknown".to_string());
989
990    debug_msgs.push(LayoutDebugMessage::new(
991        LayoutDebugMessageType::PositionCalculation,
992        format!(
993            "[CHILD POS {}] {} - parent content-box=({:.2}, {:.2}) + relative=({:.2}, {:.2}) + \
994             margin=({:.2}, {:.2}) = absolute=({:.2}, {:.2})",
995            child_index,
996            child_dom_name,
997            self_content_box_pos.x,
998            self_content_box_pos.y,
999            child_relative_pos.x,
1000            child_relative_pos.y,
1001            child_node.box_props.margin.left,
1002            child_node.box_props.margin.top,
1003            child_absolute_pos.x,
1004            child_absolute_pos.y
1005        ),
1006    ));
1007}
1008
1009/// Processes a single in-flow child: sets position and recurses.
1010///
1011/// For Flex/Grid containers, Taffy has already laid out the children completely.
1012/// We only recurse to position their grandchildren.
1013/// For other formatting contexts (Block, Inline, Table), we do full recursive layout.
1014fn process_inflow_child<T: ParsedFontTrait>(
1015    ctx: &mut LayoutContext<'_, T>,
1016    tree: &mut LayoutTree,
1017    text_cache: &mut TextLayoutCache,
1018    child_index: usize,
1019    child_relative_pos: LogicalPosition,
1020    self_content_box_pos: LogicalPosition,
1021    inner_size_after_scrollbars: LogicalSize,
1022    writing_mode: LayoutWritingMode,
1023    is_flex_or_grid: bool,
1024    calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
1025    reflow_needed_for_scrollbars: &mut bool,
1026    float_cache: &mut BTreeMap<usize, fc::FloatingContext>,
1027) -> Result<()> {
1028    // Set relative position on child
1029    let child_node = tree.get_mut(child_index).ok_or(LayoutError::InvalidTree)?;
1030    child_node.relative_position = Some(child_relative_pos);
1031
1032    // Calculate absolute position
1033    let child_absolute_pos = LogicalPosition::new(
1034        self_content_box_pos.x + child_relative_pos.x,
1035        self_content_box_pos.y + child_relative_pos.y,
1036    );
1037
1038    // Debug logging
1039    {
1040        let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1041        log_child_positioning(
1042            ctx,
1043            child_index,
1044            child_node,
1045            self_content_box_pos,
1046            child_relative_pos,
1047            child_absolute_pos,
1048        );
1049    }
1050
1051    calculated_positions.insert(child_index, child_absolute_pos);
1052
1053    // Recurse based on parent's formatting context
1054    if is_flex_or_grid {
1055        // For Flex/Grid: Taffy already set used_size. Only recurse for grandchildren.
1056        let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1057        let child_content_box_pos =
1058            calculate_content_box_pos(child_absolute_pos, &child_node.box_props);
1059        let child_inner_size = child_node
1060            .box_props
1061            .inner_size(child_node.used_size.unwrap_or_default(), writing_mode);
1062
1063        position_flex_child_descendants(
1064            ctx,
1065            tree,
1066            text_cache,
1067            child_index,
1068            child_content_box_pos,
1069            child_inner_size,
1070            calculated_positions,
1071            reflow_needed_for_scrollbars,
1072            float_cache,
1073        )?;
1074    } else {
1075        // For Block/Inline/Table: do full recursive layout
1076        calculate_layout_for_subtree(
1077            ctx,
1078            tree,
1079            text_cache,
1080            child_index,
1081            child_absolute_pos,
1082            inner_size_after_scrollbars,
1083            calculated_positions,
1084            reflow_needed_for_scrollbars,
1085            float_cache,
1086        )?;
1087    }
1088
1089    Ok(())
1090}
1091
1092/// Processes out-of-flow children (absolute/fixed positioned elements).
1093///
1094/// Out-of-flow elements don't appear in layout_output.positions but still need
1095/// a static position for when no explicit offsets are specified. This sets their
1096/// static position to the parent's content-box origin.
1097fn process_out_of_flow_children<T: ParsedFontTrait>(
1098    ctx: &mut LayoutContext<'_, T>,
1099    tree: &mut LayoutTree,
1100    text_cache: &mut TextLayoutCache,
1101    node_index: usize,
1102    self_content_box_pos: LogicalPosition,
1103    calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
1104) -> Result<()> {
1105    // Collect out-of-flow children (those not already positioned)
1106    let out_of_flow_children: Vec<(usize, Option<NodeId>)> = {
1107        let current_node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1108        current_node
1109            .children
1110            .iter()
1111            .filter_map(|&child_index| {
1112                if calculated_positions.contains_key(&child_index) {
1113                    return None;
1114                }
1115                let child = tree.get(child_index)?;
1116                Some((child_index, child.dom_node_id))
1117            })
1118            .collect()
1119    };
1120
1121    for (child_index, child_dom_id_opt) in out_of_flow_children {
1122        let Some(child_dom_id) = child_dom_id_opt else {
1123            continue;
1124        };
1125
1126        let position_type = get_position_type(ctx.styled_dom, Some(child_dom_id));
1127        if position_type != LayoutPosition::Absolute && position_type != LayoutPosition::Fixed {
1128            continue;
1129        }
1130
1131        // Set static position to parent's content-box origin
1132        calculated_positions.insert(child_index, self_content_box_pos);
1133
1134        // Recursively set static positions for nested out-of-flow descendants
1135        set_static_positions_recursive(
1136            ctx,
1137            tree,
1138            text_cache,
1139            child_index,
1140            self_content_box_pos,
1141            calculated_positions,
1142        )?;
1143    }
1144
1145    Ok(())
1146}
1147
1148/// Recursive, top-down pass to calculate used sizes and positions for a given subtree.
1149/// This is the single, authoritative function for in-flow layout.
1150pub fn calculate_layout_for_subtree<T: ParsedFontTrait>(
1151    ctx: &mut LayoutContext<'_, T>,
1152    tree: &mut LayoutTree,
1153    text_cache: &mut TextLayoutCache,
1154    node_index: usize,
1155    containing_block_pos: LogicalPosition,
1156    containing_block_size: LogicalSize,
1157    calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
1158    reflow_needed_for_scrollbars: &mut bool,
1159    float_cache: &mut BTreeMap<usize, fc::FloatingContext>,
1160) -> Result<()> {
1161    // Phase 1: Prepare layout context (calculate used size, constraints)
1162    let PreparedLayoutContext {
1163        constraints,
1164        dom_id,
1165        writing_mode,
1166        mut final_used_size,
1167        box_props,
1168    } = {
1169        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1170        prepare_layout_context(ctx, node, containing_block_size)?
1171    };
1172
1173    // Phase 2: Layout children using the formatting context
1174    let layout_result =
1175        layout_formatting_context(ctx, tree, text_cache, node_index, &constraints, float_cache)?;
1176    let content_size = layout_result.output.overflow_size;
1177
1178    // Phase 2.5: Resolve 'auto' main-axis size based on content
1179    // For anonymous boxes, use default styled node state
1180    let styled_node_state = dom_id
1181        .and_then(|id| ctx.styled_dom.styled_nodes.as_container().get(id).cloned())
1182        .map(|n| n.styled_node_state)
1183        .unwrap_or_default();
1184
1185    let css_height: MultiValue<LayoutHeight> = match dom_id {
1186        Some(id) => get_css_height(ctx.styled_dom, id, &styled_node_state),
1187        None => MultiValue::Auto, // Anonymous boxes have auto height
1188    };
1189    if should_use_content_height(&css_height) {
1190        final_used_size = apply_content_based_height(
1191            final_used_size,
1192            content_size,
1193            tree,
1194            node_index,
1195            writing_mode,
1196        );
1197    }
1198
1199    // Phase 3: Scrollbar handling
1200    // Anonymous boxes don't have scrollbars
1201    let skip_scrollbar_check = ctx.fragmentation_context.is_some();
1202    let scrollbar_info = match dom_id {
1203        Some(id) => compute_scrollbar_info(
1204            ctx,
1205            id,
1206            &styled_node_state,
1207            content_size,
1208            &box_props,
1209            final_used_size,
1210            writing_mode,
1211        ),
1212        None => ScrollbarRequirements::default(),
1213    };
1214
1215    if check_scrollbar_change(tree, node_index, &scrollbar_info, skip_scrollbar_check) {
1216        *reflow_needed_for_scrollbars = true;
1217    }
1218
1219    let merged_scrollbar_info = merge_scrollbar_info(tree, node_index, &scrollbar_info);
1220    let content_box_size = box_props.inner_size(final_used_size, writing_mode);
1221    let inner_size_after_scrollbars = merged_scrollbar_info.shrink_size(content_box_size);
1222
1223    // Phase 4: Update this node's state
1224    let self_content_box_pos = {
1225        let current_node = tree.get_mut(node_index).ok_or(LayoutError::InvalidTree)?;
1226
1227        // Table cells get their size from the table layout algorithm, don't overwrite
1228        let is_table_cell = matches!(
1229            current_node.formatting_context,
1230            FormattingContext::TableCell
1231        );
1232        if !is_table_cell || current_node.used_size.is_none() {
1233            current_node.used_size = Some(final_used_size);
1234        }
1235        current_node.scrollbar_info = Some(merged_scrollbar_info);
1236        // Store overflow content size for scroll frame calculation
1237        current_node.overflow_content_size = Some(content_size);
1238
1239        let pos = calculate_content_box_pos(containing_block_pos, &current_node.box_props);
1240        log_content_box_calculation(ctx, node_index, current_node, containing_block_pos, pos);
1241        pos
1242    };
1243
1244    // Phase 5: Determine formatting context type
1245    let is_flex_or_grid = {
1246        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1247        matches!(
1248            node.formatting_context,
1249            FormattingContext::Flex | FormattingContext::Grid
1250        )
1251    };
1252
1253    // Phase 6: Process in-flow children
1254    let positions: Vec<_> = layout_result
1255        .output
1256        .positions
1257        .iter()
1258        .map(|(&idx, &pos)| (idx, pos))
1259        .collect();
1260
1261    for (child_index, child_relative_pos) in positions {
1262        process_inflow_child(
1263            ctx,
1264            tree,
1265            text_cache,
1266            child_index,
1267            child_relative_pos,
1268            self_content_box_pos,
1269            inner_size_after_scrollbars,
1270            writing_mode,
1271            is_flex_or_grid,
1272            calculated_positions,
1273            reflow_needed_for_scrollbars,
1274            float_cache,
1275        )?;
1276    }
1277
1278    // Phase 7: Process out-of-flow children (absolute/fixed)
1279    process_out_of_flow_children(
1280        ctx,
1281        tree,
1282        text_cache,
1283        node_index,
1284        self_content_box_pos,
1285        calculated_positions,
1286    )?;
1287
1288    Ok(())
1289}
1290
1291/// Recursively set static positions for out-of-flow descendants without doing layout
1292/// Recursively positions descendants of Flex/Grid children.
1293///
1294/// When a Flex container lays out its children via Taffy, the children have their
1295/// used_size and relative_position set, but their GRANDCHILDREN don't have positions
1296/// in calculated_positions yet. This function traverses down the tree and positions
1297/// all descendants properly.
1298fn position_flex_child_descendants<T: ParsedFontTrait>(
1299    ctx: &mut LayoutContext<'_, T>,
1300    tree: &mut LayoutTree,
1301    text_cache: &mut TextLayoutCache,
1302    node_index: usize,
1303    content_box_pos: LogicalPosition,
1304    available_size: LogicalSize,
1305    calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
1306    reflow_needed_for_scrollbars: &mut bool,
1307    float_cache: &mut BTreeMap<usize, fc::FloatingContext>,
1308) -> Result<()> {
1309    let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1310    let children: Vec<usize> = node.children.clone();
1311    let fc = node.formatting_context.clone();
1312
1313    // If this node is itself a Flex/Grid container, its children were laid out by Taffy
1314    // and already have relative_position set. We just need to convert to absolute and recurse.
1315    if matches!(fc, FormattingContext::Flex | FormattingContext::Grid) {
1316        for &child_index in &children {
1317            let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1318            let child_rel_pos = child_node.relative_position.unwrap_or_default();
1319            let child_abs_pos = LogicalPosition::new(
1320                content_box_pos.x + child_rel_pos.x,
1321                content_box_pos.y + child_rel_pos.y,
1322            );
1323
1324            // Insert position
1325            calculated_positions.insert(child_index, child_abs_pos);
1326
1327            // Get child's content box for recursion
1328            let child_content_box = LogicalPosition::new(
1329                child_abs_pos.x
1330                    + child_node.box_props.border.left
1331                    + child_node.box_props.padding.left,
1332                child_abs_pos.y
1333                    + child_node.box_props.border.top
1334                    + child_node.box_props.padding.top,
1335            );
1336            let child_inner_size = child_node.box_props.inner_size(
1337                child_node.used_size.unwrap_or_default(),
1338                LayoutWritingMode::HorizontalTb,
1339            );
1340
1341            // Recurse
1342            position_flex_child_descendants(
1343                ctx,
1344                tree,
1345                text_cache,
1346                child_index,
1347                child_content_box,
1348                child_inner_size,
1349                calculated_positions,
1350                reflow_needed_for_scrollbars,
1351                float_cache,
1352            )?;
1353        }
1354    } else {
1355        // For Block/Inline/Table children, their descendants need proper layout calculation
1356        // Use the output.positions from their own layout
1357        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1358        let children: Vec<usize> = node.children.clone();
1359
1360        for &child_index in &children {
1361            let child_node = tree.get(child_index).ok_or(LayoutError::InvalidTree)?;
1362            let child_rel_pos = child_node.relative_position.unwrap_or_default();
1363            let child_abs_pos = LogicalPosition::new(
1364                content_box_pos.x + child_rel_pos.x,
1365                content_box_pos.y + child_rel_pos.y,
1366            );
1367
1368            // Insert position
1369            calculated_positions.insert(child_index, child_abs_pos);
1370
1371            // Get child's content box for recursion
1372            let child_content_box = LogicalPosition::new(
1373                child_abs_pos.x
1374                    + child_node.box_props.border.left
1375                    + child_node.box_props.padding.left,
1376                child_abs_pos.y
1377                    + child_node.box_props.border.top
1378                    + child_node.box_props.padding.top,
1379            );
1380            let child_inner_size = child_node.box_props.inner_size(
1381                child_node.used_size.unwrap_or_default(),
1382                LayoutWritingMode::HorizontalTb,
1383            );
1384
1385            // Recurse
1386            position_flex_child_descendants(
1387                ctx,
1388                tree,
1389                text_cache,
1390                child_index,
1391                child_content_box,
1392                child_inner_size,
1393                calculated_positions,
1394                reflow_needed_for_scrollbars,
1395                float_cache,
1396            )?;
1397        }
1398    }
1399
1400    Ok(())
1401}
1402
1403fn set_static_positions_recursive<T: ParsedFontTrait>(
1404    ctx: &mut LayoutContext<'_, T>,
1405    tree: &mut LayoutTree,
1406    _text_cache: &mut TextLayoutCache,
1407    node_index: usize,
1408    parent_content_box_pos: LogicalPosition,
1409    calculated_positions: &mut BTreeMap<usize, LogicalPosition>,
1410) -> Result<()> {
1411    let out_of_flow_children: Vec<(usize, Option<NodeId>)> = {
1412        let node = tree.get(node_index).ok_or(LayoutError::InvalidTree)?;
1413        node.children
1414            .iter()
1415            .filter_map(|&child_index| {
1416                if calculated_positions.contains_key(&child_index) {
1417                    None
1418                } else {
1419                    let child = tree.get(child_index)?;
1420                    Some((child_index, child.dom_node_id))
1421                }
1422            })
1423            .collect()
1424    };
1425
1426    for (child_index, child_dom_id_opt) in out_of_flow_children {
1427        if let Some(child_dom_id) = child_dom_id_opt {
1428            let position_type = get_position_type(ctx.styled_dom, Some(child_dom_id));
1429            if position_type == LayoutPosition::Absolute || position_type == LayoutPosition::Fixed {
1430                calculated_positions.insert(child_index, parent_content_box_pos);
1431
1432                // Continue recursively
1433                set_static_positions_recursive(
1434                    ctx,
1435                    tree,
1436                    _text_cache,
1437                    child_index,
1438                    parent_content_box_pos,
1439                    calculated_positions,
1440                )?;
1441            }
1442        }
1443    }
1444
1445    Ok(())
1446}
1447
1448/// Checks if the given CSS height value should use content-based sizing
1449fn should_use_content_height(css_height: &MultiValue<LayoutHeight>) -> bool {
1450    match css_height {
1451        MultiValue::Auto | MultiValue::Initial | MultiValue::Inherit => {
1452            // Auto/Initial/Inherit height should use content-based sizing
1453            true
1454        }
1455        MultiValue::Exact(height) => match height {
1456            LayoutHeight::Auto => {
1457                // Auto height should use content-based sizing
1458                true
1459            }
1460            LayoutHeight::Px(px) => {
1461                // Check if it's zero or if it has explicit value
1462                // If it's a percentage or em, it's not auto
1463                use azul_css::props::basic::{pixel::PixelValue, SizeMetric};
1464                px == &PixelValue::zero()
1465                    || (px.metric != SizeMetric::Px
1466                        && px.metric != SizeMetric::Percent
1467                        && px.metric != SizeMetric::Em
1468                        && px.metric != SizeMetric::Rem)
1469            }
1470            LayoutHeight::MinContent | LayoutHeight::MaxContent => {
1471                // These are content-based, so they should use the content size
1472                true
1473            }
1474        },
1475    }
1476}
1477
1478/// Applies content-based height sizing to a node
1479///
1480/// **Note**: This function respects min-height/max-height constraints from Phase 1.
1481///
1482/// According to CSS 2.2 § 10.7, when height is 'auto', the final height must be
1483/// max(min_height, min(content_height, max_height)).
1484///
1485/// The `used_size` parameter already contains the size constrained by
1486/// min-height/max-height from the initial sizing pass. We must take the
1487/// maximum of this constrained size and the new content-based size to ensure
1488/// min-height is not lost.
1489fn apply_content_based_height(
1490    mut used_size: LogicalSize,
1491    content_size: LogicalSize,
1492    tree: &LayoutTree,
1493    node_index: usize,
1494    writing_mode: LayoutWritingMode,
1495) -> LogicalSize {
1496    let node_props = &tree.get(node_index).unwrap().box_props;
1497    let main_axis_padding_border =
1498        node_props.padding.main_sum(writing_mode) + node_props.border.main_sum(writing_mode);
1499
1500    // CRITICAL: 'old_main_size' holds the size constrained by min-height/max-height from Phase 1
1501    let old_main_size = used_size.main(writing_mode);
1502    let new_main_size = content_size.main(writing_mode) + main_axis_padding_border;
1503
1504    // Final size = max(min_height_constrained_size, content_size)
1505    // This ensures that min-height is respected even when content is smaller
1506    let final_main_size = old_main_size.max(new_main_size);
1507
1508    used_size = used_size.with_main(writing_mode, final_main_size);
1509
1510    used_size
1511}
1512
1513fn hash_styled_node_data(dom: &StyledDom, node_id: NodeId) -> u64 {
1514    let mut hasher = DefaultHasher::new();
1515    if let Some(styled_node) = dom.styled_nodes.as_container().get(node_id) {
1516        styled_node.styled_node_state.hash(&mut hasher);
1517    }
1518    if let Some(node_data) = dom.node_data.as_container().get(node_id) {
1519        node_data.get_node_type().hash(&mut hasher);
1520    }
1521    hasher.finish()
1522}
1523
1524fn calculate_subtree_hash(node_self_hash: u64, child_hashes: &[u64]) -> SubtreeHash {
1525    let mut hasher = DefaultHasher::new();
1526    node_self_hash.hash(&mut hasher);
1527    child_hashes.hash(&mut hasher);
1528    SubtreeHash(hasher.finish())
1529}
1530
1531/// Computes CSS counter values for all nodes in the layout tree.
1532///
1533/// This function traverses the tree in document order and processes counter-reset
1534/// and counter-increment properties. The computed values are stored in cache.counters.
1535///
1536/// CSS counters work with a stack-based scoping model:
1537/// - `counter-reset` creates a new scope and sets the counter to a value
1538/// - `counter-increment` increments the counter in the current scope
1539/// - When leaving a subtree, counter scopes are popped
1540pub fn compute_counters(
1541    styled_dom: &StyledDom,
1542    tree: &LayoutTree,
1543    counters: &mut BTreeMap<(usize, String), i32>,
1544) {
1545    use std::collections::HashMap;
1546
1547    // Track counter stacks: counter_name -> Vec<value>
1548    // Each entry in the Vec represents a nested scope
1549    let mut counter_stacks: HashMap<String, Vec<i32>> = HashMap::new();
1550
1551    // Stack to track which counters were reset at each tree level
1552    // When we pop back up the tree, we need to pop these counter scopes
1553    let mut scope_stack: Vec<Vec<String>> = Vec::new();
1554
1555    compute_counters_recursive(
1556        styled_dom,
1557        tree,
1558        tree.root,
1559        counters,
1560        &mut counter_stacks,
1561        &mut scope_stack,
1562    );
1563}
1564
1565fn compute_counters_recursive(
1566    styled_dom: &StyledDom,
1567    tree: &LayoutTree,
1568    node_idx: usize,
1569    counters: &mut BTreeMap<(usize, String), i32>,
1570    counter_stacks: &mut std::collections::HashMap<String, Vec<i32>>,
1571    scope_stack: &mut Vec<Vec<String>>,
1572) {
1573    let node = match tree.get(node_idx) {
1574        Some(n) => n,
1575        None => return,
1576    };
1577
1578    // Skip pseudo-elements (::marker, ::before, ::after) for counter processing
1579    // Pseudo-elements inherit counter values from their parent element
1580    // but don't participate in counter-reset or counter-increment themselves
1581    if node.pseudo_element.is_some() {
1582        // Store the parent's counter values for this pseudo-element
1583        // so it can be looked up during marker text generation
1584        if let Some(parent_idx) = node.parent {
1585            // Copy all counter values from parent to this pseudo-element
1586            let parent_counters: Vec<_> = counters
1587                .iter()
1588                .filter(|((idx, _), _)| *idx == parent_idx)
1589                .map(|((_, name), &value)| (name.clone(), value))
1590                .collect();
1591
1592            for (counter_name, value) in parent_counters {
1593                counters.insert((node_idx, counter_name), value);
1594            }
1595        }
1596
1597        // Don't recurse to children of pseudo-elements
1598        // (pseudo-elements shouldn't have children in normal circumstances)
1599        return;
1600    }
1601
1602    // Only process real DOM nodes, not anonymous boxes
1603    let dom_id = match node.dom_node_id {
1604        Some(id) => id,
1605        None => {
1606            // For anonymous boxes, just recurse to children
1607            for &child_idx in &node.children {
1608                compute_counters_recursive(
1609                    styled_dom,
1610                    tree,
1611                    child_idx,
1612                    counters,
1613                    counter_stacks,
1614                    scope_stack,
1615                );
1616            }
1617            return;
1618        }
1619    };
1620
1621    let node_data = &styled_dom.node_data.as_container()[dom_id];
1622    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
1623    let cache = &styled_dom.css_property_cache.ptr;
1624
1625    // Track which counters we reset at this level (for cleanup later)
1626    let mut reset_counters_at_this_level = Vec::new();
1627
1628    // CSS Lists §3: display: list-item automatically increments the "list-item" counter
1629    // Check if this is a list-item
1630    let display = cache
1631        .get_display(node_data, &dom_id, node_state)
1632        .and_then(|d| d.get_property().copied());
1633    let is_list_item = matches!(display, Some(LayoutDisplay::ListItem));
1634
1635    // Process counter-reset (now properly typed)
1636    let counter_reset = cache
1637        .get_counter_reset(node_data, &dom_id, node_state)
1638        .and_then(|v| v.get_property());
1639
1640    if let Some(counter_reset) = counter_reset {
1641        let counter_name_str = counter_reset.counter_name.as_str();
1642        if counter_name_str != "none" {
1643            let counter_name = counter_name_str.to_string();
1644            let reset_value = counter_reset.value;
1645
1646            // Reset the counter by pushing a new scope
1647            counter_stacks
1648                .entry(counter_name.clone())
1649                .or_default()
1650                .push(reset_value);
1651            reset_counters_at_this_level.push(counter_name);
1652        }
1653    }
1654
1655    // Process counter-increment (now properly typed)
1656    let counter_inc = cache
1657        .get_counter_increment(node_data, &dom_id, node_state)
1658        .and_then(|v| v.get_property());
1659
1660    if let Some(counter_inc) = counter_inc {
1661        let counter_name_str = counter_inc.counter_name.as_str();
1662        if counter_name_str != "none" {
1663            let counter_name = counter_name_str.to_string();
1664            let inc_value = counter_inc.value;
1665
1666            // Increment the counter in the current scope
1667            let stack = counter_stacks.entry(counter_name.clone()).or_default();
1668            if stack.is_empty() {
1669                // Auto-initialize if counter doesn't exist
1670                stack.push(inc_value);
1671            } else if let Some(current) = stack.last_mut() {
1672                *current += inc_value;
1673            }
1674        }
1675    }
1676
1677    // CSS Lists §3: display: list-item automatically increments "list-item" counter
1678    if is_list_item {
1679        let counter_name = "list-item".to_string();
1680        let stack = counter_stacks.entry(counter_name.clone()).or_default();
1681        if stack.is_empty() {
1682            // Auto-initialize if counter doesn't exist
1683            stack.push(1);
1684        } else {
1685            if let Some(current) = stack.last_mut() {
1686                *current += 1;
1687            }
1688        }
1689    }
1690
1691    // Store the current counter values for this node
1692    for (counter_name, stack) in counter_stacks.iter() {
1693        if let Some(&value) = stack.last() {
1694            counters.insert((node_idx, counter_name.clone()), value);
1695        }
1696    }
1697
1698    // Push scope tracking for cleanup
1699    scope_stack.push(reset_counters_at_this_level.clone());
1700
1701    // Recurse to children
1702    for &child_idx in &node.children {
1703        compute_counters_recursive(
1704            styled_dom,
1705            tree,
1706            child_idx,
1707            counters,
1708            counter_stacks,
1709            scope_stack,
1710        );
1711    }
1712
1713    // Pop counter scopes that were created at this level
1714    if let Some(reset_counters) = scope_stack.pop() {
1715        for counter_name in reset_counters {
1716            if let Some(stack) = counter_stacks.get_mut(&counter_name) {
1717                stack.pop();
1718            }
1719        }
1720    }
1721}