Skip to main content

azul_core/
styled_dom.rs

1//! `StyledDom` - the result of applying CSS styles to a DOM tree.
2//!
3//! This module contains [`StyledDom`], which is produced by combining a [`Dom`]
4//! with a [`Css`] stylesheet via [`StyledDom::create`]. It stores the flattened
5//! node hierarchy, per-node styled states, cascade information, and the CSS
6//! property cache. Restyle operations (`restyle_nodes_hover`, etc.) allow
7//! incremental updates when pseudo-class states change at runtime.
8//!
9//! `StyledDom` is the primary input to the layout engine.
10
11use alloc::{boxed::Box, collections::btree_map::BTreeMap, string::String, vec::Vec};
12use core::{
13    fmt,
14    hash::{Hash, Hasher},
15};
16
17use azul_css::{
18    css::Css,
19    props::{
20        basic::{StyleFontFamily, StyleFontFamilyVec, StyleFontSize},
21        property::{
22            BoxDecorationBreakValue, BreakInsideValue, CaretAnimationDurationValue,
23            CaretColorValue, ColumnCountValue, ColumnFillValue, ColumnRuleColorValue,
24            ColumnRuleStyleValue, ColumnRuleWidthValue, ColumnSpanValue, ColumnWidthValue,
25            ContentValue, CounterIncrementValue, CounterResetValue, CssProperty, CssPropertyType,
26            FlowFromValue, FlowIntoValue, LayoutAlignContentValue, LayoutAlignItemsValue,
27            LayoutAlignSelfValue, LayoutBorderBottomWidthValue, LayoutBorderLeftWidthValue,
28            LayoutBorderRightWidthValue, LayoutBorderTopWidthValue, LayoutBoxSizingValue,
29            LayoutClearValue, LayoutColumnGapValue, LayoutDisplayValue, LayoutFlexBasisValue,
30            LayoutFlexDirectionValue, LayoutFlexGrowValue, LayoutFlexShrinkValue,
31            LayoutFlexWrapValue, LayoutFloatValue, LayoutGapValue, LayoutGridAutoColumnsValue,
32            LayoutGridAutoFlowValue, LayoutGridAutoRowsValue, LayoutGridColumnValue,
33            LayoutGridRowValue, LayoutGridTemplateColumnsValue, LayoutGridTemplateRowsValue,
34            LayoutHeightValue, LayoutInsetBottomValue, LayoutJustifyContentValue,
35            LayoutJustifyItemsValue, LayoutJustifySelfValue, LayoutLeftValue,
36            LayoutMarginBottomValue, LayoutMarginLeftValue, LayoutMarginRightValue,
37            LayoutMarginTopValue, LayoutMaxHeightValue, LayoutMaxWidthValue, LayoutMinHeightValue,
38            LayoutMinWidthValue, LayoutOverflowValue, LayoutPaddingBottomValue,
39            LayoutPaddingLeftValue, LayoutPaddingRightValue, LayoutPaddingTopValue,
40            LayoutPositionValue, LayoutRightValue, LayoutRowGapValue, LayoutScrollbarWidthValue,
41            LayoutTextJustifyValue, LayoutTopValue, LayoutWidthValue, LayoutWritingModeValue,
42            LayoutZIndexValue, OrphansValue, PageBreakValue, RelayoutScope,
43            SelectionBackgroundColorValue, SelectionColorValue, ShapeImageThresholdValue,
44            ShapeMarginValue, ShapeOutsideValue, StringSetValue, StyleBackfaceVisibilityValue,
45            StyleBackgroundContentVecValue, StyleBackgroundPositionVecValue,
46            StyleBackgroundRepeatVecValue, StyleBackgroundSizeVecValue,
47            StyleBorderBottomColorValue, StyleBorderBottomLeftRadiusValue,
48            StyleBorderBottomRightRadiusValue, StyleBorderBottomStyleValue,
49            StyleBorderLeftColorValue, StyleBorderLeftStyleValue, StyleBorderRightColorValue,
50            StyleBorderRightStyleValue, StyleBorderTopColorValue, StyleBorderTopLeftRadiusValue,
51            StyleBorderTopRightRadiusValue, StyleBorderTopStyleValue, StyleBoxShadowValue,
52            StyleCursorValue, StyleDirectionValue, StyleFilterVecValue, StyleFontFamilyVecValue,
53            StyleFontSizeValue, StyleFontValue, StyleHyphensValue, StyleLetterSpacingValue,
54            StyleLineHeightValue, StyleMixBlendModeValue, StyleOpacityValue,
55            StylePerspectiveOriginValue, StyleScrollbarColorValue, StyleTabSizeValue,
56            StyleTextAlignValue, StyleTextColorValue, StyleTransformOriginValue,
57            StyleTransformVecValue, StyleVisibilityValue, StyleWhiteSpaceValue,
58            StyleWordSpacingValue, WidowsValue,
59        },
60        style::StyleTextColor,
61    },
62    AzString,
63};
64
65use crate::{
66    callbacks::Update,
67    dom::{Dom, DomId, NodeData, NodeDataVec, OptionTabIndex, TabIndex, TagId},
68    events::{RelayoutNodes, RestyleNodes},
69    id::{
70        Node, NodeDataContainer, NodeDataContainerRef, NodeDataContainerRefMut, NodeHierarchy,
71        NodeId,
72    },
73    menu::Menu,
74    prop_cache::{CssPropertyCache, CssPropertyCachePtr},
75    refany::RefAny,
76    resources::{Au, ImageCache, ImageRef, ImmediateFontId, RendererResources},
77    style::{
78        construct_html_cascade_tree, matches_html_element, rule_ends_with, CascadeInfo,
79        CascadeInfoVec,
80    },
81    FastBTreeSet, OrderedMap,
82};
83
84#[repr(C)]
85#[derive(Debug, Clone, PartialEq, Hash, PartialOrd, Eq, Ord)]
86pub struct ChangedCssProperty {
87    pub previous_state: StyledNodeState,
88    pub previous_prop: CssProperty,
89    pub current_state: StyledNodeState,
90    pub current_prop: CssProperty,
91}
92
93impl_option!(
94    ChangedCssProperty,
95    OptionChangedCssProperty,
96    copy = false,
97    [Debug, Clone, PartialEq, Hash, PartialOrd, Eq, Ord]
98);
99
100impl_vec!(
101    ChangedCssProperty,
102    ChangedCssPropertyVec,
103    ChangedCssPropertyVecDestructor,
104    ChangedCssPropertyVecDestructorType,
105    ChangedCssPropertyVecSlice,
106    OptionChangedCssProperty
107);
108impl_vec_debug!(ChangedCssProperty, ChangedCssPropertyVec);
109impl_vec_partialord!(ChangedCssProperty, ChangedCssPropertyVec);
110impl_vec_clone!(
111    ChangedCssProperty,
112    ChangedCssPropertyVec,
113    ChangedCssPropertyVecDestructor
114);
115impl_vec_partialeq!(ChangedCssProperty, ChangedCssPropertyVec);
116
117/// Focus state change for restyle operations
118#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub struct FocusChange {
120    /// Node that lost focus (if any)
121    pub lost_focus: Option<NodeId>,
122    /// Node that gained focus (if any)
123    pub gained_focus: Option<NodeId>,
124}
125
126/// Hover state change for restyle operations
127#[derive(Debug, Clone, PartialEq, Eq)]
128pub struct HoverChange {
129    /// Nodes that the mouse left
130    pub left_nodes: Vec<NodeId>,
131    /// Nodes that the mouse entered
132    pub entered_nodes: Vec<NodeId>,
133}
134
135/// Active (mouse down) state change for restyle operations
136#[derive(Debug, Clone, PartialEq, Eq)]
137pub struct ActiveChange {
138    /// Nodes that were deactivated (mouse up)
139    pub deactivated: Vec<NodeId>,
140    /// Nodes that were activated (mouse down)
141    pub activated: Vec<NodeId>,
142}
143
144/// Result of a restyle operation, indicating what needs to be updated
145#[derive(Debug, Clone, Default)]
146pub struct RestyleResult {
147    /// Nodes whose CSS properties changed, with details of the changes
148    pub changed_nodes: RestyleNodes,
149    /// Whether layout needs to be recalculated (layout properties changed)
150    pub needs_layout: bool,
151    /// Whether display list needs regeneration (visual properties changed)
152    pub needs_display_list: bool,
153    /// Whether only GPU-level properties changed (opacity, transform)
154    /// If true and `needs_display_list` is false, we can update via GPU without display list rebuild
155    pub gpu_only_changes: bool,
156    /// The highest `RelayoutScope` seen across all property changes.
157    ///
158    /// This enables the IFC incremental layout optimization (Phase 2):
159    /// - `None`      -> repaint only, zero layout work
160    /// - `IfcOnly`   -> only the affected IFC needs re-shaping/repositioning
161    /// - `SizingOnly`-> this node's size changed, parent repositions siblings
162    /// - `Full`      -> full subtree relayout
163    ///
164    /// When `max_relayout_scope <= IfcOnly`, the layout engine can skip
165    /// full `calculate_layout_for_subtree` and use the IFC fast path instead.
166    pub max_relayout_scope: RelayoutScope,
167}
168
169impl RestyleResult {
170    /// Returns true if any changes occurred
171    #[must_use]
172    pub fn has_changes(&self) -> bool {
173        !self.changed_nodes.is_empty()
174    }
175
176    /// Merge another `RestyleResult` into this one
177    pub fn merge(&mut self, other: Self) {
178        for (node_id, changes) in other.changed_nodes {
179            self.changed_nodes
180                .entry(node_id)
181                .or_default()
182                .extend(changes);
183        }
184        self.needs_layout = self.needs_layout || other.needs_layout;
185        self.needs_display_list = self.needs_display_list || other.needs_display_list;
186        self.gpu_only_changes = self.gpu_only_changes && other.gpu_only_changes;
187        // Keep the highest (most expensive) scope
188        if other.max_relayout_scope > self.max_relayout_scope {
189            self.max_relayout_scope = other.max_relayout_scope;
190        }
191    }
192}
193
194/// NOTE: multiple states can be active at the same time
195///
196/// Tracks all CSS pseudo-class states for a node.
197/// Each flag is independent - a node can be both :hover and :focus simultaneously.
198#[repr(C)]
199#[derive(Clone, Copy, PartialEq, Hash, PartialOrd, Eq, Ord, Default)]
200pub struct StyledNodeState {
201    /// Element is being hovered (:hover)
202    pub hover: bool,
203    /// Element is active/being clicked (:active)
204    pub active: bool,
205    /// Element has focus (:focus)
206    pub focused: bool,
207    /// Element is disabled (:disabled)
208    pub disabled: bool,
209    /// Element is checked/selected (:checked)
210    pub checked: bool,
211    /// Element or descendant has focus (:focus-within)
212    pub focus_within: bool,
213    /// Link has been visited (:visited)
214    pub visited: bool,
215    /// Window is not focused (:backdrop) - GTK compatibility
216    pub backdrop: bool,
217    /// Element is currently being dragged (:dragging)
218    pub dragging: bool,
219    /// A dragged element is over this drop target (:drag-over)
220    pub drag_over: bool,
221    /// The PROMPT of an empty editable is being styled (`::placeholder`).
222    ///
223    /// A pseudo-ELEMENT rather than a state of the node: it is never set on
224    /// a real node during the cascade. The engine turns it on for the single
225    /// resolve that produces the prompt's own style, so `::placeholder`
226    /// rules travel the same bucketing/inheritance/lookup path as `:hover`
227    /// and `:focus` instead of needing storage of their own.
228    pub placeholder: bool,
229    /// A NON-primary pointer seat focuses this node (`:seat-focus`,
230    /// 9b-ii-a-i-d-iii-a). Independent of `focused`, which is the primary's.
231    pub seat_focused: bool,
232}
233
234impl fmt::Debug for StyledNodeState {
235    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
236        let mut v = Vec::new();
237        if self.hover {
238            v.push("hover");
239        }
240        if self.active {
241            v.push("active");
242        }
243        if self.focused {
244            v.push("focused");
245        }
246        if self.disabled {
247            v.push("disabled");
248        }
249        if self.checked {
250            v.push("checked");
251        }
252        if self.seat_focused {
253            v.push("seat_focused");
254        }
255        if self.focus_within {
256            v.push("focus_within");
257        }
258        if self.visited {
259            v.push("visited");
260        }
261        if self.backdrop {
262            v.push("backdrop");
263        }
264        if self.dragging {
265            v.push("dragging");
266        }
267        if self.drag_over {
268            v.push("drag_over");
269        }
270        if v.is_empty() {
271            v.push("normal");
272        }
273        write!(f, "{v:?}")
274    }
275}
276
277impl StyledNodeState {
278    /// Creates a new state with all states set to false (normal state).
279    #[must_use]
280    pub const fn new() -> Self {
281        Self {
282            hover: false,
283            active: false,
284            focused: false,
285            disabled: false,
286            checked: false,
287            focus_within: false,
288            visited: false,
289            backdrop: false,
290            dragging: false,
291            drag_over: false,
292            placeholder: false,
293            seat_focused: false,
294        }
295    }
296
297    /// Check if a specific pseudo-state is active
298    #[must_use]
299    pub const fn has_state(&self, state_type: u8) -> bool {
300        match state_type {
301            0 => true, // Normal is always active
302            1 => self.hover,
303            2 => self.active,
304            3 => self.focused,
305            4 => self.disabled,
306            5 => self.checked,
307            6 => self.focus_within,
308            7 => self.visited,
309            8 => self.backdrop,
310            9 => self.dragging,
311            10 => self.drag_over,
312            11 => self.seat_focused,
313            _ => false,
314        }
315    }
316
317    /// Returns true if no special state is active (just normal)
318    #[must_use]
319    pub const fn is_normal(&self) -> bool {
320        !self.hover
321            && !self.active
322            && !self.focused
323            && !self.disabled
324            && !self.checked
325            && !self.focus_within
326            && !self.visited
327            && !self.backdrop
328            && !self.dragging
329            && !self.drag_over
330            // The `::placeholder` pseudo-ELEMENT is NOT the normal state:
331            // leaving it out here sent the prompt's resolve down the
332            // compact-cache fast path, which is keyed for the normal state,
333            // so a `::placeholder` rule silently never applied.
334            && !self.placeholder
335            && !self.seat_focused
336    }
337
338    /// Create from `PseudoStateFlags`
339    #[must_use]
340    pub const fn from_pseudo_state_flags(
341        flags: &azul_css::dynamic_selector::PseudoStateFlags,
342    ) -> Self {
343        Self {
344            hover: flags.hover,
345            active: flags.active,
346            focused: flags.focused,
347            disabled: flags.disabled,
348            checked: flags.checked,
349            focus_within: flags.focus_within,
350            visited: flags.visited,
351            backdrop: flags.backdrop,
352            dragging: flags.dragging,
353            drag_over: flags.drag_over,
354            placeholder: flags.placeholder,
355            seat_focused: flags.seat_focused,
356        }
357    }
358}
359
360/// A styled Dom node
361// Per-DOM-node hot type passed by reference throughout the layout/style
362// pipeline; kept non-Copy on purpose so it isn't silently bulk-copied and to
363// avoid trivially_copy_pass_by_ref churn across the many &StyledNode callers.
364#[allow(missing_copy_implementations)]
365#[repr(C)]
366#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd)]
367pub struct StyledNode {
368    /// Current state of this styled node (used later for caching the style / layout)
369    pub styled_node_state: StyledNodeState,
370}
371
372impl_option!(
373    StyledNode,
374    OptionStyledNode,
375    copy = false,
376    [Debug, Clone, PartialEq, Eq, PartialOrd]
377);
378
379impl_vec!(
380    StyledNode,
381    StyledNodeVec,
382    StyledNodeVecDestructor,
383    StyledNodeVecDestructorType,
384    StyledNodeVecSlice,
385    OptionStyledNode
386);
387impl_vec_mut!(StyledNode, StyledNodeVec);
388impl_vec_debug!(StyledNode, StyledNodeVec);
389impl_vec_partialord!(StyledNode, StyledNodeVec);
390impl_vec_clone!(StyledNode, StyledNodeVec, StyledNodeVecDestructor);
391impl_vec_partialeq!(StyledNode, StyledNodeVec);
392
393impl StyledNodeVec {
394    /// Returns an immutable container reference for indexed access.
395    #[must_use]
396    pub fn as_container(&self) -> NodeDataContainerRef<'_, StyledNode> {
397        NodeDataContainerRef {
398            internal: self.as_ref(),
399        }
400    }
401    /// Returns a mutable container reference for indexed access.
402    pub fn as_container_mut(&mut self) -> NodeDataContainerRefMut<'_, StyledNode> {
403        NodeDataContainerRefMut {
404            internal: self.as_mut(),
405        }
406    }
407}
408
409#[test]
410#[allow(clippy::used_underscore_binding)] // intentional `_`-prefix (FFI/api.json pub field, or cfg-gated binding); access is deliberate
411fn test_css_styling_with_nested_divs() {
412    let s = "
413        html, body, p {
414            margin: 0;
415            padding: 0;
416        }
417        #div1 {
418            border: solid black;
419            height: 2in;
420            position: absolute;
421            top: 1in;
422            width: 3in;
423        }
424        div div {
425            background: blue;
426            height: 1in;
427            position: fixed;
428            width: 1in;
429        }
430    ";
431
432    let css = azul_css::parser2::new_from_str(s);
433    let mut _styled_dom = Dom::create_body().with_children(
434        vec![Dom::create_div()
435            .with_ids_and_classes(vec![crate::dom::IdOrClass::Id("div1".to_string().into())].into())
436            .with_children(vec![Dom::create_div()].into())]
437        .into(),
438    );
439    _styled_dom.add_component_css(css.0);
440}
441
442/// Regression test for the calc.c "frame ≥2 loses all backgrounds" bug:
443/// `recompute_inheritance_and_compact_cache()` must reproduce the
444/// `hot_flags` that `create_from_compact_dom` produced on frame 1. If the
445/// recompute path silently drops to the getters-only `build_compact_cache`
446/// variant, `HOT_FLAG_HAS_BACKGROUND` is never written, the renderer's
447/// `has_any_background()` negative fast-path returns false for every node,
448/// and every painted background vanishes on the next layout pass.
449#[test]
450fn test_recompute_preserves_hot_flag_has_background() {
451    use azul_css::compact_cache::HOT_FLAG_HAS_BACKGROUND;
452
453    let css_str = "
454        body { margin: 0; padding: 0; }
455        .painted { background: red; width: 100px; height: 100px; }
456    ";
457    let css = azul_css::parser2::new_from_str(css_str).0;
458
459    let mut dom = Dom::create_body()
460        .with_children(vec![Dom::create_div().with_class("painted".to_string().into())].into());
461    let mut styled = StyledDom::create(&mut dom, css);
462
463    // Frame 1: find the painted node by walking its hot_flags.
464    let any_bg_frame1 = {
465        let cache = styled
466            .css_property_cache
467            .ptr
468            .compact_cache
469            .as_ref()
470            .expect("compact_cache populated by create_from_compact_dom");
471        (0..styled.node_hierarchy.as_ref().len())
472            .any(|i| cache.tier2_cold[i].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0)
473    };
474    assert!(
475        any_bg_frame1,
476        "frame 1: expected HOT_FLAG_HAS_BACKGROUND on the .painted node",
477    );
478
479    // Frame 2+: simulate regenerate_layout rebuilding the compact cache.
480    // This is the path the calculator hit on every resize tick, and the
481    // one that had silently regressed to the getter-only builder.
482    styled.recompute_inheritance_and_compact_cache();
483
484    let any_bg_frame2 = {
485        let cache = styled
486            .css_property_cache
487            .ptr
488            .compact_cache
489            .as_ref()
490            .expect("compact_cache rebuilt by recompute_inheritance_and_compact_cache");
491        (0..styled.node_hierarchy.as_ref().len())
492            .any(|i| cache.tier2_cold[i].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0)
493    };
494    assert!(
495        any_bg_frame2,
496        "frame ≥2 after recompute_inheritance_and_compact_cache: \
497         HOT_FLAG_HAS_BACKGROUND disappeared. The recompute path must \
498         use build_compact_cache_with_inheritance (not plain \
499         build_compact_cache) so apply_css_property_to_compact runs and \
500         populates hot_flags for the renderer's negative fast-paths.",
501    );
502}
503
504/// Calculated hash of a font-family
505#[derive(Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
506pub struct StyleFontFamilyHash(pub u64);
507
508impl ::core::fmt::Debug for StyleFontFamilyHash {
509    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
510        write!(f, "StyleFontFamilyHash({})", self.0)
511    }
512}
513
514impl StyleFontFamilyHash {
515    /// Computes a 64-bit hash of a font family for cache lookups.
516    #[must_use]
517    pub fn new(family: &StyleFontFamily) -> Self {
518        use core::hash::Hasher;
519        let mut hasher = crate::hash::DefaultHasher::new();
520        family.hash(&mut hasher);
521        Self(hasher.finish())
522    }
523}
524
525/// Calculated hash of a font-family
526#[derive(Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
527pub struct StyleFontFamiliesHash(pub u64);
528
529impl ::core::fmt::Debug for StyleFontFamiliesHash {
530    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
531        write!(f, "StyleFontFamiliesHash({})", self.0)
532    }
533}
534
535impl StyleFontFamiliesHash {
536    /// Computes a 64-bit hash of multiple font families for cache lookups.
537    #[must_use]
538    pub fn new(families: &[StyleFontFamily]) -> Self {
539        use core::hash::Hasher;
540        let mut hasher = crate::hash::DefaultHasher::new();
541        // Prefix with the length so that e.g. `[A, B]` and `[AB]` (or any two
542        // family lists whose concatenated element hashes coincide) cannot
543        // collide into the same cache key.
544        families.len().hash(&mut hasher);
545        for f in families {
546            f.hash(&mut hasher);
547        }
548        Self(hasher.finish())
549    }
550}
551
552/// FFI-safe representation of `Option<NodeId>` as a single `usize`.
553///
554/// # Encoding (1-based)
555///
556/// - `inner = 0` -> `None` (no node)
557/// - `inner = n > 0` -> `Some(NodeId(n - 1))`
558///
559/// This type exists because C/C++ cannot use Rust's `Option` type.
560/// Use [`NodeHierarchyItemId::into_crate_internal`] to decode and
561/// [`NodeHierarchyItemId::from_crate_internal`] to encode.
562///
563/// # Difference from `NodeId`
564///
565/// - **`NodeId`**: A 0-based array index. `NodeId::new(0)` refers to the first node.
566///   Use directly for array indexing: `nodes[node_id.index()]`.
567///
568/// - **`NodeHierarchyItemId`**: A 1-based encoded `Option<NodeId>`.
569///   `inner = 0` means `None`, `inner = 1` means `Some(NodeId(0))`.
570///   **Never use `inner` as an array index!** Always decode first.
571///
572/// # Warning
573///
574/// The `inner` field uses **1-based encoding**, not a direct index!
575/// Never use `inner` directly as an array index - always decode first.
576///
577/// # Example
578///
579/// ```ignore
580/// // Encoding: Option<NodeId> -> NodeHierarchyItemId
581/// let opt = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(5)));
582/// assert_eq!(opt.into_raw(), 6);  // 5 + 1 = 6
583///
584/// // Decoding: NodeHierarchyItemId -> Option<NodeId>
585/// let decoded = opt.into_crate_internal();
586/// assert_eq!(decoded, Some(NodeId::new(5)));
587///
588/// // None case
589/// let none = NodeHierarchyItemId::NONE;
590/// assert_eq!(none.into_raw(), 0);
591/// assert_eq!(none.into_crate_internal(), None);
592/// ```
593#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
594#[repr(C)]
595pub struct NodeHierarchyItemId {
596    // Uses 1-based encoding: 0 = None, n > 0 = Some(NodeId(n-1))
597    // Do NOT use directly as an array index!
598    inner: usize,
599}
600
601impl fmt::Debug for NodeHierarchyItemId {
602    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
603        match self.into_crate_internal() {
604            Some(n) => write!(f, "Some(NodeId({n}))"),
605            None => write!(f, "None"),
606        }
607    }
608}
609
610impl fmt::Display for NodeHierarchyItemId {
611    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
612        write!(f, "{self:?}")
613    }
614}
615
616impl NodeHierarchyItemId {
617    /// Represents `None` (no node). Encoded as `inner = 0`.
618    pub const NONE: Self = Self { inner: 0 };
619
620    /// Creates an `NodeHierarchyItemId` from a raw 1-based encoded value.
621    ///
622    /// # Warning
623    ///
624    /// The value must use 1-based encoding (0 = None, n = NodeId(n-1)).
625    /// Prefer using [`NodeHierarchyItemId::from_crate_internal`] instead.
626    #[inline]
627    #[must_use]
628    pub const fn from_raw(value: usize) -> Self {
629        Self { inner: value }
630    }
631
632    /// Returns the raw 1-based encoded value.
633    ///
634    /// # Warning
635    ///
636    /// The returned value uses 1-based encoding. Do NOT use as an array index!
637    #[inline]
638    #[must_use]
639    pub const fn into_raw(&self) -> usize {
640        self.inner
641    }
642}
643
644impl_option!(
645    NodeHierarchyItemId,
646    OptionNodeHierarchyItemId,
647    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
648);
649
650impl_vec!(
651    NodeHierarchyItemId,
652    NodeHierarchyItemIdVec,
653    NodeHierarchyItemIdVecDestructor,
654    NodeHierarchyItemIdVecDestructorType,
655    NodeHierarchyItemIdVecSlice,
656    OptionNodeHierarchyItemId
657);
658impl_vec_mut!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
659impl_vec_debug!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
660impl_vec_ord!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
661impl_vec_eq!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
662impl_vec_hash!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
663impl_vec_partialord!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
664impl_vec_clone!(
665    NodeHierarchyItemId,
666    NodeHierarchyItemIdVec,
667    NodeHierarchyItemIdVecDestructor
668);
669impl_vec_partialeq!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
670
671impl NodeHierarchyItemId {
672    /// Decodes to `Option<NodeId>` (0 = None, n > 0 = Some(NodeId(n-1))).
673    #[inline]
674    #[must_use]
675    pub const fn into_crate_internal(&self) -> Option<NodeId> {
676        NodeId::from_usize(self.inner)
677    }
678
679    /// Encodes from `Option<NodeId>` (None -> 0, Some(NodeId(n)) -> n+1).
680    #[inline]
681    #[must_use]
682    pub const fn from_crate_internal(t: Option<NodeId>) -> Self {
683        Self {
684            inner: NodeId::into_raw(&t),
685        }
686    }
687}
688
689impl From<Option<NodeId>> for NodeHierarchyItemId {
690    #[inline]
691    fn from(opt: Option<NodeId>) -> Self {
692        Self::from_crate_internal(opt)
693    }
694}
695
696impl From<NodeHierarchyItemId> for Option<NodeId> {
697    #[inline]
698    fn from(id: NodeHierarchyItemId) -> Self {
699        id.into_crate_internal()
700    }
701}
702
703#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
704#[repr(C)]
705pub struct NodeHierarchyItem {
706    pub parent: usize,
707    pub previous_sibling: usize,
708    pub next_sibling: usize,
709    pub last_child: usize,
710}
711
712impl_option!(
713    NodeHierarchyItem,
714    OptionNodeHierarchyItem,
715    [Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash]
716);
717
718/// Iterator over a DOM node's ancestor chain - see [`hierarchy_ancestors`].
719#[derive(Debug)]
720pub struct HierarchyAncestors<'a> {
721    hierarchy: &'a [NodeHierarchyItem],
722    cursor: Option<NodeId>,
723    budget: usize,
724}
725
726impl Iterator for HierarchyAncestors<'_> {
727    type Item = NodeId;
728
729    fn next(&mut self) -> Option<Self::Item> {
730        if self.budget == 0 {
731            return None;
732        }
733        let nid = self.cursor?;
734        self.budget -= 1;
735        self.cursor = self
736            .hierarchy
737            .get(nid.index())
738            .and_then(NodeHierarchyItem::parent_id);
739        Some(nid)
740    }
741}
742
743/// THE ancestor walk over a `NodeHierarchy` - the DOM-tree twin of
744/// `LayoutTree::ancestor_chain`.
745///
746/// Yields `node` (only when `inclusivity` says so) followed by every parent up
747/// to the root, nearest first. `inclusivity` is an argument rather than a
748/// property of the function's name because that is exactly the distinction
749/// `ScrollManager::find_scroll_parent` used to bury in a `nid != node_id`
750/// guard, where no caller could see it.
751///
752/// The walk is bounded by `hierarchy.len()`: no acyclic path can be longer, so
753/// a corrupt parent chain terminates instead of spinning.
754#[inline]
755pub fn hierarchy_ancestors(
756    hierarchy: &[NodeHierarchyItem],
757    node: NodeId,
758    inclusivity: crate::spaces::Inclusivity,
759) -> HierarchyAncestors<'_> {
760    let cursor = if inclusivity.includes_self() {
761        Some(node)
762    } else {
763        hierarchy
764            .get(node.index())
765            .and_then(NodeHierarchyItem::parent_id)
766    };
767    HierarchyAncestors {
768        hierarchy,
769        cursor,
770        budget: hierarchy.len(),
771    }
772}
773
774impl NodeHierarchyItem {
775    /// Creates a zeroed hierarchy item (no parent, siblings, or children).
776    #[must_use]
777    pub const fn zeroed() -> Self {
778        Self {
779            parent: 0,
780            previous_sibling: 0,
781            next_sibling: 0,
782            last_child: 0,
783        }
784    }
785}
786
787impl From<Node> for NodeHierarchyItem {
788    fn from(node: Node) -> Self {
789        Self {
790            parent: NodeId::into_raw(&node.parent),
791            previous_sibling: NodeId::into_raw(&node.previous_sibling),
792            next_sibling: NodeId::into_raw(&node.next_sibling),
793            last_child: NodeId::into_raw(&node.last_child),
794        }
795    }
796}
797
798impl NodeHierarchyItem {
799    /// Returns the parent node ID, if any.
800    #[must_use]
801    pub const fn parent_id(&self) -> Option<NodeId> {
802        NodeId::from_usize(self.parent)
803    }
804    /// Returns the previous sibling node ID, if any.
805    #[must_use]
806    pub const fn previous_sibling_id(&self) -> Option<NodeId> {
807        NodeId::from_usize(self.previous_sibling)
808    }
809    /// Returns the next sibling node ID, if any.
810    #[must_use]
811    pub const fn next_sibling_id(&self) -> Option<NodeId> {
812        NodeId::from_usize(self.next_sibling)
813    }
814    /// Returns the first child node ID (`current_node_id` + 1 if has children).
815    #[must_use]
816    pub fn first_child_id(&self, current_node_id: NodeId) -> Option<NodeId> {
817        self.last_child_id().map(|_| current_node_id + 1)
818    }
819    /// Returns the last child node ID, if any.
820    #[must_use]
821    pub const fn last_child_id(&self) -> Option<NodeId> {
822        NodeId::from_usize(self.last_child)
823    }
824}
825
826impl_vec!(
827    NodeHierarchyItem,
828    NodeHierarchyItemVec,
829    NodeHierarchyItemVecDestructor,
830    NodeHierarchyItemVecDestructorType,
831    NodeHierarchyItemVecSlice,
832    OptionNodeHierarchyItem
833);
834impl_vec_mut!(NodeHierarchyItem, NodeHierarchyItemVec);
835impl_vec_debug!(AzNode, NodeHierarchyItemVec);
836impl_vec_partialord!(AzNode, NodeHierarchyItemVec);
837impl_vec_clone!(
838    NodeHierarchyItem,
839    NodeHierarchyItemVec,
840    NodeHierarchyItemVecDestructor
841);
842impl_vec_partialeq!(AzNode, NodeHierarchyItemVec);
843
844impl NodeHierarchyItemVec {
845    /// Returns an immutable container reference for indexed access.
846    #[must_use]
847    pub fn as_container(&self) -> NodeDataContainerRef<'_, NodeHierarchyItem> {
848        NodeDataContainerRef {
849            internal: self.as_ref(),
850        }
851    }
852    /// Returns a mutable container reference for indexed access.
853    pub fn as_container_mut(&mut self) -> NodeDataContainerRefMut<'_, NodeHierarchyItem> {
854        NodeDataContainerRefMut {
855            internal: self.as_mut(),
856        }
857    }
858}
859
860impl NodeDataContainerRef<'_, NodeHierarchyItem> {
861    /// Returns the number of descendant nodes under the given parent.
862    #[inline]
863    #[must_use]
864    pub fn subtree_len(&self, parent_id: NodeId) -> usize {
865        let self_item_index = parent_id.index();
866        let next_item_index = self[parent_id]
867            .next_sibling_id()
868            .map_or_else(|| self.len(), |s| s.index());
869        // saturating: a malformed FastDom can leave next_sibling <= parent,
870        // which would underflow-panic the subtraction.
871        next_item_index
872            .saturating_sub(self_item_index)
873            .saturating_sub(1)
874    }
875}
876
877#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
878#[repr(C)]
879pub struct ParentWithNodeDepth {
880    pub depth: usize,
881    pub node_id: NodeHierarchyItemId,
882}
883
884impl_option!(
885    ParentWithNodeDepth,
886    OptionParentWithNodeDepth,
887    [Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash]
888);
889
890impl fmt::Debug for ParentWithNodeDepth {
891    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
892        write!(
893            f,
894            "{{ depth: {}, node: {:?} }}",
895            self.depth,
896            self.node_id.into_crate_internal()
897        )
898    }
899}
900
901impl_vec!(
902    ParentWithNodeDepth,
903    ParentWithNodeDepthVec,
904    ParentWithNodeDepthVecDestructor,
905    ParentWithNodeDepthVecDestructorType,
906    ParentWithNodeDepthVecSlice,
907    OptionParentWithNodeDepth
908);
909impl_vec_mut!(ParentWithNodeDepth, ParentWithNodeDepthVec);
910impl_vec_debug!(ParentWithNodeDepth, ParentWithNodeDepthVec);
911impl_vec_partialord!(ParentWithNodeDepth, ParentWithNodeDepthVec);
912impl_vec_clone!(
913    ParentWithNodeDepth,
914    ParentWithNodeDepthVec,
915    ParentWithNodeDepthVecDestructor
916);
917impl_vec_partialeq!(ParentWithNodeDepth, ParentWithNodeDepthVec);
918
919#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
920#[repr(C)]
921pub struct TagIdToNodeIdMapping {
922    // Hit-testing tag ID (not all nodes have a tag, only nodes that are hit-testable)
923    pub tag_id: TagId,
924    /// Node ID of the node that has a tag
925    pub node_id: NodeHierarchyItemId,
926    /// Whether this node has a tab-index field
927    pub tab_index: OptionTabIndex,
928}
929
930impl_option!(
931    TagIdToNodeIdMapping,
932    OptionTagIdToNodeIdMapping,
933    copy = false,
934    [Debug, Clone, PartialEq, Eq, Ord, PartialOrd]
935);
936
937impl_vec!(
938    TagIdToNodeIdMapping,
939    TagIdToNodeIdMappingVec,
940    TagIdToNodeIdMappingVecDestructor,
941    TagIdToNodeIdMappingVecDestructorType,
942    TagIdToNodeIdMappingVecSlice,
943    OptionTagIdToNodeIdMapping
944);
945impl_vec_mut!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
946impl_vec_debug!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
947impl_vec_partialord!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
948impl_vec_clone!(
949    TagIdToNodeIdMapping,
950    TagIdToNodeIdMappingVec,
951    TagIdToNodeIdMappingVecDestructor
952);
953impl_vec_partialeq!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
954
955#[derive(Debug, Clone, PartialEq, PartialOrd)]
956#[repr(C)]
957pub struct ContentGroup {
958    /// The parent of the current node group, i.e. either the root node (0)
959    /// or the last positioned node ()
960    pub root: NodeHierarchyItemId,
961    /// Node ids in order of drawing
962    pub children: ContentGroupVec,
963}
964
965impl_option!(
966    ContentGroup,
967    OptionContentGroup,
968    copy = false,
969    [Debug, Clone, PartialEq, PartialOrd]
970);
971
972impl_vec!(
973    ContentGroup,
974    ContentGroupVec,
975    ContentGroupVecDestructor,
976    ContentGroupVecDestructorType,
977    ContentGroupVecSlice,
978    OptionContentGroup
979);
980impl_vec_mut!(ContentGroup, ContentGroupVec);
981impl_vec_debug!(ContentGroup, ContentGroupVec);
982impl_vec_partialord!(ContentGroup, ContentGroupVec);
983impl_vec_clone!(ContentGroup, ContentGroupVec, ContentGroupVecDestructor);
984impl_vec_partialeq!(ContentGroup, ContentGroupVec);
985
986#[derive(Debug, PartialEq, Clone)]
987#[repr(C)]
988pub struct StyledDom {
989    pub root: NodeHierarchyItemId,
990    pub node_hierarchy: NodeHierarchyItemVec,
991    pub node_data: NodeDataVec,
992    pub styled_nodes: StyledNodeVec,
993    pub cascade_info: CascadeInfoVec,
994    pub nodes_with_window_callbacks: NodeHierarchyItemIdVec,
995    pub nodes_with_datasets: NodeHierarchyItemIdVec,
996    pub tag_ids_to_node_ids: TagIdToNodeIdMappingVec,
997    pub non_leaf_nodes: ParentWithNodeDepthVec,
998    pub css_property_cache: CssPropertyCachePtr,
999    /// The ID of this DOM in the layout tree (for multi-DOM support with `VirtualViews`)
1000    pub dom_id: DomId,
1001}
1002impl_option!(
1003    StyledDom,
1004    OptionStyledDom,
1005    copy = false,
1006    [Debug, Clone, PartialEq]
1007);
1008
1009impl Default for StyledDom {
1010    fn default() -> Self {
1011        let root_node: NodeHierarchyItem = Node::ROOT.into();
1012        let root_node_id: NodeHierarchyItemId =
1013            NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO));
1014        Self {
1015            root: root_node_id,
1016            node_hierarchy: vec![root_node].into(),
1017            node_data: vec![NodeData::create_body()].into(),
1018            styled_nodes: vec![StyledNode::default()].into(),
1019            cascade_info: vec![CascadeInfo {
1020                index_in_parent: 0,
1021                is_last_child: true,
1022            }]
1023            .into(),
1024            tag_ids_to_node_ids: Vec::new().into(),
1025            non_leaf_nodes: vec![ParentWithNodeDepth {
1026                depth: 0,
1027                node_id: root_node_id,
1028            }]
1029            .into(),
1030            nodes_with_window_callbacks: Vec::new().into(),
1031            nodes_with_datasets: Vec::new().into(),
1032            css_property_cache: CssPropertyCachePtr::new(CssPropertyCache::empty(1)),
1033            dom_id: DomId::ROOT_ID,
1034        }
1035    }
1036}
1037
1038/// Per-field heap-byte breakdown of a `StyledDom`.
1039#[derive(Debug, Clone, Copy, Default)]
1040pub struct StyledDomMemoryReport {
1041    pub node_count: usize,
1042    pub node_hierarchy_bytes: usize,
1043    pub node_data_bytes: usize,
1044    pub styled_nodes_bytes: usize,
1045    pub cascade_info_bytes: usize,
1046    pub tag_ids_bytes: usize,
1047    pub non_leaf_nodes_bytes: usize,
1048    pub callback_vecs_bytes: usize,
1049    pub css_property_cache: crate::prop_cache::CssPropertyCacheBreakdown,
1050}
1051
1052impl StyledDomMemoryReport {
1053    #[must_use]
1054    pub const fn total_bytes(&self) -> usize {
1055        self.node_hierarchy_bytes
1056            + self.node_data_bytes
1057            + self.styled_nodes_bytes
1058            + self.cascade_info_bytes
1059            + self.tag_ids_bytes
1060            + self.non_leaf_nodes_bytes
1061            + self.callback_vecs_bytes
1062            + self.css_property_cache.total_bytes()
1063    }
1064}
1065
1066/// `AZ_CASCADE_TRACE=1`: one line per cascade-invalidating decision.
1067///
1068/// The two questions this exists to answer are the ones that gate any further
1069/// work on `CssPropertyCache::css_props` (24.2 MB, the largest remaining line
1070/// item): how often is the compact cache REBUILT, and how often does the
1071/// dynamic selector context actually change?
1072///
1073/// `css_props` is written only by `restyle()` and is therefore fully
1074/// reproducible from `retained_author_css`, so it could be pruned of
1075/// compact-encoded Normal properties and re-derived on demand — but only if
1076/// rebuilds are RARE. `prune_compact_normal_props` currently refuses to prune
1077/// it, citing a per-frame rebuild; that claim is untested, and the e2e runner
1078/// does not exercise `set_dynamic_selector_context` at all, so it cannot be
1079/// tested there. Run a real app with this dial to settle it.
1080#[cfg(feature = "std")]
1081pub(crate) fn cascade_trace(msg: impl FnOnce() -> String) {
1082    use std::sync::atomic::{AtomicUsize, Ordering};
1083    use std::sync::OnceLock;
1084    static ON: OnceLock<bool> = OnceLock::new();
1085    static N: AtomicUsize = AtomicUsize::new(0);
1086    if *ON.get_or_init(|| std::env::var("AZ_CASCADE_TRACE").is_ok()) {
1087        eprintln!("[cascade #{}] {}", N.fetch_add(1, Ordering::Relaxed) + 1, msg());
1088    }
1089}
1090
1091#[cfg(not(feature = "std"))]
1092pub(crate) fn cascade_trace(_: impl FnOnce() -> alloc::string::String) {}
1093
1094impl StyledDom {
1095    /// Approximate heap bytes retained by this `StyledDom`, broken out by field.
1096    #[must_use]
1097    pub fn memory_report(&self) -> StyledDomMemoryReport {
1098        let n = self.node_data.len();
1099        StyledDomMemoryReport {
1100            node_count: n,
1101            node_hierarchy_bytes: size_of_val(self.node_hierarchy.as_ref()),
1102            node_data_bytes: {
1103                let base = n * size_of::<NodeData>();
1104                // NodeData contains inline Vecs (callbacks, css_props, datasets)
1105                // that have their own heap allocations. Approximate:
1106                let mut inner = 0usize;
1107                for nd in self.node_data.as_ref() {
1108                    inner += nd.get_callbacks().len() * 64; // rough per-callback
1109                                                            // Each rule = path + decls Vec + conditions Vec + priority byte.
1110                                                            // Approximate at 64 bytes per rule + the heap for declarations.
1111                    inner += nd.style.rules.as_ref().len() * 64;
1112                }
1113                base + inner
1114            },
1115            styled_nodes_bytes: n * size_of::<StyledNode>(),
1116            cascade_info_bytes: n * size_of::<CascadeInfo>(),
1117            tag_ids_bytes: size_of_val(self.tag_ids_to_node_ids.as_ref()),
1118            non_leaf_nodes_bytes: size_of_val(self.non_leaf_nodes.as_ref()),
1119            callback_vecs_bytes: self.nodes_with_window_callbacks.as_ref().len() * 8
1120                + self.nodes_with_datasets.as_ref().len() * 8,
1121            css_property_cache: self.css_property_cache.ptr.memory_breakdown(),
1122        }
1123    }
1124
1125    /// Creates a new `StyledDom` by applying CSS styles to a DOM tree.
1126    ///
1127    /// NOTE: After calling this function, the DOM will be reset to an empty DOM.
1128    // This is for memory optimization, so that the DOM does not need to be cloned.
1129    //
1130    // The CSS will be left in-place, but will be re-ordered
1131    pub fn create(dom: &mut Dom, css: Css) -> Self {
1132        use core::mem;
1133
1134        let mut swap_dom = Dom::create_body();
1135        mem::swap(dom, &mut swap_dom);
1136
1137        // Silent-loss fix: this entry point used to DISCARD every
1138        // node-attached stylesheet (`Dom::with_css` / `add_component_css`) —
1139        // the string parsed, rode the Dom to here, and vanished in the
1140        // CompactDom conversion, while `create_from_dom` applied it. The
1141        // classic split-brain constructor: half the callers styled nothing
1142        // and nobody was told (a `width:260px; height:100px` box silently
1143        // laid out auto-sized). Collect + subtree-scope exactly like
1144        // `create_from_dom` (#47), APPENDED after the caller's stylesheet —
1145        // inline rules already carry `rule_priority::INLINE`, so the cascade
1146        // ranks them correctly regardless of order.
1147        swap_dom.fixup_children_estimated();
1148        let mut next_scope_id = 0usize;
1149        scope_inline_css(&mut swap_dom, &mut next_scope_id);
1150        let mut node_css: Vec<Css> = Vec::new();
1151        collect_css_from_dom(&swap_dom, &mut node_css);
1152        let css = if node_css.is_empty() {
1153            css
1154        } else {
1155            let mut combined_rules = css.rules.into_library_owned_vec();
1156            let mut combined_keyframes = css.keyframes.into_library_owned_vec();
1157            for c in node_css {
1158                combined_rules.extend(c.rules.into_library_owned_vec());
1159                combined_keyframes.extend(c.keyframes.into_library_owned_vec());
1160            }
1161            let mut merged = Css::new(combined_rules);
1162            merged.keyframes = combined_keyframes.into();
1163            merged
1164        };
1165        strip_css_from_dom(&mut swap_dom);
1166
1167        let compact_dom: CompactDom = swap_dom.into();
1168        let node_hierarchy: NodeHierarchyItemVec = compact_dom
1169            .node_hierarchy
1170            .as_ref()
1171            .internal
1172            .iter()
1173            .map(|i| (*i).into())
1174            .collect::<Vec<NodeHierarchyItem>>()
1175            .into();
1176
1177        Self::create_from_compact_dom(compact_dom, css, node_hierarchy)
1178    }
1179
1180    /// Creates a `StyledDom` from a `FastDom` (arena-based DOM).
1181    ///
1182    /// This skips the `convert_dom_into_compact_dom` tree->arena conversion
1183    /// entirely since `FastDom` already has flat `NodeHierarchyItemVec` and
1184    /// `NodeDataVec`. CSS is collected from `CssWithNodeIdVec`.
1185    #[must_use]
1186    pub fn create_from_fast_dom(fast_dom: crate::dom::FastDom) -> Self {
1187        use azul_css::css::Css;
1188
1189        // 1. Merge CSS from CssWithNodeIdVec into a single Css, scoping each
1190        //    node-attached stylesheet to its owner's subtree (#47): push_front a
1191        //    Root([owner, owner+subtree_len]) selector so inline/XML css can't leak
1192        //    globally — the same scoping the recursive create_from_dom path applies
1193        //    via scope_inline_css. `node_id` is the owner's flat id (0 = root).
1194        let mut combined_rules: Vec<azul_css::css::CssRuleBlock> = Vec::new();
1195        let mut combined_keyframes: Vec<azul_css::css::Keyframes> = Vec::new();
1196        let css_entries = fast_dom.css.into_library_owned_vec();
1197        {
1198            let hierarchy = fast_dom.node_hierarchy.as_container();
1199            for mut css_with_id in css_entries {
1200                // Keyframes are name-global (no scoping): collect before the
1201                // rules are consumed. Later definitions win at resolve time.
1202                combined_keyframes.extend(
1203                    core::mem::take(&mut css_with_id.css.keyframes).into_library_owned_vec(),
1204                );
1205                let owner = css_with_id.node_id;
1206                let end = if owner < hierarchy.len() {
1207                    owner + hierarchy.subtree_len(NodeId::new(owner))
1208                } else {
1209                    owner
1210                };
1211                for mut rule in css_with_id.css.rules.into_library_owned_vec() {
1212                    // Bare-declaration wrappers (INLINE priority) stay
1213                    // node-only; a stylesheet's `* { ... }` (AUTHOR/UA
1214                    // priority) scopes to the whole subtree. See
1215                    // push_front_scope_for.
1216                    let node_only = rule.priority >= azul_css::css::rule_priority::INLINE;
1217                    rule.path.push_front_scope_for(owner, end, node_only);
1218                    combined_rules.push(rule);
1219                }
1220            }
1221        }
1222        let combined_css = if combined_rules.is_empty() && combined_keyframes.is_empty() {
1223            Css::empty()
1224        } else {
1225            let mut css = Css::new(combined_rules);
1226            css.keyframes = combined_keyframes.into();
1227            css
1228        };
1229
1230        // 2. Convert NodeHierarchyItemVec → NodeHierarchy (Vec<Node>)
1231        //    for cascade tree computation
1232        let node_hierarchy_items = fast_dom.node_hierarchy;
1233        let nodes: Vec<Node> = node_hierarchy_items
1234            .as_ref()
1235            .iter()
1236            .map(|item| Node {
1237                parent: NodeId::from_usize(item.parent),
1238                previous_sibling: NodeId::from_usize(item.previous_sibling),
1239                next_sibling: NodeId::from_usize(item.next_sibling),
1240                last_child: NodeId::from_usize(item.last_child),
1241            })
1242            .collect();
1243        let node_hierarchy_internal = NodeHierarchy { internal: nodes };
1244
1245        // 3. Build CompactDom from the flat arenas (no conversion needed)
1246        let node_data_vec = fast_dom.node_data.into_library_owned_vec();
1247        let compact_dom = CompactDom {
1248            node_hierarchy: node_hierarchy_internal,
1249            node_data: NodeDataContainer {
1250                internal: node_data_vec,
1251            },
1252            root: NodeId::ZERO,
1253        };
1254
1255        // 4. Delegate to create() which handles cascade, UA CSS, etc.
1256        //    We need a mutable Dom to pass to create(), but we already have CompactDom.
1257        //    Instead, inline the cascade logic from create() with our CompactDom.
1258        Self::create_from_compact_dom(compact_dom, combined_css, node_hierarchy_items)
1259    }
1260
1261    /// Internal: creates `StyledDom` from a `CompactDom` + CSS + pre-built hierarchy items.
1262    /// Shared by both the Slow path (create -> `convert_dom_into_compact_dom` -> this)
1263    /// and the Fast path (`create_from_fast_dom` -> this).
1264    #[allow(clippy::similar_names)] // domain-standard coordinate/control-point names
1265    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
1266    fn create_from_compact_dom(
1267        compact_dom: CompactDom,
1268        mut css: Css,
1269        node_hierarchy: NodeHierarchyItemVec,
1270    ) -> Self {
1271        use crate::dom::EventFilter;
1272
1273        static CASCADE_BREAKDOWN: crate::sync::OnceLock<bool> = crate::sync::OnceLock::new();
1274        let cascade_dbg = *CASCADE_BREAKDOWN.get_or_init(crate::profile::memory_enabled);
1275
1276        let node_count = compact_dom.len();
1277
1278        let non_leaf_nodes = compact_dom
1279            .node_hierarchy
1280            .as_ref()
1281            .get_parents_sorted_by_depth();
1282
1283        let mut styled_nodes = vec![
1284            StyledNode {
1285                styled_node_state: StyledNodeState::new()
1286            };
1287            node_count
1288        ];
1289
1290        let mut css_property_cache = CssPropertyCache::empty(compact_dom.node_data.len());
1291
1292        let html_tree = construct_html_cascade_tree(
1293            &compact_dom.node_hierarchy.as_ref(),
1294            &non_leaf_nodes[..],
1295            &compact_dom.node_data.as_ref(),
1296        );
1297
1298        let non_leaf_nodes = non_leaf_nodes
1299            .iter()
1300            .map(|(depth, node_id)| ParentWithNodeDepth {
1301                depth: *depth,
1302                node_id: NodeHierarchyItemId::from_crate_internal(Some(*node_id)),
1303            })
1304            .collect::<Vec<_>>();
1305
1306        let non_leaf_nodes: ParentWithNodeDepthVec = non_leaf_nodes.into();
1307
1308        let _restyle_tag_ids = css_property_cache.restyle(
1309            &mut css,
1310            &compact_dom.node_data.as_ref(),
1311            &node_hierarchy,
1312            &non_leaf_nodes,
1313            &html_tree.as_ref(),
1314        );
1315
1316        // Retain the author stylesheet on the cache (this used to `drop(css)` to
1317        // save ~500 KiB, but that made runtime-inserted nodes unstyleable: the
1318        // rules were gone, so nothing could ever re-run the cascade for them —
1319        // see e2e/bug-inserted-node-no-author-css.json).
1320        css_property_cache.retained_author_css = css;
1321
1322        // Apply UA defaults + compute inherited values so consumers that
1323        // read `css_property_cache.computed_values` (the web/HTML
1324        // renderer in `dll/src/web/html_render.rs`) see resolved
1325        // properties. The compact cache below stores the same info in
1326        // a different layout for the desktop renderer; computed_values
1327        // is the "tall" form that the web renderer's CSS emitter
1328        // (`emit_css_from_cache`) walks per node.
1329        css_property_cache.apply_ua_css(compact_dom.node_data.as_ref().internal);
1330        css_property_cache.compute_inherited_values(
1331            node_hierarchy.as_container().internal,
1332            compact_dom.node_data.as_ref().internal,
1333        );
1334
1335        let prev_font_hashes: Vec<u64> = css_property_cache
1336            .compact_cache
1337            .as_ref()
1338            .map(|c| c.prev_font_hashes.clone())
1339            .unwrap_or_default();
1340        let compact = css_property_cache.build_compact_cache_with_inheritance(
1341            compact_dom.node_data.as_ref().internal,
1342            node_hierarchy.as_container().internal,
1343            &prev_font_hashes,
1344        );
1345        css_property_cache.compact_cache = Some(compact);
1346        let pre_prune = if cascade_dbg {
1347            Some(css_property_cache.memory_breakdown())
1348        } else {
1349            None
1350        };
1351        css_property_cache.prune_compact_normal_props();
1352        if let Some(pre) = pre_prune {
1353            let post = css_property_cache.memory_breakdown();
1354            #[cfg(feature = "std")]
1355            eprintln!(
1356                "[PRUNE] css_props {} → {} KiB  cascaded {} → {} KiB  (saved {} KiB)",
1357                pre.css_props_bytes / 1024,
1358                post.css_props_bytes / 1024,
1359                pre.cascaded_props_bytes / 1024,
1360                post.cascaded_props_bytes / 1024,
1361                (pre.total_bytes().saturating_sub(post.total_bytes())) / 1024
1362            );
1363            #[cfg(not(feature = "std"))]
1364            let _ = post;
1365        }
1366
1367        let tag_ids =
1368            css_property_cache.generate_tag_ids(&compact_dom.node_data.as_ref(), &node_hierarchy);
1369
1370        if cascade_dbg {
1371            let bd = css_property_cache.memory_breakdown();
1372            #[cfg(feature = "std")]
1373            eprintln!("[CASCADE] {} nodes  cascaded_props={} KiB  css_props={} KiB  compact={} KiB  computed={} KiB  total={} KiB",
1374                node_count,
1375                bd.cascaded_props_bytes / 1024, bd.css_props_bytes / 1024,
1376                bd.compact_cache_bytes / 1024, bd.computed_values_bytes / 1024,
1377                bd.total_bytes() / 1024);
1378            #[cfg(not(feature = "std"))]
1379            let _ = bd;
1380        }
1381
1382        // Collect callback/dataset nodes in a single pass (avoids 3 separate 50K scans).
1383        // For XHTML-parsed DOMs with no callbacks, this early-exits immediately.
1384        let has_any_callbacks = compact_dom
1385            .node_data
1386            .as_ref()
1387            .internal
1388            .iter()
1389            .any(|c| !c.get_callbacks().is_empty() || c.get_dataset().is_some());
1390
1391        let (nodes_with_window_callbacks, nodes_with_datasets) = if has_any_callbacks {
1392            let mut win_cbs = Vec::new();
1393            let mut datasets = Vec::new();
1394            for (node_id, c) in compact_dom.node_data.as_ref().internal.iter().enumerate() {
1395                let cbs = c.get_callbacks();
1396                let has_dataset = c.get_dataset().is_some();
1397                if !cbs.is_empty() || has_dataset {
1398                    datasets.push(NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(
1399                        node_id,
1400                    ))));
1401                }
1402                for cb in cbs {
1403                    if let EventFilter::Window(_) = cb.event {
1404                        win_cbs.push(NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(
1405                            node_id,
1406                        ))));
1407                        break;
1408                    }
1409                }
1410            }
1411            (win_cbs, datasets)
1412        } else {
1413            (Vec::new(), Vec::new())
1414        };
1415        let mut styled_dom = Self {
1416            root: NodeHierarchyItemId::from_crate_internal(Some(compact_dom.root)),
1417            node_hierarchy,
1418            node_data: compact_dom.node_data.internal.into(),
1419            cascade_info: html_tree.internal.into(),
1420            styled_nodes: styled_nodes.into(),
1421            tag_ids_to_node_ids: tag_ids.into(),
1422            nodes_with_window_callbacks: nodes_with_window_callbacks.into(),
1423            nodes_with_datasets: nodes_with_datasets.into(),
1424            non_leaf_nodes,
1425            css_property_cache: CssPropertyCachePtr::new(css_property_cache),
1426            dom_id: DomId::ROOT_ID,
1427        };
1428        #[cfg(feature = "table_layout")]
1429        if let Err(_e) = crate::dom_table::generate_anonymous_table_elements(&mut styled_dom) {}
1430
1431        styled_dom
1432    }
1433
1434    /// Creates a `StyledDom` from a recursive Dom tree with deferred CSS.
1435    ///
1436    /// This is the Phase 7.2 entry point: the layout callback returns a recursive
1437    /// `Dom` with `css: Vec<Css>` on each node. This function:
1438    ///
1439    /// 1. Collects all CSS objects from the recursive tree
1440    /// 2. Flattens the Dom into contiguous arrays (`CompactDom`)
1441    /// 3. Merges all CSS objects and runs a single cascade pass
1442    /// 4. Runs `apply_ua_css` -> `compute_inherited_values` -> `build_compact_cache`
1443    /// 5. Generates anonymous table elements
1444    #[must_use]
1445    pub fn create_from_dom(mut dom: Dom) -> Self {
1446        use azul_css::css::Css;
1447
1448        // #47: scope each node's inline css to its subtree BEFORE collecting, so a
1449        // non-root node's with_css cannot leak to the whole tree. Uses the same
1450        // pre-order ids the flatten (convert_dom_into_compact_dom) will assign;
1451        // needs estimated_total_children populated first.
1452        dom.fixup_children_estimated();
1453        let mut next_scope_id = 0usize;
1454        scope_inline_css(&mut dom, &mut next_scope_id);
1455
1456        // 1. Collect all CSS objects from the recursive Dom tree (now scoped)
1457        let mut all_css = Vec::new();
1458        collect_css_from_dom(&dom, &mut all_css);
1459
1460        // 2. Merge all CSS objects into one combined Css
1461        let mut combined_css = if all_css.is_empty() {
1462            Css::empty()
1463        } else {
1464            let mut combined_rules: Vec<azul_css::css::CssRuleBlock> = Vec::new();
1465            let mut combined_keyframes: Vec<azul_css::css::Keyframes> = Vec::new();
1466            for css in all_css {
1467                combined_rules.extend(css.rules.into_library_owned_vec());
1468                combined_keyframes.extend(css.keyframes.into_library_owned_vec());
1469            }
1470            let mut css = Css::new(combined_rules);
1471            css.keyframes = combined_keyframes.into();
1472            css
1473        };
1474
1475        // 3. Strip CSS from all Dom nodes before flattening
1476        //    (CSS is already collected, don't need it in the flat tree)
1477        strip_css_from_dom(&mut dom);
1478
1479        // 4. Use existing StyledDom::create to flatten + cascade
1480        Self::create(&mut dom, combined_css)
1481    }
1482
1483    /// Appends another `StyledDom` as a child to the `self.root`
1484    /// without re-styling the DOM itself
1485    pub fn append_child(&mut self, other: Self) {
1486        let self_root_id = self.root.into_crate_internal().unwrap_or(NodeId::ZERO);
1487        let current_root_children_count = self_root_id
1488            .az_children(&self.node_hierarchy.as_container())
1489            .count();
1490        self.append_child_with_index(other, current_root_children_count);
1491        self.finalize_non_leaf_nodes();
1492    }
1493
1494    /// Optimized version of `append_child` that takes the child index directly
1495    /// instead of counting existing children (O(1) instead of O(n))
1496    pub fn append_child_with_index(&mut self, mut other: Self, child_index: usize) {
1497        // shift all the node ids in other by self.len()
1498        let self_len = self.node_hierarchy.as_ref().len();
1499        let other_len = other.node_hierarchy.as_ref().len();
1500        let self_root_id = self.root.into_crate_internal().unwrap_or(NodeId::ZERO);
1501        let other_root_id = other.root.into_crate_internal().unwrap_or(NodeId::ZERO);
1502
1503        // Use provided index instead of counting children
1504        other.cascade_info.as_mut()[other_root_id.index()].index_in_parent =
1505            u32::try_from(child_index).unwrap_or(u32::MAX);
1506        other.cascade_info.as_mut()[other_root_id.index()].is_last_child = true;
1507
1508        self.cascade_info.append(&mut other.cascade_info);
1509
1510        // adjust node hierarchy
1511        for other in other.node_hierarchy.as_mut().iter_mut() {
1512            if other.parent != 0 {
1513                other.parent += self_len;
1514            }
1515            if other.previous_sibling != 0 {
1516                other.previous_sibling += self_len;
1517            }
1518            if other.next_sibling != 0 {
1519                other.next_sibling += self_len;
1520            }
1521            if other.last_child != 0 {
1522                other.last_child += self_len;
1523            }
1524        }
1525
1526        other.node_hierarchy.as_container_mut()[other_root_id].parent =
1527            NodeId::into_raw(&Some(self_root_id));
1528        let current_last_child = self.node_hierarchy.as_container()[self_root_id].last_child_id();
1529        other.node_hierarchy.as_container_mut()[other_root_id].previous_sibling =
1530            NodeId::into_raw(&current_last_child);
1531        if let Some(current_last) = current_last_child {
1532            if self.node_hierarchy.as_container_mut()[current_last]
1533                .next_sibling_id()
1534                .is_some()
1535            {
1536                self.node_hierarchy.as_container_mut()[current_last].next_sibling +=
1537                    other_root_id.index() + other_len;
1538            } else {
1539                self.node_hierarchy.as_container_mut()[current_last].next_sibling =
1540                    NodeId::into_raw(&Some(NodeId::new(self_len + other_root_id.index())));
1541            }
1542        }
1543        self.node_hierarchy.as_container_mut()[self_root_id].last_child =
1544            NodeId::into_raw(&Some(NodeId::new(self_len + other_root_id.index())));
1545
1546        self.node_hierarchy.append(&mut other.node_hierarchy);
1547        self.node_data.append(&mut other.node_data);
1548        self.styled_nodes.append(&mut other.styled_nodes);
1549        self.get_css_property_cache_mut()
1550            .append(other.get_css_property_cache_mut());
1551
1552        // Tag IDs are globally unique (AtomicUsize counter) and never collide,
1553        // so we only shift node_id (which changes when DOMs are merged).
1554        for tag_id_node_id in &mut other.tag_ids_to_node_ids {
1555            tag_id_node_id.node_id.inner += self_len;
1556        }
1557
1558        self.tag_ids_to_node_ids
1559            .append(&mut other.tag_ids_to_node_ids);
1560
1561        for nid in &mut other.nodes_with_window_callbacks {
1562            nid.inner += self_len;
1563        }
1564        self.nodes_with_window_callbacks
1565            .append(&mut other.nodes_with_window_callbacks);
1566
1567        for nid in &mut other.nodes_with_datasets {
1568            nid.inner += self_len;
1569        }
1570        self.nodes_with_datasets
1571            .append(&mut other.nodes_with_datasets);
1572
1573        // edge case: if the other StyledDom consists of only one node
1574        // then it is not a parent itself
1575        if other_len != 1 {
1576            for other_non_leaf_node in &mut other.non_leaf_nodes {
1577                other_non_leaf_node.node_id.inner += self_len;
1578                other_non_leaf_node.depth += 1;
1579            }
1580            self.non_leaf_nodes.append(&mut other.non_leaf_nodes);
1581            // NOTE: Sorting deferred - call finalize_non_leaf_nodes() after all appends
1582        }
1583    }
1584
1585    /// Call this after all `append_child_with_index` operations are complete
1586    /// to sort `non_leaf_nodes` by depth (required for correct rendering)
1587    pub fn finalize_non_leaf_nodes(&mut self) {
1588        self.non_leaf_nodes.sort_by(|a, b| a.depth.cmp(&b.depth));
1589    }
1590
1591    /// Same as `append_child()`, but as a builder method
1592    #[must_use]
1593    pub fn with_child(mut self, other: Self) -> Self {
1594        self.append_child(other);
1595        self
1596    }
1597
1598    /// Sets the context menu for the root node
1599    pub fn set_context_menu(&mut self, context_menu: Menu) {
1600        if let Some(root_id) = self.root.into_crate_internal() {
1601            self.node_data.as_container_mut()[root_id].set_context_menu(context_menu);
1602        }
1603    }
1604
1605    /// Builder method for setting the context menu
1606    #[must_use]
1607    pub fn with_context_menu(mut self, context_menu: Menu) -> Self {
1608        self.set_context_menu(context_menu);
1609        self
1610    }
1611
1612    /// Sets the menu bar for the root node
1613    pub fn set_menu_bar(&mut self, menu_bar: Menu) {
1614        if let Some(root_id) = self.root.into_crate_internal() {
1615            self.node_data.as_container_mut()[root_id].set_menu_bar(menu_bar);
1616        }
1617    }
1618
1619    /// Builder method for setting the menu bar
1620    #[must_use]
1621    pub fn with_menu_bar(mut self, menu_bar: Menu) -> Self {
1622        self.set_menu_bar(menu_bar);
1623        self
1624    }
1625
1626    /// Re-compute inherited CSS values and rebuild the compact layout cache.
1627    ///
1628    /// This MUST be called after `append_child()` merges multiple `StyledDom`s.
1629    /// `append_child()` concatenates the CSS property caches but does NOT
1630    /// re-run inheritance or rebuild the compact cache. This means:
1631    ///
1632    /// 1. **Broken inheritance**: Inherited properties (`color`, `font-size`,
1633    ///    `direction`) from the parent DOM do not flow into appended subtrees.
1634    /// 2. **Stale compact cache**: The child's tier 1/2/2b entries still reflect
1635    ///    the child's isolated cascade, not the composed tree.
1636    ///
1637    /// Calling this method after all `append_child()` calls fixes both issues
1638    /// by re-running a full depth-first inheritance pass and rebuilding the
1639    /// compact cache from scratch on the composed tree.
1640    pub fn recompute_inheritance_and_compact_cache(&mut self) {
1641        cascade_trace(|| "compact cache REBUILT from css_props".to_string());
1642        // Use the _with_inheritance variant: it does inheritance inline (via
1643        // parent-compact-field copy) AND populates hot_flags via
1644        // apply_css_property_to_compact.  The plain build_compact_cache would
1645        // leave HOT_FLAG_HAS_BACKGROUND / HAS_CLIP_PATH / extra_flags at 0,
1646        // causing renderer negative fast-paths to skip paint (regression
1647        // introduced by ff059052b).  No SIGABRT risk — _with_inheritance
1648        // never pushes to the flat cascaded_props storage.
1649        let prev_font_hashes: Vec<u64> = self
1650            .css_property_cache
1651            .downcast_mut()
1652            .compact_cache
1653            .as_ref()
1654            .map(|c| c.prev_font_hashes.clone())
1655            .unwrap_or_default();
1656        let compact = self
1657            .css_property_cache
1658            .downcast_mut()
1659            .build_compact_cache_with_inheritance(
1660                self.node_data.as_container().internal,
1661                self.node_hierarchy.as_container().internal,
1662                &prev_font_hashes,
1663            );
1664        self.css_property_cache.downcast_mut().compact_cache = Some(compact);
1665    }
1666
1667    /// Re-applies CSS styles to the existing DOM structure.
1668    /// Grow retained author-CSS subtree scopes to cover a node just appended under
1669    /// `parent`. Mount/`with_css` rules carry a `Root([start, end])` scope
1670    /// (`push_front_scope`) that only matches nodes within a node's ORIGINAL subtree
1671    /// range, so a node appended afterwards falls outside every scope and
1672    /// `restyle_retained` cannot match it. Appending under `parent` (rightmost-spine
1673    /// only, so subtrees stay contiguous in the flat arena) grows `parent`'s and its
1674    /// ancestors' subtrees; bump the inclusive `end` of every scope that already
1675    /// covers `parent` out to the new node.
1676    #[allow(clippy::similar_names)] // new_node/parent and the p/n index locals read clearly in context
1677    pub fn extend_author_scopes_for_appended(&mut self, new_node: NodeId, parent: NodeId) {
1678        use azul_css::css::CssPathSelector;
1679        let p = parent.index();
1680        let n = new_node.index();
1681        let cache = self.css_property_cache.downcast_mut();
1682        for rule in cache.retained_author_css.rules.as_mut() {
1683            let mut sels = rule.path.selectors.as_ref().to_vec();
1684            let mut changed = false;
1685            for sel in &mut sels {
1686                if let CssPathSelector::Root(range) = sel {
1687                    if range.contains(p) && range.end < n {
1688                        range.end = n;
1689                        changed = true;
1690                    }
1691                }
1692            }
1693            if changed {
1694                rule.path.selectors = sels.into();
1695            }
1696        }
1697    }
1698
1699    /// Re-run the author cascade from the stylesheet retained at creation /
1700    /// last `restyle` (`CssPropertyCache::retained_author_css`). Call after a
1701    /// structural DOM mutation (e.g. inserting a node) so new nodes receive
1702    /// author CSS; a no-op when no author stylesheet was ever attached.
1703    /// The PER-TICK override channel: write `user_overridden_properties`
1704    /// WITHOUT recomputing inheritance or the compact cache. Sound only when
1705    /// the caller supplies the pixels itself (the transition driver patches
1706    /// the display list with the interpolated value directly) - every other
1707    /// caller wants [`Self::restyle_user_property`]. At t=1 the override is
1708    /// removed and the (correctly cascaded) target shows through.
1709    pub fn set_user_property_override_fast(
1710        &mut self,
1711        node_id: &NodeId,
1712        new_properties: &[CssProperty],
1713    ) {
1714        let node_count = self.node_data.as_ref().len();
1715        if node_id.index() >= node_count {
1716            return;
1717        }
1718        let cache = self.get_css_property_cache_mut();
1719        if cache.user_overridden_properties.len() < node_count {
1720            cache
1721                .user_overridden_properties
1722                .resize(node_count, Vec::new());
1723        }
1724        for new_prop in new_properties {
1725            let prop_type = new_prop.get_type();
1726            let vec = &mut cache.user_overridden_properties[node_id.index()];
1727            if new_prop.is_initial() {
1728                if let Ok(idx) = vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
1729                    vec.remove(idx);
1730                }
1731            } else {
1732                match vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
1733                    Ok(idx) => vec[idx].1 = new_prop.clone(),
1734                    Err(idx) => vec.insert(idx, (prop_type, new_prop.clone())),
1735                }
1736            }
1737        }
1738    }
1739
1740    pub fn restyle_retained(&mut self) {
1741        let css = self
1742            .css_property_cache
1743            .downcast_mut()
1744            .retained_author_css
1745            .clone();
1746        if css.is_empty() {
1747            return;
1748        }
1749        self.restyle(css);
1750    }
1751
1752    pub fn restyle(&mut self, mut css: Css) {
1753        // NOTE: the tag_ids returned by `cache.restyle` here are generated from
1754        // the STALE `compact_cache` (display/overflow reads) and are intentionally
1755        // discarded — we regenerate them below AFTER the compact cache and
1756        // inheritance have been recomputed (audit styled_dom.rs:1404/1426).
1757        let _stale_tag_ids = self.css_property_cache.downcast_mut().restyle(
1758            &mut css,
1759            &self.node_data.as_container(),
1760            &self.node_hierarchy,
1761            &self.non_leaf_nodes,
1762            &self.cascade_info.as_container(),
1763        );
1764
1765        // Keep the stylesheet for later structural restyles (inserted nodes).
1766        self.css_property_cache.downcast_mut().retained_author_css = css;
1767
1768        // Apply UA CSS properties before computing inheritance
1769        self.css_property_cache
1770            .downcast_mut()
1771            .apply_ua_css(self.node_data.as_container().internal);
1772
1773        // Compute inherited values after restyle and apply_ua_css (resolves em, %, etc.)
1774        self.css_property_cache
1775            .downcast_mut()
1776            .compute_inherited_values(
1777                self.node_hierarchy.as_container().internal,
1778                self.node_data.as_container().internal,
1779            );
1780
1781        // The old compact_cache was built from the pre-restyle CSS. If we do not
1782        // rebuild it, layout-hot properties (display/overflow/background/clip,
1783        // resolved font sizes) keep their stale values and the restyle silently
1784        // no-ops for them. Drop it, rebuild via the _with_inheritance path (which
1785        // repopulates hot_flags), and invalidate the cached resolved font sizes.
1786        let prev_font_hashes: Vec<u64> = self
1787            .css_property_cache
1788            .downcast_mut()
1789            .compact_cache
1790            .as_ref()
1791            .map(|c| c.prev_font_hashes.clone())
1792            .unwrap_or_default();
1793        self.css_property_cache.downcast_mut().compact_cache = None;
1794        let compact = self
1795            .css_property_cache
1796            .downcast_mut()
1797            .build_compact_cache_with_inheritance(
1798                self.node_data.as_container().internal,
1799                self.node_hierarchy.as_container().internal,
1800                &prev_font_hashes,
1801            );
1802        self.css_property_cache.downcast_mut().compact_cache = Some(compact);
1803        self.css_property_cache
1804            .downcast_mut()
1805            .invalidate_resolved_font_sizes();
1806
1807        // Regenerate tag_ids from the freshly rebuilt compact cache so the
1808        // hit-test map reflects the post-restyle display/overflow values.
1809        let new_tag_ids = self
1810            .css_property_cache
1811            .downcast_mut()
1812            .generate_tag_ids(&self.node_data.as_container(), &self.node_hierarchy);
1813        self.tag_ids_to_node_ids = new_tag_ids.into();
1814    }
1815
1816    /// Returns the total number of nodes in this `StyledDom`.
1817    #[inline]
1818    #[must_use]
1819    pub const fn node_count(&self) -> usize {
1820        self.node_data.len()
1821    }
1822
1823    /// Returns an immutable reference to the CSS property cache.
1824    #[inline]
1825    #[must_use]
1826    pub fn get_css_property_cache(&self) -> &CssPropertyCache {
1827        &self.css_property_cache.ptr
1828    }
1829
1830    /// Returns a mutable reference to the CSS property cache.
1831    #[inline]
1832    pub fn get_css_property_cache_mut(&mut self) -> &mut CssPropertyCache {
1833        &mut self.css_property_cache.ptr
1834    }
1835
1836    /// Returns the current state (hover, active, focus) of a styled node.
1837    #[inline]
1838    #[must_use]
1839    pub fn get_styled_node_state(&self, node_id: &NodeId) -> StyledNodeState {
1840        self.styled_nodes.as_container()[*node_id].styled_node_state
1841    }
1842
1843    /// Updates hover state for nodes and returns changed CSS properties.
1844    #[must_use]
1845    pub fn restyle_nodes_hover(&mut self, nodes: &[NodeId], new_hover_state: bool) -> RestyleNodes {
1846        self.restyle_nodes_state(
1847            nodes,
1848            new_hover_state,
1849            |state, val| state.hover = val,
1850            azul_css::dynamic_selector::PseudoStateType::Hover,
1851        )
1852    }
1853
1854    /// Updates active state for nodes and returns changed CSS properties.
1855    #[must_use]
1856    pub fn restyle_nodes_active(
1857        &mut self,
1858        nodes: &[NodeId],
1859        new_active_state: bool,
1860    ) -> RestyleNodes {
1861        self.restyle_nodes_state(
1862            nodes,
1863            new_active_state,
1864            |state, val| state.active = val,
1865            azul_css::dynamic_selector::PseudoStateType::Active,
1866        )
1867    }
1868
1869    /// Updates focus state for nodes and returns changed CSS properties.
1870    #[must_use]
1871    pub fn restyle_nodes_focus(&mut self, nodes: &[NodeId], new_focus_state: bool) -> RestyleNodes {
1872        self.restyle_nodes_state(
1873            nodes,
1874            new_focus_state,
1875            |state, val| state.focused = val,
1876            azul_css::dynamic_selector::PseudoStateType::Focus,
1877        )
1878    }
1879
1880    /// `:seat-focus` on/off for `nodes` (9b-ii-a-i-d-iii-a).
1881    pub fn restyle_nodes_seat_focus(&mut self, nodes: &[NodeId], on: bool) -> RestyleNodes {
1882        self.restyle_nodes_state(
1883            nodes,
1884            on,
1885            |state, val| state.seat_focused = val,
1886            azul_css::dynamic_selector::PseudoStateType::SeatFocus,
1887        )
1888    }
1889
1890    /// A non-primary seat's focus moved: `lost` drops `:seat-focus`, `gained`
1891    /// takes it. Same result classification as `restyle_on_state_change`.
1892    pub fn restyle_on_seat_focus_change(
1893        &mut self,
1894        lost: Option<NodeId>,
1895        gained: Option<NodeId>,
1896    ) -> RestyleResult {
1897        let mut result = RestyleResult {
1898            gpu_only_changes: true,
1899            ..RestyleResult::default()
1900        };
1901        let mut process = |changes: RestyleNodes, result: &mut RestyleResult| {
1902            for (node_id, props) in changes {
1903                for change in &props {
1904                    let prop_type = change.current_prop.get_type();
1905                    let scope = prop_type.relayout_scope(true);
1906                    if scope > result.max_relayout_scope {
1907                        result.max_relayout_scope = scope;
1908                    }
1909                    if scope != RelayoutScope::None {
1910                        result.needs_layout = true;
1911                        result.gpu_only_changes = false;
1912                    }
1913                    if !prop_type.is_gpu_only_property() {
1914                        result.gpu_only_changes = false;
1915                    }
1916                    result.needs_display_list = true;
1917                }
1918                result.changed_nodes.entry(node_id).or_default().extend(props);
1919            }
1920        };
1921        if let Some(old) = lost {
1922            let changes = self.restyle_nodes_seat_focus(&[old], false);
1923            process(changes, &mut result);
1924        }
1925        if let Some(new) = gained {
1926            let changes = self.restyle_nodes_seat_focus(&[new], true);
1927            process(changes, &mut result);
1928        }
1929        result
1930    }
1931
1932    /// Generic restyle method parameterized by the state field and pseudo-state type.
1933    fn restyle_nodes_state(
1934        &mut self,
1935        nodes: &[NodeId],
1936        new_state_value: bool,
1937        set_state: impl Fn(&mut StyledNodeState, bool),
1938        pseudo_state_type: azul_css::dynamic_selector::PseudoStateType,
1939    ) -> RestyleNodes {
1940        // Drop any stale NodeIds that no longer index into this DOM (e.g. left
1941        // over from a previous, larger tree). Indexing styled_nodes / node_data
1942        // with an out-of-range id would panic. Filtering here keeps the
1943        // downstream zip with `old_node_states` aligned.
1944        let node_count = self.node_count();
1945        let nodes: Vec<NodeId> = nodes
1946            .iter()
1947            .copied()
1948            .filter(|nid| nid.index() < node_count)
1949            .collect();
1950
1951        // save the old node state
1952        let old_node_states = nodes
1953            .iter()
1954            .map(|nid| self.styled_nodes.as_container()[*nid].styled_node_state)
1955            .collect::<Vec<_>>();
1956
1957        for nid in &nodes {
1958            set_state(
1959                &mut self.styled_nodes.as_container_mut()[*nid].styled_node_state,
1960                new_state_value,
1961            );
1962        }
1963
1964        let css_property_cache = self.get_css_property_cache();
1965        let styled_nodes = self.styled_nodes.as_container();
1966        let node_data = self.node_data.as_container();
1967
1968        // scan all properties that could have changed because of addition / removal
1969        let v = nodes
1970            .iter()
1971            .zip(old_node_states.iter())
1972            .filter_map(|(node_id, old_node_state)| {
1973                let mut keys_normal: Vec<_> = CssPropertyCache::prop_types_for_state(
1974                    css_property_cache.css_props.get_slice(node_id.index()),
1975                    pseudo_state_type,
1976                ).collect();
1977                let mut keys_inherited: Vec<_> = CssPropertyCache::prop_types_for_state(
1978                    css_property_cache.cascaded_props.get_slice(node_id.index()),
1979                    pseudo_state_type,
1980                ).collect();
1981                let keys_inline: Vec<CssPropertyType> = {
1982                    use azul_css::dynamic_selector::DynamicSelector;
1983                    node_data[*node_id]
1984                        .style
1985                        .iter_inline_properties()
1986                        .filter_map(|(prop, conds)| {
1987                            let matches = conds.as_slice().iter().any(|c| {
1988                                matches!(c, DynamicSelector::PseudoState(pst) if *pst == pseudo_state_type)
1989                            });
1990                            if matches {
1991                                Some(prop.get_type())
1992                            } else {
1993                                None
1994                            }
1995                        })
1996                        .collect()
1997                };
1998                let mut keys_inline_ref: Vec<_> = keys_inline.iter().collect();
1999
2000                keys_normal.append(&mut keys_inherited);
2001                keys_normal.append(&mut keys_inline_ref);
2002
2003                let node_properties_that_could_have_changed = keys_normal;
2004
2005                if node_properties_that_could_have_changed.is_empty() {
2006                    return None;
2007                }
2008
2009                let new_node_state = &styled_nodes[*node_id].styled_node_state;
2010                let node_data = &node_data[*node_id];
2011
2012                let changes = node_properties_that_could_have_changed
2013                    .into_iter()
2014                    .filter_map(|prop| {
2015                        // calculate both the old and the new state
2016                        let old = css_property_cache.get_property_slow(
2017                            node_data,
2018                            node_id,
2019                            old_node_state,
2020                            prop,
2021                        );
2022                        let new = css_property_cache.get_property_slow(
2023                            node_data,
2024                            node_id,
2025                            new_node_state,
2026                            prop,
2027                        );
2028                        if old == new {
2029                            None
2030                        } else {
2031                            Some(ChangedCssProperty {
2032                                previous_state: *old_node_state,
2033                                previous_prop: old.map_or_else(|| CssProperty::auto(*prop), Clone::clone),
2034                                current_state: *new_node_state,
2035                                current_prop: new.map_or_else(|| CssProperty::auto(*prop), Clone::clone),
2036                            })
2037                        }
2038                    })
2039                    .collect::<Vec<_>>();
2040
2041                if changes.is_empty() {
2042                    None
2043                } else {
2044                    Some((*node_id, changes))
2045                }
2046            })
2047            .collect::<Vec<_>>();
2048
2049        v.into_iter().collect()
2050    }
2051
2052    /// Unified entry point for all CSS restyle operations.
2053    ///
2054    /// This function synchronizes the `StyledNodeState` with runtime state
2055    /// and computes which CSS properties have changed. It determines whether
2056    /// layout, display list, or GPU-only updates are needed.
2057    ///
2058    /// # Arguments
2059    /// * `focus_changes` - Nodes gaining/losing focus
2060    /// * `hover_changes` - Nodes gaining/losing hover
2061    /// * `active_changes` - Nodes gaining/losing active (mouse down)
2062    ///
2063    /// # Returns
2064    /// * `RestyleResult` containing changed nodes and what needs updating
2065    #[must_use]
2066    pub fn restyle_on_state_change(
2067        &mut self,
2068        focus_changes: Option<FocusChange>,
2069        hover_changes: Option<HoverChange>,
2070        active_changes: Option<ActiveChange>,
2071    ) -> RestyleResult {
2072        // Start with GPU-only assumption; refined below as changes are analyzed.
2073        let mut result = RestyleResult {
2074            gpu_only_changes: true,
2075            ..RestyleResult::default()
2076        };
2077
2078        // Helper closure to merge changes and analyze property categories
2079        let mut process_changes = |changes: RestyleNodes| {
2080            for (node_id, props) in changes {
2081                for change in &props {
2082                    let prop_type = change.current_prop.get_type();
2083
2084                    // Use the granular RelayoutScope instead of the binary
2085                    // can_trigger_relayout(). We pass node_is_ifc_member = true
2086                    // conservatively: this means font/text property changes will
2087                    // produce IfcOnly (rather than None). Phase 2c can refine
2088                    // this by checking whether the node actually participates
2089                    // in an IFC.
2090                    let scope = prop_type.relayout_scope(/* node_is_ifc_member */ true);
2091
2092                    // Track the highest scope seen
2093                    if scope > result.max_relayout_scope {
2094                        result.max_relayout_scope = scope;
2095                    }
2096
2097                    // Any scope above None triggers layout
2098                    if scope != RelayoutScope::None {
2099                        result.needs_layout = true;
2100                        result.gpu_only_changes = false;
2101                    }
2102
2103                    // Check if this is a GPU-only property
2104                    if !prop_type.is_gpu_only_property() {
2105                        result.gpu_only_changes = false;
2106                    }
2107
2108                    // Any visual change needs display list update (unless GPU-only)
2109                    result.needs_display_list = true;
2110                }
2111
2112                result
2113                    .changed_nodes
2114                    .entry(node_id)
2115                    .or_default()
2116                    .extend(props);
2117            }
2118        };
2119
2120        // 1. Process focus changes
2121        if let Some(focus) = focus_changes {
2122            if let Some(old) = focus.lost_focus {
2123                let changes = self.restyle_nodes_focus(&[old], false);
2124                process_changes(changes);
2125            }
2126            if let Some(new) = focus.gained_focus {
2127                let changes = self.restyle_nodes_focus(&[new], true);
2128                process_changes(changes);
2129            }
2130        }
2131
2132        // 2. Process hover changes
2133        if let Some(hover) = hover_changes {
2134            if !hover.left_nodes.is_empty() {
2135                let changes = self.restyle_nodes_hover(&hover.left_nodes, false);
2136                process_changes(changes);
2137            }
2138            if !hover.entered_nodes.is_empty() {
2139                let changes = self.restyle_nodes_hover(&hover.entered_nodes, true);
2140                process_changes(changes);
2141            }
2142        }
2143
2144        // 3. Process active changes
2145        if let Some(active) = active_changes {
2146            if !active.deactivated.is_empty() {
2147                let changes = self.restyle_nodes_active(&active.deactivated, false);
2148                process_changes(changes);
2149            }
2150            if !active.activated.is_empty() {
2151                let changes = self.restyle_nodes_active(&active.activated, true);
2152                process_changes(changes);
2153            }
2154        }
2155
2156        // If no changes, reset display_list flag
2157        if result.changed_nodes.is_empty() {
2158            result.needs_display_list = false;
2159            result.gpu_only_changes = false;
2160        }
2161
2162        // If layout is needed, display list is also needed
2163        if result.needs_layout {
2164            result.needs_display_list = true;
2165            result.gpu_only_changes = false;
2166        }
2167
2168        result
2169    }
2170
2171    /// Overrides CSS properties for a single node from user code (typically a
2172    /// callback). Writes into `CssPropertyCache::user_overridden_properties`,
2173    /// which `get_property_slow` / `get_property_fast` / `get_computed_value`
2174    /// consult at higher priority than the static CSS cascade - making this
2175    /// the fast path for animating a handful of properties per frame.
2176    ///
2177    /// Passing `CssProperty::Initial` for a property removes any override for
2178    /// that type, restoring the cascaded value. Returns the set of
2179    /// `ChangedCssProperty` entries the caller can feed into the incremental
2180    /// restyle pipeline.
2181    #[must_use]
2182    pub fn restyle_user_property(
2183        &mut self,
2184        node_id: &NodeId,
2185        new_properties: &[CssProperty],
2186    ) -> RestyleNodes {
2187        let mut map = BTreeMap::default();
2188
2189        if new_properties.is_empty() {
2190            return map;
2191        }
2192
2193        let node_count = self.node_data.as_ref().len();
2194        if node_id.index() >= node_count {
2195            return map;
2196        }
2197
2198        let node_data = self.node_data.as_container();
2199        let node_data = &node_data[*node_id];
2200
2201        let node_states = &self.styled_nodes.as_container();
2202        let old_node_state = &node_states[*node_id].styled_node_state;
2203
2204        let changes: Vec<ChangedCssProperty> = {
2205            let css_property_cache = self.get_css_property_cache();
2206
2207            new_properties
2208                .iter()
2209                .filter_map(|new_prop| {
2210                    let old_prop = css_property_cache.get_property_slow(
2211                        node_data,
2212                        node_id,
2213                        old_node_state,
2214                        &new_prop.get_type(),
2215                    );
2216
2217                    let old_prop = old_prop
2218                        .map_or_else(|| CssProperty::auto(new_prop.get_type()), Clone::clone);
2219
2220                    if old_prop == *new_prop {
2221                        None
2222                    } else {
2223                        Some(ChangedCssProperty {
2224                            previous_state: *old_node_state,
2225                            previous_prop: old_prop,
2226                            // overriding a user property does not change the state
2227                            current_state: *old_node_state,
2228                            current_prop: new_prop.clone(),
2229                        })
2230                    }
2231                })
2232                .collect()
2233        };
2234
2235        let css_property_cache_mut = self.get_css_property_cache_mut();
2236
2237        // user_overridden_properties is built lazily (empty after StyledDom
2238        // construction). Grow to cover this node_id before indexing so the
2239        // override path works on any DOM, not just ones that already have
2240        // overrides from a prior mutation.
2241        if css_property_cache_mut.user_overridden_properties.len() < node_count {
2242            css_property_cache_mut
2243                .user_overridden_properties
2244                .resize(node_count, Vec::new());
2245        }
2246
2247        for new_prop in new_properties {
2248            let prop_type = new_prop.get_type();
2249            let vec = &mut css_property_cache_mut.user_overridden_properties[node_id.index()];
2250            if new_prop.is_initial() {
2251                // CssProperty::Initial = remove overridden property
2252                if let Ok(idx) = vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
2253                    vec.remove(idx);
2254                }
2255            } else {
2256                match vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
2257                    Ok(idx) => vec[idx].1 = new_prop.clone(),
2258                    Err(idx) => vec.insert(idx, (prop_type, new_prop.clone())),
2259                }
2260            }
2261        }
2262
2263        // The compact cache is a precomputed per-node array that the layout
2264        // getters read on their FAST PATH (`get_display`, `get_width`, ...)
2265        // BEFORE consulting `user_overridden_properties`. An override that
2266        // changes geometry would therefore be written, reported as changed,
2267        // and then ignored by layout — which is why a runtime
2268        // `display: none -> flex` patch (combobox list, popover, ribbon
2269        // gallery panel) left the node at zero size and invisible.
2270        //
2271        // REBUILD the cache rather than merely dropping it. The builder's
2272        // per-node walk applies `user_overridden_properties` as its last
2273        // step, so the rebuilt cache reflects the patch — and every consumer
2274        // that treats the compact cache as the source of truth keeps
2275        // working. Leaving it `None` until "the next full cascade" was a
2276        // trap: the font phase derives its requirements (font-stack
2277        // signature, font chains, the GC keep-set) from this cache, so the
2278        // very relayout that applies the patch resolved an EMPTY font world
2279        // — the chain cache was replaced with nothing and the patched-in
2280        // subtree's text laid out at zero size (the gallery panel opened as
2281        // an 18px blank strip). Overrides are user-interaction-rate, so the
2282        // rebuild is not a per-frame cost; the animation channel
2283        // (colour/opacity/transform) keeps the fast path untouched.
2284        // INHERITED paint props need the recompute too: a `color` override on
2285        // a container is READ by its text children through the precomputed
2286        // inheritance tables, so skipping the recompute left descendants at
2287        // the stale colour — a colour transition on a DIV animated nothing
2288        // visible (found by the css_anim_perf_transition damage law). The
2289        // per-tick animation channel avoids this whole fn via
2290        // `set_user_property_override_fast` + display-list patching.
2291        if new_properties
2292            .iter()
2293            .any(|p| p.get_type().can_trigger_relayout() || p.get_type().is_inheritable())
2294        {
2295            self.recompute_inheritance_and_compact_cache();
2296            self.get_css_property_cache_mut()
2297                .invalidate_resolved_font_sizes();
2298        }
2299
2300        if !changes.is_empty() {
2301            map.insert(*node_id, changes);
2302        }
2303
2304        map
2305    }
2306
2307    /// Provide (or update) the window's `DynamicSelectorContext` - viewport
2308    /// size, theme, OS, media type - for this DOM's cascade.
2309    ///
2310    /// Inline conditional properties (`CssPropertyWithConditions` with
2311    /// viewport/@media/theme/OS selectors) evaluate against this context in
2312    /// BOTH production readers: `get_property_slow` (per lookup) and the
2313    /// compact-cache builder (at build time). A freshly created `StyledDom`
2314    /// has NO context - non-pseudo conditions do not apply until a window
2315    /// adopts the DOM and calls this, which the layout funnel
2316    /// (`LayoutWindow::layout_and_generate_display_list`) does before every
2317    /// pass.
2318    ///
2319    /// When the context actually changed AND the compact cache says some
2320    /// node's resting style depends on it (`has_dynamic_conditions`), the
2321    /// compact cache is rebuilt and hit-test tags are regenerated (a
2322    /// condition can flip `display`, which decides which nodes carry tags).
2323    /// For the common condition-free DOM a context change costs one bool
2324    /// read.
2325    pub fn set_dynamic_selector_context(
2326        &mut self,
2327        context: azul_css::dynamic_selector::DynamicSelectorContext,
2328    ) {
2329        {
2330            let cache = self.get_css_property_cache_mut();
2331            let same = cache.dynamic_context.as_deref() == Some(&context);
2332            cascade_trace(|| format!("dynamic context offered, unchanged={same}"));
2333            if same {
2334                return;
2335            }
2336            cache.dynamic_context = Some(Box::new(context));
2337        }
2338        // Author-css @-rule conditions are baked at CASCADE time (restyle
2339        // drops non-matching rule blocks), so a context change must re-run
2340        // the author cascade — rebuilding the compact cache alone would
2341        // keep the stale rule selection. Only DOMs whose stylesheet
2342        // actually has conditional rules pay this. `env()` values are baked
2343        // the same way (resolved against the context's safe-area insets),
2344        // so a stylesheet using them counts as conditional too.
2345        let author_conditional = self
2346            .get_css_property_cache()
2347            .retained_author_css
2348            .rules
2349            .as_ref()
2350            .iter()
2351            .any(azul_css::css::CssRuleBlock::depends_on_dynamic_context);
2352        if author_conditional {
2353            self.restyle_retained();
2354        }
2355        let needs_rebuild = self
2356            .get_css_property_cache()
2357            .compact_cache
2358            .as_ref()
2359            .is_none_or(|cc| cc.has_dynamic_conditions);
2360        if needs_rebuild {
2361            self.recompute_inheritance_and_compact_cache();
2362            self.get_css_property_cache_mut()
2363                .invalidate_resolved_font_sizes();
2364            let new_tag_ids = self
2365                .css_property_cache
2366                .downcast_mut()
2367                .generate_tag_ids(&self.node_data.as_container(), &self.node_hierarchy);
2368            self.tag_ids_to_node_ids = new_tag_ids.into();
2369        }
2370    }
2371
2372    /// The viewport-size thresholds (widths, heights, logical px) at which
2373    /// any conditional styling in this DOM can flip: the author
2374    /// stylesheet's `@media (min-/max-width/height)` bounds plus every
2375    /// inline conditional property's `ViewportWidth`/`ViewportHeight`
2376    /// bounds (harvested by the compact-cache builder). Sorted, deduped.
2377    ///
2378    /// `None` when the compact cache has not been built yet (no styling
2379    /// pass) - callers should treat that as "unknown" and fall back to a
2380    /// conservative policy. The engine's resize decision uses this instead
2381    /// of the old hardcoded `CSS_BREAKPOINTS` guess list, which failed both
2382    /// ways: a widget breakpoint like the ribbon's 720px was not on it (so
2383    /// shrinking onto the mobile layout never regenerated), and its eight
2384    /// guessed thresholds fired ~66ms full regenerations on every drag
2385    /// across 640/768/1024/...
2386    #[must_use]
2387    pub fn viewport_breakpoints(&self) -> Option<(Vec<f32>, Vec<f32>)> {
2388        let cache = self.get_css_property_cache();
2389        let cc = cache.compact_cache.as_ref()?;
2390        let (mut w, mut h) = cache.retained_author_css.viewport_breakpoints();
2391        w.extend(cc.inline_viewport_w.iter().copied().map(f32::from_bits));
2392        h.extend(cc.inline_viewport_h.iter().copied().map(f32::from_bits));
2393        w.sort_by_key(|v| v.to_bits());
2394        w.dedup_by_key(|v| v.to_bits());
2395        h.sort_by_key(|v| v.to_bits());
2396        h.dedup_by_key(|v| v.to_bits());
2397        Some((w, h))
2398    }
2399
2400    /// Migrate runtime CSS overrides (`user_overridden_properties`) from a
2401    /// previous generation's property cache onto this DOM, following the
2402    /// reconciliation node matches.
2403    ///
2404    /// State follows node identity across a `RefreshDom` rebuild - exactly
2405    /// like datasets (`diff::transfer_states`), scroll offsets and text
2406    /// cursors already do. Without this, every runtime patch
2407    /// (`set_css_property`) silently reverted on the next app-driven DOM
2408    /// rebuild: the ribbon's collapsed band and an open combobox/gallery
2409    /// panel "un-toggled" whenever any callback returned `RefreshDom` (the
2410    /// ribbon's own tab-click does), because the fresh cascade knows nothing
2411    /// of the old override layer.
2412    ///
2413    /// Rebuilds the compact cache when anything migrated, so the layout fast
2414    /// path sees the carried-over values immediately.
2415    pub fn migrate_user_overrides_from(
2416        &mut self,
2417        old_cache: &CssPropertyCache,
2418        node_moves: &[crate::diff::NodeMove],
2419    ) {
2420        let node_count = self.node_data.as_ref().len();
2421        let mut migrated_any = false;
2422        for m in node_moves {
2423            let Some(old_vec) = old_cache
2424                .user_overridden_properties
2425                .get(m.old_node_id.index())
2426                .filter(|v| !v.is_empty())
2427            else {
2428                continue;
2429            };
2430            let new_idx = m.new_node_id.index();
2431            if new_idx >= node_count {
2432                continue;
2433            }
2434            let old_vec = old_vec.clone();
2435            let cache = self.get_css_property_cache_mut();
2436            if cache.user_overridden_properties.len() < node_count {
2437                cache
2438                    .user_overridden_properties
2439                    .resize(node_count, Vec::new());
2440            }
2441            cache.user_overridden_properties[new_idx] = old_vec;
2442            migrated_any = true;
2443        }
2444        if migrated_any {
2445            self.recompute_inheritance_and_compact_cache();
2446            self.get_css_property_cache_mut()
2447                .invalidate_resolved_font_sizes();
2448        }
2449    }
2450
2451    /// Reconstruct a plain [`Dom`](crate::dom::Dom) from a subtree of this
2452    /// styled DOM by cloning each node's [`NodeData`](crate::dom::NodeData)
2453    /// (ids/classes, inline CSS, callbacks, dataset - `RefAny`/`ImageRef`
2454    /// fields are refcounted handles, so nothing heavy is copied).
2455    ///
2456    /// `root`: the subtree root, or `None` for the DOM's root node.
2457    ///
2458    /// The returned `Dom` CARRIES THE STYLESHEETS: the cascade retains the
2459    /// author CSS (`CssPropertyCache::retained_author_css`), and it is
2460    /// re-attached to the returned root's `css` field - re-styling the
2461    /// reconstruction reproduces the on-screen cascade. For a NON-root
2462    /// subtree this is an approximation: selectors that depended on
2463    /// ancestors OUTSIDE the subtree (descendant combinators through cut-off
2464    /// parents, `:nth-child` against removed siblings) may match differently
2465    /// in the new document. When exact pixel parity matters, hand the whole
2466    /// `StyledDom` clone to the consumer instead (e.g.
2467    /// `Pdf::from_styled_dom_with_resources`), which skips re-cascading
2468    /// entirely.
2469    #[must_use]
2470    pub fn reconstruct_dom_subtree(&self, root: Option<NodeId>) -> Dom {
2471        use crate::dom::NodeData;
2472
2473        let hierarchy = self.node_hierarchy.as_container();
2474        let node_data = self.node_data.as_container();
2475        let root_id = root.unwrap_or(NodeId::ZERO);
2476
2477        let make_dom = |id: NodeId| -> Dom {
2478            Dom {
2479                root: node_data
2480                    .get(id)
2481                    .cloned()
2482                    .unwrap_or_else(NodeData::create_div),
2483                children: Vec::new().into(),
2484                css: Vec::new().into(),
2485                estimated_total_children: 0,
2486            }
2487        };
2488
2489        // Iterative post-order: a node is folded into its parent via
2490        // `add_child` (which maintains `estimated_total_children`) once all
2491        // of its own children are assembled, so arbitrary depth cannot
2492        // overflow the stack.
2493        let mut result_stack: Vec<Dom> = vec![make_dom(root_id)];
2494        let mut visit_stack: Vec<(NodeId, Option<NodeId>)> = vec![(
2495            root_id,
2496            hierarchy
2497                .get(root_id)
2498                .and_then(|n| n.first_child_id(root_id)),
2499        )];
2500
2501        while let Some((node, next_child)) = visit_stack.pop() {
2502            if let Some(child) = next_child {
2503                // Come back to `node` for the sibling AFTER `child`,
2504                // then descend into `child`.
2505                let sibling = hierarchy
2506                    .get(child)
2507                    .and_then(NodeHierarchyItem::next_sibling_id);
2508                visit_stack.push((node, sibling));
2509                result_stack.push(make_dom(child));
2510                visit_stack.push((
2511                    child,
2512                    hierarchy.get(child).and_then(|c| c.first_child_id(child)),
2513                ));
2514            } else {
2515                let Some(finished) = result_stack.pop() else {
2516                    break;
2517                };
2518                if let Some(parent) = result_stack.last_mut() {
2519                    parent.add_child(finished);
2520                } else {
2521                    let mut finished = finished;
2522                    let author_css = self.get_css_property_cache().retained_author_css.clone();
2523                    if !author_css.is_empty() {
2524                        finished.css = vec![author_css].into();
2525                    }
2526                    return finished;
2527                }
2528            }
2529        }
2530
2531        // Unreachable for a well-formed hierarchy; degrade to an empty div.
2532        Dom::create_div()
2533    }
2534
2535    /// Returns a HTML-formatted version of the DOM for easier debugging.
2536    ///
2537    /// For example, a DOM with a parent div containing a child div would return:
2538    ///
2539    /// ```xml,no_run,ignore
2540    /// <div id="hello">
2541    ///      <div id="test" />
2542    /// </div>
2543    /// ```
2544    #[must_use]
2545    pub fn get_html_string(&self, custom_head: &str, custom_body: &str, test_mode: bool) -> String {
2546        let css_property_cache = self.get_css_property_cache();
2547
2548        let mut output = String::new();
2549
2550        // After which nodes should a close tag be printed?
2551        let mut should_print_close_tag_after_node: BTreeMap<NodeId, Vec<(NodeId, usize)>> =
2552            BTreeMap::new();
2553
2554        let should_print_close_tag_debug = self
2555            .non_leaf_nodes
2556            .iter()
2557            .filter_map(|p| {
2558                let parent_node_id = p.node_id.into_crate_internal()?;
2559                let mut total_last_child = None;
2560                recursive_get_last_child(
2561                    parent_node_id,
2562                    self.node_hierarchy.as_ref(),
2563                    &mut total_last_child,
2564                );
2565                let total_last_child = total_last_child?;
2566                Some((parent_node_id, (total_last_child, p.depth)))
2567            })
2568            .collect::<BTreeMap<_, _>>();
2569
2570        for (parent_id, (last_child, parent_depth)) in should_print_close_tag_debug {
2571            should_print_close_tag_after_node
2572                .entry(last_child)
2573                .or_default()
2574                .push((parent_id, parent_depth));
2575        }
2576
2577        let mut all_node_depths = self
2578            .non_leaf_nodes
2579            .iter()
2580            .filter_map(|p| {
2581                let parent_node_id = p.node_id.into_crate_internal()?;
2582                Some((parent_node_id, p.depth))
2583            })
2584            .collect::<BTreeMap<_, _>>();
2585
2586        for (parent_node_id, parent_depth) in self
2587            .non_leaf_nodes
2588            .iter()
2589            .filter_map(|p| Some((p.node_id.into_crate_internal()?, p.depth)))
2590        {
2591            for child_id in parent_node_id.az_children(&self.node_hierarchy.as_container()) {
2592                all_node_depths.insert(child_id, parent_depth + 1);
2593            }
2594        }
2595
2596        for node_id in self.node_hierarchy.as_container().linear_iter() {
2597            // A single-node DOM (or any node not reached as a non-leaf parent or
2598            // one of their children, e.g. a lone root) has no entry here; treat
2599            // its depth as 0 instead of panic-indexing the map.
2600            let depth = all_node_depths.get(&node_id).copied().unwrap_or(0);
2601
2602            let node_data = &self.node_data.as_container()[node_id];
2603            let node_state = &self.styled_nodes.as_container()[node_id].styled_node_state;
2604            let tabs = String::from("    ").repeat(depth);
2605
2606            output.push_str("\r\n");
2607            output.push_str(&tabs);
2608            output.push_str(&node_data.debug_print_start(css_property_cache, &node_id, node_state));
2609
2610            if let Some(content) = node_data.get_node_type().format().as_ref() {
2611                output.push_str(content);
2612            }
2613
2614            let node_has_children = self.node_hierarchy.as_container()[node_id]
2615                .first_child_id(node_id)
2616                .is_some();
2617            if !node_has_children {
2618                let node_data = &self.node_data.as_container()[node_id];
2619                output.push_str(&node_data.debug_print_end());
2620            }
2621
2622            if let Some(close_tag_vec) = should_print_close_tag_after_node.get(&node_id) {
2623                let mut close_tag_vec = close_tag_vec.clone();
2624                close_tag_vec.sort_by(|a, b| b.1.cmp(&a.1)); // sort by depth descending
2625                for (close_tag_parent_id, close_tag_depth) in close_tag_vec {
2626                    let node_data = &self.node_data.as_container()[close_tag_parent_id];
2627                    let tabs = String::from("    ").repeat(close_tag_depth);
2628                    output.push_str("\r\n");
2629                    output.push_str(&tabs);
2630                    output.push_str(&node_data.debug_print_end());
2631                }
2632            }
2633        }
2634
2635        if test_mode {
2636            output
2637        } else {
2638            format!(
2639                "
2640                <html>
2641                    <head>
2642                    <style>* {{ margin:0px; padding:0px; }}</style>
2643                    {custom_head}
2644                    </head>
2645                {output}
2646                {custom_body}
2647                </html>
2648            "
2649            )
2650        }
2651    }
2652
2653    /// Returns nodes grouped by their rendering order (respects z-index and position).
2654    #[must_use]
2655    pub fn get_rects_in_rendering_order(&self) -> ContentGroup {
2656        Self::determine_rendering_order(
2657            self.non_leaf_nodes.as_ref(),
2658            &self.node_hierarchy.as_container(),
2659            &self.styled_nodes.as_container(),
2660            &self.node_data.as_container(),
2661            self.get_css_property_cache(),
2662        )
2663    }
2664
2665    /// Returns the rendering order of the items (the rendering
2666    /// order doesn't have to be the original order)
2667    fn determine_rendering_order(
2668        non_leaf_nodes: &[ParentWithNodeDepth],
2669        node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
2670        styled_nodes: &NodeDataContainerRef<'_, StyledNode>,
2671        node_data_container: &NodeDataContainerRef<'_, NodeData>,
2672        css_property_cache: &CssPropertyCache,
2673    ) -> ContentGroup {
2674        let children_sorted = non_leaf_nodes
2675            .iter()
2676            .filter_map(|parent| {
2677                Some((
2678                    parent.node_id,
2679                    sort_children_by_position(
2680                        parent.node_id.into_crate_internal()?,
2681                        node_hierarchy,
2682                        styled_nodes,
2683                        node_data_container,
2684                        css_property_cache,
2685                    ),
2686                ))
2687            })
2688            .collect::<Vec<_>>();
2689
2690        let children_sorted: BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>> =
2691            children_sorted.into_iter().collect();
2692
2693        let mut root_content_group = ContentGroup {
2694            root: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
2695            children: Vec::new().into(),
2696        };
2697
2698        fill_content_group_children(&mut root_content_group, &children_sorted);
2699
2700        root_content_group
2701    }
2702
2703    /// Replaces this `StyledDom` with default and returns the old value.
2704    #[must_use]
2705    pub fn swap_with_default(&mut self) -> Self {
2706        let mut new = Self::default();
2707        core::mem::swap(self, &mut new);
2708        new
2709    }
2710}
2711
2712/// Same as `Dom`, but arena-based for more efficient memory layout and faster traversal.
2713#[derive(Debug, PartialEq, PartialOrd, Eq)]
2714pub struct CompactDom {
2715    /// The arena containing the hierarchical relationships (parent, child, sibling) of all nodes.
2716    pub node_hierarchy: NodeHierarchy,
2717    /// The arena containing the actual data (`NodeData`) for each node.
2718    pub node_data: NodeDataContainer<NodeData>,
2719    /// The ID of the root node of the DOM tree.
2720    pub root: NodeId,
2721}
2722
2723impl CompactDom {
2724    /// Returns the number of nodes in this DOM.
2725    #[inline]
2726    #[must_use]
2727    pub fn len(&self) -> usize {
2728        self.node_hierarchy.as_ref().len()
2729    }
2730
2731    /// Returns `true` if this DOM has no nodes.
2732    #[inline]
2733    #[must_use]
2734    pub fn is_empty(&self) -> bool {
2735        self.node_hierarchy.as_ref().is_empty()
2736    }
2737}
2738
2739impl From<Dom> for CompactDom {
2740    fn from(dom: Dom) -> Self {
2741        convert_dom_into_compact_dom(dom)
2742    }
2743}
2744
2745/// Converts a tree-based Dom into an arena-based `CompactDom` for efficient traversal.
2746#[must_use]
2747pub fn convert_dom_into_compact_dom(mut dom: Dom) -> CompactDom {
2748    // note: somehow convert this into a non-recursive form later on!
2749    fn convert_dom_into_compact_dom_internal(
2750        dom: &mut Dom,
2751        node_hierarchy: &mut [Node],
2752        node_data: &mut Vec<NodeData>,
2753        parent_node_id: NodeId,
2754        node: Node,
2755        cur_node_id: &mut usize,
2756    ) {
2757        // - parent [0]
2758        //    - child [1]
2759        //    - child [2]
2760        //        - child of child 2 [2]
2761        //        - child of child 2 [4]
2762        //    - child [5]
2763        //    - child [6]
2764        //        - child of child 4 [7]
2765
2766        // Write node into the arena here!
2767        node_hierarchy[parent_node_id.index()] = node;
2768
2769        // MOVE the node's inline `style` AND its `extra` (NodeDataExt) box instead of relying on
2770        // copy_special's `self.style.clone()` / `self.extra.clone()`. Both derived Clones lower to
2771        // indirect-jump jump tables that remill mis-lifts on the web backend: CssProperty's clone
2772        // comes back with discriminant 0 (drops simple inline CSS) and for COMPLEX values (AzButton's
2773        // gradient; the NodeDataExt attributes Vec) the mis-lifted clone reads/writes wrong-sized data,
2774        // which clobbers the adjacent `style` temporary → "memory access out of bounds" later in the
2775        // cascade (StyledDom::create → restyle's inheritance loop reads the corrupted style). 2026-06-02:
2776        // copy_special_moving_complex mem::takes BOTH style+extra before copy_special, so copy_special
2777        // clones an EMPTY style + None extra (no broken clone runs) and restores them after. (Extra was
2778        // added after the AzButton ids/classes node — which lazily allocates NodeDataExt — OOB'd even
2779        // with the style-only take.) The Dom is consumed here, so the move is correct.
2780        let copy = dom.root.copy_special_moving_complex();
2781
2782        node_data[parent_node_id.index()] = copy;
2783
2784        *cur_node_id += 1;
2785
2786        let mut previous_sibling_id = None;
2787        let children_len = dom.children.len();
2788        for (child_index, child_dom) in dom.children.as_mut().iter_mut().enumerate() {
2789            let child_node_id = NodeId::new(*cur_node_id);
2790            let is_last_child = (child_index + 1) == children_len;
2791            let child_dom_is_empty = child_dom.children.is_empty();
2792            let child_node = Node {
2793                parent: Some(parent_node_id),
2794                previous_sibling: previous_sibling_id,
2795                next_sibling: if is_last_child {
2796                    None
2797                } else {
2798                    Some(child_node_id + child_dom.estimated_total_children + 1)
2799                },
2800                last_child: if child_dom_is_empty {
2801                    None
2802                } else {
2803                    Some(child_node_id + child_dom.estimated_total_children)
2804                },
2805            };
2806            previous_sibling_id = Some(child_node_id);
2807            // recurse BEFORE adding the next child
2808            convert_dom_into_compact_dom_internal(
2809                child_dom,
2810                node_hierarchy,
2811                node_data,
2812                child_node_id,
2813                child_node,
2814                cur_node_id,
2815            );
2816        }
2817
2818        // AUTHORITATIVE last_child. The per-child `last_child` set at construction used
2819        // `child_node_id + estimated_total_children`, which is the last node of the
2820        // whole SUBTREE (its deepest descendant), NOT the last DIRECT child — wrong
2821        // whenever that last child has children of its own. It corrupted `last_child_id()`
2822        // and, through it, append_child (which spliced onto the wrong node). The loop
2823        // above already tracked `previous_sibling_id`, which now holds the real last
2824        // direct child (None if there were none), so overwrite with it. This runs for
2825        // every node including the root, so it also corrects the root's own computation.
2826        node_hierarchy[parent_node_id.index()].last_child = previous_sibling_id;
2827    }
2828
2829    // Pre-allocate all nodes (+ 1 root node)
2830    let sum_nodes = dom.fixup_children_estimated();
2831
2832    let mut node_hierarchy = vec![Node::ROOT; sum_nodes + 1];
2833    let mut node_data = vec![NodeData::create_div(); sum_nodes + 1];
2834    let mut cur_node_id = 0;
2835
2836    let root_node_id = NodeId::ZERO;
2837    let root_node = Node {
2838        parent: None,
2839        previous_sibling: None,
2840        next_sibling: None,
2841        last_child: if dom.children.is_empty() {
2842            None
2843        } else {
2844            Some(root_node_id + dom.estimated_total_children)
2845        },
2846    };
2847
2848    convert_dom_into_compact_dom_internal(
2849        &mut dom,
2850        &mut node_hierarchy,
2851        &mut node_data,
2852        root_node_id,
2853        root_node,
2854        &mut cur_node_id,
2855    );
2856
2857    CompactDom {
2858        node_hierarchy: NodeHierarchy {
2859            internal: node_hierarchy,
2860        },
2861        node_data: NodeDataContainer {
2862            internal: node_data,
2863        },
2864        root: root_node_id,
2865    }
2866}
2867
2868/// #47: scope every node's inline css to its own subtree. Walks the tree in the
2869/// SAME pre-order `convert_dom_into_compact_dom` uses to assign flat `NodeIds`, so the
2870/// `[flat_id, flat_id + estimated_total_children]` range pushed onto each rule (via
2871/// `CssPath::push_front_scope`) matches the ids the cascade will later see. After
2872/// this, a node's `with_css`/`set_css` rules can only match nodes inside its subtree;
2873/// they can no longer leak to the whole tree. `fixup_children_estimated()` must
2874/// have run first so `estimated_total_children` is populated/exact.
2875fn scope_inline_css(dom: &mut Dom, next_id: &mut usize) {
2876    let start = *next_id;
2877    let end = start + dom.estimated_total_children;
2878    for css in dom.css.as_mut().iter_mut() {
2879        for rule in css.rules.as_mut().iter_mut() {
2880            // Bare-decl wrappers (INLINE priority, from set_css/with_css
2881            // selector-less declarations) are scoped node-only so a non-root
2882            // background can't leak to descendants (#47). A stylesheet's
2883            // `* { ... }` (AUTHOR/UA priority) scopes to the SUBTREE - the
2884            // classic `* { margin: 0 }` reset must reach every element of the
2885            // mounted document, not just the mount root.
2886            let node_only = rule.priority >= azul_css::css::rule_priority::INLINE;
2887            rule.path.push_front_scope_for(start, end, node_only);
2888        }
2889    }
2890    *next_id += 1;
2891    for child in dom.children.as_mut().iter_mut() {
2892        scope_inline_css(child, next_id);
2893    }
2894}
2895
2896/// Recursively collect all CSS objects from a Dom tree (depth-first).
2897/// Inner (deeper) CSS objects come first, outer (shallower) CSS objects come last.
2898/// This means outer CSS has higher cascade priority when applied in order.
2899fn collect_css_from_dom(dom: &Dom, out: &mut Vec<Css>) {
2900    // First, recurse into children (inner CSS = lower priority)
2901    for child in &dom.children {
2902        collect_css_from_dom(child, out);
2903    }
2904    // Then, add this node's CSS objects (outer CSS = higher priority)
2905    for css in &dom.css {
2906        out.push(css.clone());
2907    }
2908}
2909
2910/// Recursively strip CSS from all Dom nodes (sets css to empty vec).
2911/// Called after collecting CSS so the `CompactDom` doesn't carry CSS data.
2912fn strip_css_from_dom(dom: &mut Dom) {
2913    dom.css = Vec::new().into();
2914    for child in dom.children.as_mut().iter_mut() {
2915        strip_css_from_dom(child);
2916    }
2917}
2918
2919fn fill_content_group_children(
2920    group: &mut ContentGroup,
2921    children_sorted: &BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>>,
2922) {
2923    if let Some(c) = children_sorted.get(&group.root) {
2924        // returns None for leaf nodes
2925        group.children = c
2926            .iter()
2927            .map(|child| ContentGroup {
2928                root: *child,
2929                children: Vec::new().into(),
2930            })
2931            .collect::<Vec<ContentGroup>>()
2932            .into();
2933
2934        for c in group.children.as_mut() {
2935            fill_content_group_children(c, children_sorted);
2936        }
2937    }
2938}
2939
2940fn sort_children_by_position(
2941    parent: NodeId,
2942    node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
2943    rectangles: &NodeDataContainerRef<'_, StyledNode>,
2944    node_data_container: &NodeDataContainerRef<'_, NodeData>,
2945    css_property_cache: &CssPropertyCache,
2946) -> Vec<NodeHierarchyItemId> {
2947    use azul_css::props::layout::LayoutPosition::Absolute;
2948
2949    let children_positions = parent
2950        .az_children(node_hierarchy)
2951        .map(|nid| {
2952            let position = css_property_cache
2953                .get_position(
2954                    &node_data_container[nid],
2955                    &nid,
2956                    &rectangles[nid].styled_node_state,
2957                )
2958                .and_then(|p| (*p).get_property_or_default())
2959                .unwrap_or_default();
2960            let id = NodeHierarchyItemId::from_crate_internal(Some(nid));
2961            (id, position)
2962        })
2963        .collect::<Vec<_>>();
2964
2965    let mut not_absolute_children = children_positions
2966        .iter()
2967        .filter_map(|(node_id, position)| {
2968            if *position == Absolute {
2969                None
2970            } else {
2971                Some(*node_id)
2972            }
2973        })
2974        .collect::<Vec<_>>();
2975
2976    let mut absolute_children = children_positions
2977        .iter()
2978        .filter_map(|(node_id, position)| {
2979            if *position == Absolute {
2980                Some(*node_id)
2981            } else {
2982                None
2983            }
2984        })
2985        .collect::<Vec<_>>();
2986
2987    // Append the position:absolute children after the regular children
2988    not_absolute_children.append(&mut absolute_children);
2989    not_absolute_children
2990}
2991
2992// calls get_last_child() recursively until the last child of the last child of the ... has been
2993// found
2994fn recursive_get_last_child(
2995    node_id: NodeId,
2996    node_hierarchy: &[NodeHierarchyItem],
2997    target: &mut Option<NodeId>,
2998) {
2999    match node_hierarchy[node_id.index()].last_child_id() {
3000        None => (),
3001        Some(s) => {
3002            *target = Some(s);
3003            recursive_get_last_child(s, node_hierarchy, target);
3004        }
3005    }
3006}
3007
3008// ============================================================================
3009// DOM TRAVERSAL FOR MULTI-NODE SELECTION
3010// ============================================================================
3011
3012/// Determine if `node_a` comes before `node_b` in document order.
3013///
3014/// Document order is defined as pre-order depth-first traversal order.
3015/// This is equivalent to the order nodes appear in HTML source.
3016///
3017/// ## Algorithm
3018/// 1. Find the path from root to each node
3019/// 2. Find the Lowest Common Ancestor (LCA)
3020/// 3. At the divergence point, the child that appears first in sibling order comes first
3021#[must_use]
3022pub fn is_before_in_document_order(
3023    hierarchy: &NodeHierarchyItemVec,
3024    node_a: NodeId,
3025    node_b: NodeId,
3026) -> bool {
3027    if node_a == node_b {
3028        return false;
3029    }
3030
3031    let hierarchy = hierarchy.as_container();
3032
3033    // Get paths from root to each node (stored as root-first order)
3034    let path_a = get_path_to_root(&hierarchy, node_a);
3035    let path_b = get_path_to_root(&hierarchy, node_b);
3036
3037    // Find divergence point (last common ancestor)
3038    let min_len = path_a.len().min(path_b.len());
3039
3040    for i in 0..min_len {
3041        if path_a[i] != path_b[i] {
3042            // Found divergence - check which sibling comes first
3043            let child_towards_a = path_a[i];
3044            let child_towards_b = path_b[i];
3045
3046            // A smaller NodeId index means it was created earlier in DOM construction,
3047            // which means it comes first in document order for siblings
3048            return child_towards_a.index() < child_towards_b.index();
3049        }
3050    }
3051
3052    // One path is a prefix of the other - the shorter path (ancestor) comes first
3053    path_a.len() < path_b.len()
3054}
3055
3056/// Get the path from root to a node, returned in root-first order.
3057fn get_path_to_root(
3058    hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
3059    node: NodeId,
3060) -> Vec<NodeId> {
3061    let mut path = Vec::new();
3062    let mut current = Some(node);
3063
3064    while let Some(node_id) = current {
3065        path.push(node_id);
3066        current = hierarchy
3067            .get(node_id)
3068            .and_then(NodeHierarchyItem::parent_id);
3069    }
3070
3071    // Reverse to get root-first order
3072    path.reverse();
3073    path
3074}
3075
3076/// Collect all nodes between start and end (inclusive) in document order.
3077///
3078/// This performs a pre-order depth-first traversal starting from the root,
3079/// collecting nodes once we've seen `start` and stopping at `end`.
3080///
3081/// ## Parameters
3082/// * `hierarchy` - The node hierarchy
3083/// * `start_node` - First node in document order
3084/// * `end_node` - Last node in document order
3085///
3086/// ## Returns
3087/// Vector of `NodeIds` in document order, from start to end (inclusive)
3088#[must_use]
3089pub fn collect_nodes_in_document_order(
3090    hierarchy: &NodeHierarchyItemVec,
3091    start_node: NodeId,
3092    end_node: NodeId,
3093) -> Vec<NodeId> {
3094    if start_node == end_node {
3095        return vec![start_node];
3096    }
3097
3098    let hierarchy_container = hierarchy.as_container();
3099    let hierarchy_slice = hierarchy.as_ref();
3100
3101    let mut result = Vec::new();
3102    let mut in_range = false;
3103
3104    // Pre-order DFS using a stack
3105    // We need to traverse in document order, which is pre-order DFS
3106    let mut stack: Vec<NodeId> = vec![NodeId::ZERO]; // Start from root
3107
3108    while let Some(current) = stack.pop() {
3109        // Check if we've entered the range
3110        if current == start_node {
3111            in_range = true;
3112        }
3113
3114        // Collect if in range
3115        if in_range {
3116            result.push(current);
3117        }
3118
3119        // Check if we've exited the range
3120        if current == end_node {
3121            break;
3122        }
3123
3124        // Push children in reverse order so they pop in correct order
3125        // (first child should be processed first)
3126        if let Some(item) = hierarchy_container.get(current) {
3127            // Get first child
3128            if let Some(first_child) = item.first_child_id(current) {
3129                // Collect all children by following next_sibling
3130                let mut children = Vec::new();
3131                let mut child = Some(first_child);
3132                while let Some(child_id) = child {
3133                    children.push(child_id);
3134                    child = hierarchy_container
3135                        .get(child_id)
3136                        .and_then(NodeHierarchyItem::next_sibling_id);
3137                }
3138                // Push in reverse order for correct DFS order
3139                for child_id in children.into_iter().rev() {
3140                    stack.push(child_id);
3141                }
3142            }
3143        }
3144    }
3145
3146    result
3147}
3148
3149/// Check if two `StyledDom`s are structurally equivalent for layout purposes.
3150///
3151/// Returns `true` if the DOMs have the same structure, node types, classes,
3152/// IDs, inline styles, and callback event registrations - meaning the
3153/// layout output would be identical.
3154///
3155/// Image callback nodes are compared by function pointer and `RefAny` type ID
3156/// rather than heap pointer, since each `layout()` call creates new `ImageRef`
3157/// allocations even when the callback is the same.
3158///
3159/// This is used to short-circuit the expensive layout pipeline when the DOM
3160/// hasn't actually changed (e.g., an animation timer fires but only the GL
3161/// texture content changed, not the DOM structure).
3162#[must_use]
3163pub fn is_layout_equivalent(old: &StyledDom, new: &StyledDom) -> bool {
3164    use crate::dom::NodeType;
3165    use crate::resources::DecodedImage;
3166
3167    // Quick check: node count must match
3168    let old_nodes = old.node_data.as_ref();
3169    let new_nodes = new.node_data.as_ref();
3170    if old_nodes.len() != new_nodes.len() {
3171        return false;
3172    }
3173
3174    // Check hierarchy (parent/child/sibling structure)
3175    let old_hier = old.node_hierarchy.as_ref();
3176    let new_hier = new.node_hierarchy.as_ref();
3177    if old_hier.len() != new_hier.len() {
3178        return false;
3179    }
3180    if old_hier != new_hier {
3181        return false;
3182    }
3183
3184    // Per-node comparison
3185    for (old_node, new_node) in old_nodes.iter().zip(new_nodes.iter()) {
3186        // Compare node type discriminant
3187        if core::mem::discriminant(&old_node.node_type)
3188            != core::mem::discriminant(&new_node.node_type)
3189        {
3190            return false;
3191        }
3192
3193        // Compare node type content (with special handling for image callbacks)
3194        match (&old_node.node_type, &new_node.node_type) {
3195            (NodeType::Image(old_img), NodeType::Image(new_img)) => {
3196                match (old_img.get_data(), new_img.get_data()) {
3197                    (DecodedImage::Callback(old_cb), DecodedImage::Callback(new_cb)) => {
3198                        // Compare callback function pointer (stable across frames)
3199                        if old_cb.callback.cb != new_cb.callback.cb {
3200                            return false;
3201                        }
3202                        // Compare RefAny type ID (not instance pointer)
3203                        if old_cb.refany.get_type_id() != new_cb.refany.get_type_id() {
3204                            return false;
3205                        }
3206                    }
3207                    _ => {
3208                        // Raw images / GL textures: compare by pointer identity
3209                        if old_img != new_img {
3210                            return false;
3211                        }
3212                    }
3213                }
3214            }
3215            _ => {
3216                if old_node.node_type != new_node.node_type {
3217                    return false;
3218                }
3219            }
3220        }
3221
3222        // Compare IDs and classes (now stored in attributes as AttributeType::Id/Class)
3223        {
3224            use crate::dom::AttributeType;
3225            let old_ids_classes: Vec<_> = old_node
3226                .attributes()
3227                .as_ref()
3228                .iter()
3229                .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
3230                .collect();
3231            let new_ids_classes: Vec<_> = new_node
3232                .attributes()
3233                .as_ref()
3234                .iter()
3235                .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
3236                .collect();
3237            if old_ids_classes != new_ids_classes {
3238                return false;
3239            }
3240        }
3241
3242        // Compare inline CSS (direct layout input)
3243        if old_node.style != new_node.style {
3244            return false;
3245        }
3246
3247        // Compare callback event types (affects hit-test tags)
3248        // We compare only event types, not function pointers or data
3249        let old_cbs = old_node.callbacks.as_ref();
3250        let new_cbs = new_node.callbacks.as_ref();
3251        if old_cbs.len() != new_cbs.len() {
3252            return false;
3253        }
3254        for (old_cb, new_cb) in old_cbs.iter().zip(new_cbs.iter()) {
3255            if old_cb.event != new_cb.event {
3256                return false;
3257            }
3258        }
3259
3260        // Compare attributes (some affect layout, e.g. colspan)
3261        if old_node.attributes().as_ref() != new_node.attributes().as_ref() {
3262            return false;
3263        }
3264    }
3265
3266    // Compare styled node states (hover/focus/active flags affect CSS resolution)
3267    let old_styled = old.styled_nodes.as_ref();
3268    let new_styled = new.styled_nodes.as_ref();
3269    if old_styled.len() != new_styled.len() {
3270        return false;
3271    }
3272    if old_styled != new_styled {
3273        return false;
3274    }
3275
3276    true
3277}
3278
3279#[cfg(test)]
3280#[path = "styled_dom_test.rs"]
3281mod styled_dom_test;