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            RelayoutScope,
27            FlowFromValue, FlowIntoValue, LayoutAlignContentValue, LayoutAlignItemsValue,
28            LayoutAlignSelfValue, LayoutBorderBottomWidthValue, LayoutBorderLeftWidthValue,
29            LayoutBorderRightWidthValue, LayoutBorderTopWidthValue, LayoutBoxSizingValue,
30            LayoutClearValue, LayoutColumnGapValue, LayoutDisplayValue, LayoutFlexBasisValue,
31            LayoutFlexDirectionValue, LayoutFlexGrowValue, LayoutFlexShrinkValue,
32            LayoutFlexWrapValue, LayoutFloatValue, LayoutGapValue, LayoutGridAutoColumnsValue,
33            LayoutGridAutoFlowValue, LayoutGridAutoRowsValue, LayoutGridColumnValue,
34            LayoutGridRowValue, LayoutGridTemplateColumnsValue, LayoutGridTemplateRowsValue,
35            LayoutHeightValue, LayoutInsetBottomValue, LayoutJustifyContentValue,
36            LayoutJustifyItemsValue, LayoutJustifySelfValue, LayoutLeftValue,
37            LayoutMarginBottomValue, LayoutMarginLeftValue, LayoutMarginRightValue,
38            LayoutMarginTopValue, LayoutMaxHeightValue, LayoutMaxWidthValue, LayoutMinHeightValue,
39            LayoutMinWidthValue, LayoutOverflowValue, LayoutPaddingBottomValue,
40            LayoutPaddingLeftValue, LayoutPaddingRightValue, LayoutPaddingTopValue,
41            LayoutPositionValue, LayoutRightValue, LayoutRowGapValue, LayoutScrollbarWidthValue,
42            LayoutTextJustifyValue, LayoutTopValue, LayoutWidthValue, LayoutWritingModeValue,
43            LayoutZIndexValue, OrphansValue, PageBreakValue,
44            SelectionBackgroundColorValue, SelectionColorValue, ShapeImageThresholdValue,
45            ShapeMarginValue, ShapeOutsideValue, StringSetValue, StyleBackfaceVisibilityValue,
46            StyleBackgroundContentVecValue, StyleBackgroundPositionVecValue,
47            StyleBackgroundRepeatVecValue, StyleBackgroundSizeVecValue,
48            StyleBorderBottomColorValue, StyleBorderBottomLeftRadiusValue,
49            StyleBorderBottomRightRadiusValue, StyleBorderBottomStyleValue,
50            StyleBorderLeftColorValue, StyleBorderLeftStyleValue, StyleBorderRightColorValue,
51            StyleBorderRightStyleValue, StyleBorderTopColorValue, StyleBorderTopLeftRadiusValue,
52            StyleBorderTopRightRadiusValue, StyleBorderTopStyleValue, StyleBoxShadowValue,
53            StyleCursorValue, StyleDirectionValue, StyleFilterVecValue, StyleFontFamilyVecValue,
54            StyleFontSizeValue, StyleFontValue, StyleHyphensValue, StyleLetterSpacingValue,
55            StyleLineHeightValue, StyleMixBlendModeValue, StyleOpacityValue,
56            StylePerspectiveOriginValue, StyleScrollbarColorValue, StyleTabSizeValue,
57            StyleTextAlignValue, StyleTextColorValue, StyleTransformOriginValue,
58            StyleTransformVecValue, StyleVisibilityValue, StyleWhiteSpaceValue,
59            StyleWordSpacingValue, WidowsValue,
60        },
61        style::StyleTextColor,
62    },
63    AzString,
64};
65
66use crate::{
67    callbacks::Update,
68    dom::{Dom, DomId, NodeData, NodeDataVec, OptionTabIndex, TabIndex, TagId},
69    events::{RelayoutNodes, RestyleNodes},
70    id::{
71        Node, NodeDataContainer, NodeDataContainerRef, NodeDataContainerRefMut, NodeHierarchy,
72        NodeId,
73    },
74    menu::Menu,
75    prop_cache::{CssPropertyCache, CssPropertyCachePtr},
76    refany::RefAny,
77    resources::{Au, ImageCache, ImageRef, ImmediateFontId, RendererResources},
78    style::{
79        construct_html_cascade_tree, matches_html_element, rule_ends_with, CascadeInfo,
80        CascadeInfoVec,
81    },
82    FastBTreeSet, OrderedMap,
83};
84
85#[repr(C)]
86#[derive(Debug, Clone, PartialEq, Hash, PartialOrd, Eq, Ord)]
87pub struct ChangedCssProperty {
88    pub previous_state: StyledNodeState,
89    pub previous_prop: CssProperty,
90    pub current_state: StyledNodeState,
91    pub current_prop: CssProperty,
92}
93
94impl_option!(
95    ChangedCssProperty,
96    OptionChangedCssProperty,
97    copy = false,
98    [Debug, Clone, PartialEq, Hash, PartialOrd, Eq, Ord]
99);
100
101impl_vec!(ChangedCssProperty, ChangedCssPropertyVec, ChangedCssPropertyVecDestructor, ChangedCssPropertyVecDestructorType, ChangedCssPropertyVecSlice, OptionChangedCssProperty);
102impl_vec_debug!(ChangedCssProperty, ChangedCssPropertyVec);
103impl_vec_partialord!(ChangedCssProperty, ChangedCssPropertyVec);
104impl_vec_clone!(
105    ChangedCssProperty,
106    ChangedCssPropertyVec,
107    ChangedCssPropertyVecDestructor
108);
109impl_vec_partialeq!(ChangedCssProperty, ChangedCssPropertyVec);
110
111/// Focus state change for restyle operations
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub struct FocusChange {
114    /// Node that lost focus (if any)
115    pub lost_focus: Option<NodeId>,
116    /// Node that gained focus (if any)
117    pub gained_focus: Option<NodeId>,
118}
119
120/// Hover state change for restyle operations
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct HoverChange {
123    /// Nodes that the mouse left
124    pub left_nodes: Vec<NodeId>,
125    /// Nodes that the mouse entered
126    pub entered_nodes: Vec<NodeId>,
127}
128
129/// Active (mouse down) state change for restyle operations
130#[derive(Debug, Clone, PartialEq, Eq)]
131pub struct ActiveChange {
132    /// Nodes that were deactivated (mouse up)
133    pub deactivated: Vec<NodeId>,
134    /// Nodes that were activated (mouse down)
135    pub activated: Vec<NodeId>,
136}
137
138/// Result of a restyle operation, indicating what needs to be updated
139#[derive(Debug, Clone, Default)]
140pub struct RestyleResult {
141    /// Nodes whose CSS properties changed, with details of the changes
142    pub changed_nodes: RestyleNodes,
143    /// Whether layout needs to be recalculated (layout properties changed)
144    pub needs_layout: bool,
145    /// Whether display list needs regeneration (visual properties changed)
146    pub needs_display_list: bool,
147    /// Whether only GPU-level properties changed (opacity, transform)
148    /// If true and `needs_display_list` is false, we can update via GPU without display list rebuild
149    pub gpu_only_changes: bool,
150    /// The highest `RelayoutScope` seen across all property changes.
151    ///
152    /// This enables the IFC incremental layout optimization (Phase 2):
153    /// - `None`      → repaint only, zero layout work
154    /// - `IfcOnly`   → only the affected IFC needs re-shaping/repositioning
155    /// - `SizingOnly`→ this node's size changed, parent repositions siblings
156    /// - `Full`      → full subtree relayout
157    ///
158    /// When `max_relayout_scope <= IfcOnly`, the layout engine can skip
159    /// full `calculate_layout_for_subtree` and use the IFC fast path instead.
160    pub max_relayout_scope: RelayoutScope,
161}
162
163impl RestyleResult {
164    /// Returns true if any changes occurred
165    #[must_use] pub fn has_changes(&self) -> bool {
166        !self.changed_nodes.is_empty()
167    }
168
169    /// Merge another `RestyleResult` into this one
170    pub fn merge(&mut self, other: Self) {
171        for (node_id, changes) in other.changed_nodes {
172            self.changed_nodes.entry(node_id).or_default().extend(changes);
173        }
174        self.needs_layout = self.needs_layout || other.needs_layout;
175        self.needs_display_list = self.needs_display_list || other.needs_display_list;
176        self.gpu_only_changes = self.gpu_only_changes && other.gpu_only_changes;
177        // Keep the highest (most expensive) scope
178        if other.max_relayout_scope > self.max_relayout_scope {
179            self.max_relayout_scope = other.max_relayout_scope;
180        }
181    }
182}
183
184/// NOTE: multiple states can be active at the same time
185///
186/// Tracks all CSS pseudo-class states for a node.
187/// Each flag is independent - a node can be both :hover and :focus simultaneously.
188#[repr(C)]
189#[derive(Clone, Copy, PartialEq, Hash, PartialOrd, Eq, Ord, Default)]
190pub struct StyledNodeState {
191    /// Element is being hovered (:hover)
192    pub hover: bool,
193    /// Element is active/being clicked (:active)
194    pub active: bool,
195    /// Element has focus (:focus)
196    pub focused: bool,
197    /// Element is disabled (:disabled)
198    pub disabled: bool,
199    /// Element is checked/selected (:checked)
200    pub checked: bool,
201    /// Element or descendant has focus (:focus-within)
202    pub focus_within: bool,
203    /// Link has been visited (:visited)
204    pub visited: bool,
205    /// Window is not focused (:backdrop) - GTK compatibility
206    pub backdrop: bool,
207    /// Element is currently being dragged (:dragging)
208    pub dragging: bool,
209    /// A dragged element is over this drop target (:drag-over)
210    pub drag_over: bool,
211}
212
213impl fmt::Debug for StyledNodeState {
214    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215        let mut v = Vec::new();
216        if self.hover {
217            v.push("hover");
218        }
219        if self.active {
220            v.push("active");
221        }
222        if self.focused {
223            v.push("focused");
224        }
225        if self.disabled {
226            v.push("disabled");
227        }
228        if self.checked {
229            v.push("checked");
230        }
231        if self.focus_within {
232            v.push("focus_within");
233        }
234        if self.visited {
235            v.push("visited");
236        }
237        if self.backdrop {
238            v.push("backdrop");
239        }
240        if self.dragging {
241            v.push("dragging");
242        }
243        if self.drag_over {
244            v.push("drag_over");
245        }
246        if v.is_empty() {
247            v.push("normal");
248        }
249        write!(f, "{v:?}")
250    }
251}
252
253impl StyledNodeState {
254    /// Creates a new state with all states set to false (normal state).
255    #[must_use] pub const fn new() -> Self {
256        Self {
257            hover: false,
258            active: false,
259            focused: false,
260            disabled: false,
261            checked: false,
262            focus_within: false,
263            visited: false,
264            backdrop: false,
265            dragging: false,
266            drag_over: false,
267        }
268    }
269
270    /// Check if a specific pseudo-state is active
271    #[must_use] pub const fn has_state(&self, state_type: u8) -> bool {
272        match state_type {
273            0 => true, // Normal is always active
274            1 => self.hover,
275            2 => self.active,
276            3 => self.focused,
277            4 => self.disabled,
278            5 => self.checked,
279            6 => self.focus_within,
280            7 => self.visited,
281            8 => self.backdrop,
282            9 => self.dragging,
283            10 => self.drag_over,
284            _ => false,
285        }
286    }
287
288    /// Returns true if no special state is active (just normal)
289    #[must_use] pub const fn is_normal(&self) -> bool {
290        !self.hover
291            && !self.active
292            && !self.focused
293            && !self.disabled
294            && !self.checked
295            && !self.focus_within
296            && !self.visited
297            && !self.backdrop
298            && !self.dragging
299            && !self.drag_over
300    }
301
302    /// Create from `PseudoStateFlags`
303    #[must_use] pub const fn from_pseudo_state_flags(flags: &azul_css::dynamic_selector::PseudoStateFlags) -> Self {
304        Self {
305            hover: flags.hover,
306            active: flags.active,
307            focused: flags.focused,
308            disabled: flags.disabled,
309            checked: flags.checked,
310            focus_within: flags.focus_within,
311            visited: flags.visited,
312            backdrop: flags.backdrop,
313            dragging: flags.dragging,
314            drag_over: flags.drag_over,
315        }
316    }
317}
318
319/// A styled Dom node
320// Per-DOM-node hot type passed by reference throughout the layout/style
321// pipeline; kept non-Copy on purpose so it isn't silently bulk-copied and to
322// avoid trivially_copy_pass_by_ref churn across the many &StyledNode callers.
323#[allow(missing_copy_implementations)]
324#[repr(C)]
325#[derive(Debug, Default, Clone, PartialEq, Eq, PartialOrd)]
326pub struct StyledNode {
327    /// Current state of this styled node (used later for caching the style / layout)
328    pub styled_node_state: StyledNodeState,
329}
330
331impl_option!(
332    StyledNode,
333    OptionStyledNode,
334    copy = false,
335    [Debug, Clone, PartialEq, Eq, PartialOrd]
336);
337
338impl_vec!(StyledNode, StyledNodeVec, StyledNodeVecDestructor, StyledNodeVecDestructorType, StyledNodeVecSlice, OptionStyledNode);
339impl_vec_mut!(StyledNode, StyledNodeVec);
340impl_vec_debug!(StyledNode, StyledNodeVec);
341impl_vec_partialord!(StyledNode, StyledNodeVec);
342impl_vec_clone!(StyledNode, StyledNodeVec, StyledNodeVecDestructor);
343impl_vec_partialeq!(StyledNode, StyledNodeVec);
344
345impl StyledNodeVec {
346    /// Returns an immutable container reference for indexed access.
347    #[must_use] pub fn as_container(&self) -> NodeDataContainerRef<'_, StyledNode> {
348        NodeDataContainerRef {
349            internal: self.as_ref(),
350        }
351    }
352    /// Returns a mutable container reference for indexed access.
353    pub fn as_container_mut(&mut self) -> NodeDataContainerRefMut<'_, StyledNode> {
354        NodeDataContainerRefMut {
355            internal: self.as_mut(),
356        }
357    }
358}
359
360#[test]
361#[allow(clippy::used_underscore_binding)] // intentional `_`-prefix (FFI/api.json pub field, or cfg-gated binding); access is deliberate
362fn test_css_styling_with_nested_divs() {
363    let s = "
364        html, body, p {
365            margin: 0;
366            padding: 0;
367        }
368        #div1 {
369            border: solid black;
370            height: 2in;
371            position: absolute;
372            top: 1in;
373            width: 3in;
374        }
375        div div {
376            background: blue;
377            height: 1in;
378            position: fixed;
379            width: 1in;
380        }
381    ";
382
383    let css = azul_css::parser2::new_from_str(s);
384    let mut _styled_dom = Dom::create_body()
385        .with_children(
386            vec![Dom::create_div()
387                .with_ids_and_classes(
388                    vec![crate::dom::IdOrClass::Id("div1".to_string().into())].into(),
389                )
390                .with_children(vec![Dom::create_div()].into())]
391            .into(),
392        );
393    _styled_dom.add_component_css(css.0);
394}
395
396/// Regression test for the calc.c "frame ≥2 loses all backgrounds" bug:
397/// `recompute_inheritance_and_compact_cache()` must reproduce the
398/// `hot_flags` that `create_from_compact_dom` produced on frame 1. If the
399/// recompute path silently drops to the getters-only `build_compact_cache`
400/// variant, `HOT_FLAG_HAS_BACKGROUND` is never written, the renderer's
401/// `has_any_background()` negative fast-path returns false for every node,
402/// and every painted background vanishes on the next layout pass.
403#[test]
404fn test_recompute_preserves_hot_flag_has_background() {
405    use azul_css::compact_cache::HOT_FLAG_HAS_BACKGROUND;
406
407    let css_str = "
408        body { margin: 0; padding: 0; }
409        .painted { background: red; width: 100px; height: 100px; }
410    ";
411    let css = azul_css::parser2::new_from_str(css_str).0;
412
413    let mut dom = Dom::create_body().with_children(
414        vec![Dom::create_div().with_class("painted".to_string().into())].into(),
415    );
416    let mut styled = StyledDom::create(&mut dom, css);
417
418    // Frame 1: find the painted node by walking its hot_flags.
419    let any_bg_frame1 = {
420        let cache = styled
421            .css_property_cache
422            .ptr
423            .compact_cache
424            .as_ref()
425            .expect("compact_cache populated by create_from_compact_dom");
426        (0..styled.node_hierarchy.as_ref().len())
427            .any(|i| cache.tier2_cold[i].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0)
428    };
429    assert!(
430        any_bg_frame1,
431        "frame 1: expected HOT_FLAG_HAS_BACKGROUND on the .painted node",
432    );
433
434    // Frame 2+: simulate regenerate_layout rebuilding the compact cache.
435    // This is the path the calculator hit on every resize tick, and the
436    // one that had silently regressed to the getter-only builder.
437    styled.recompute_inheritance_and_compact_cache();
438
439    let any_bg_frame2 = {
440        let cache = styled
441            .css_property_cache
442            .ptr
443            .compact_cache
444            .as_ref()
445            .expect("compact_cache rebuilt by recompute_inheritance_and_compact_cache");
446        (0..styled.node_hierarchy.as_ref().len())
447            .any(|i| cache.tier2_cold[i].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0)
448    };
449    assert!(
450        any_bg_frame2,
451        "frame ≥2 after recompute_inheritance_and_compact_cache: \
452         HOT_FLAG_HAS_BACKGROUND disappeared. The recompute path must \
453         use build_compact_cache_with_inheritance (not plain \
454         build_compact_cache) so apply_css_property_to_compact runs and \
455         populates hot_flags for the renderer's negative fast-paths.",
456    );
457}
458
459/// Calculated hash of a font-family
460#[derive(Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
461pub struct StyleFontFamilyHash(pub u64);
462
463impl ::core::fmt::Debug for StyleFontFamilyHash {
464    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465        write!(f, "StyleFontFamilyHash({})", self.0)
466    }
467}
468
469impl StyleFontFamilyHash {
470    /// Computes a 64-bit hash of a font family for cache lookups.
471    #[must_use] pub fn new(family: &StyleFontFamily) -> Self {
472        use core::hash::Hasher;
473        let mut hasher = crate::hash::DefaultHasher::new();
474        family.hash(&mut hasher);
475        Self(hasher.finish())
476    }
477}
478
479/// Calculated hash of a font-family
480#[derive(Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
481pub struct StyleFontFamiliesHash(pub u64);
482
483impl ::core::fmt::Debug for StyleFontFamiliesHash {
484    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
485        write!(f, "StyleFontFamiliesHash({})", self.0)
486    }
487}
488
489impl StyleFontFamiliesHash {
490    /// Computes a 64-bit hash of multiple font families for cache lookups.
491    #[must_use] pub fn new(families: &[StyleFontFamily]) -> Self {
492        use core::hash::Hasher;
493        let mut hasher = crate::hash::DefaultHasher::new();
494        // Prefix with the length so that e.g. `[A, B]` and `[AB]` (or any two
495        // family lists whose concatenated element hashes coincide) cannot
496        // collide into the same cache key.
497        families.len().hash(&mut hasher);
498        for f in families {
499            f.hash(&mut hasher);
500        }
501        Self(hasher.finish())
502    }
503}
504
505/// FFI-safe representation of `Option<NodeId>` as a single `usize`.
506///
507/// # Encoding (1-based)
508///
509/// - `inner = 0` → `None` (no node)
510/// - `inner = n > 0` → `Some(NodeId(n - 1))`
511///
512/// This type exists because C/C++ cannot use Rust's `Option` type.
513/// Use [`NodeHierarchyItemId::into_crate_internal`] to decode and
514/// [`NodeHierarchyItemId::from_crate_internal`] to encode.
515///
516/// # Difference from `NodeId`
517///
518/// - **`NodeId`**: A 0-based array index. `NodeId::new(0)` refers to the first node.
519///   Use directly for array indexing: `nodes[node_id.index()]`.
520///
521/// - **`NodeHierarchyItemId`**: A 1-based encoded `Option<NodeId>`.
522///   `inner = 0` means `None`, `inner = 1` means `Some(NodeId(0))`.
523///   **Never use `inner` as an array index!** Always decode first.
524///
525/// # Warning
526///
527/// The `inner` field uses **1-based encoding**, not a direct index!
528/// Never use `inner` directly as an array index - always decode first.
529///
530/// # Example
531///
532/// ```ignore
533/// // Encoding: Option<NodeId> -> NodeHierarchyItemId
534/// let opt = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(5)));
535/// assert_eq!(opt.into_raw(), 6);  // 5 + 1 = 6
536///
537/// // Decoding: NodeHierarchyItemId -> Option<NodeId>
538/// let decoded = opt.into_crate_internal();
539/// assert_eq!(decoded, Some(NodeId::new(5)));
540///
541/// // None case
542/// let none = NodeHierarchyItemId::NONE;
543/// assert_eq!(none.into_raw(), 0);
544/// assert_eq!(none.into_crate_internal(), None);
545/// ```
546#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
547#[repr(C)]
548pub struct NodeHierarchyItemId {
549    // Uses 1-based encoding: 0 = None, n > 0 = Some(NodeId(n-1))
550    // Do NOT use directly as an array index!
551    inner: usize,
552}
553
554impl fmt::Debug for NodeHierarchyItemId {
555    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
556        match self.into_crate_internal() {
557            Some(n) => write!(f, "Some(NodeId({n}))"),
558            None => write!(f, "None"),
559        }
560    }
561}
562
563impl fmt::Display for NodeHierarchyItemId {
564    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
565        write!(f, "{self:?}")
566    }
567}
568
569impl NodeHierarchyItemId {
570    /// Represents `None` (no node). Encoded as `inner = 0`.
571    pub const NONE: Self = Self { inner: 0 };
572
573    /// Creates an `NodeHierarchyItemId` from a raw 1-based encoded value.
574    ///
575    /// # Warning
576    ///
577    /// The value must use 1-based encoding (0 = None, n = NodeId(n-1)).
578    /// Prefer using [`NodeHierarchyItemId::from_crate_internal`] instead.
579    #[inline]
580    #[must_use] pub const fn from_raw(value: usize) -> Self {
581        Self { inner: value }
582    }
583
584    /// Returns the raw 1-based encoded value.
585    ///
586    /// # Warning
587    ///
588    /// The returned value uses 1-based encoding. Do NOT use as an array index!
589    #[inline]
590    #[must_use] pub const fn into_raw(&self) -> usize {
591        self.inner
592    }
593}
594
595impl_option!(
596    NodeHierarchyItemId,
597    OptionNodeHierarchyItemId,
598    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
599);
600
601impl_vec!(NodeHierarchyItemId, NodeHierarchyItemIdVec, NodeHierarchyItemIdVecDestructor, NodeHierarchyItemIdVecDestructorType, NodeHierarchyItemIdVecSlice, OptionNodeHierarchyItemId);
602impl_vec_mut!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
603impl_vec_debug!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
604impl_vec_ord!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
605impl_vec_eq!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
606impl_vec_hash!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
607impl_vec_partialord!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
608impl_vec_clone!(NodeHierarchyItemId, NodeHierarchyItemIdVec, NodeHierarchyItemIdVecDestructor);
609impl_vec_partialeq!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
610
611impl NodeHierarchyItemId {
612    /// Decodes to `Option<NodeId>` (0 = None, n > 0 = Some(NodeId(n-1))).
613    #[inline]
614    #[must_use] pub const fn into_crate_internal(&self) -> Option<NodeId> {
615        NodeId::from_usize(self.inner)
616    }
617
618    /// Encodes from `Option<NodeId>` (None → 0, Some(NodeId(n)) → n+1).
619    #[inline]
620    #[must_use] pub const fn from_crate_internal(t: Option<NodeId>) -> Self {
621        Self {
622            inner: NodeId::into_raw(&t),
623        }
624    }
625}
626
627impl From<Option<NodeId>> for NodeHierarchyItemId {
628    #[inline]
629    fn from(opt: Option<NodeId>) -> Self {
630        Self::from_crate_internal(opt)
631    }
632}
633
634impl From<NodeHierarchyItemId> for Option<NodeId> {
635    #[inline]
636    fn from(id: NodeHierarchyItemId) -> Self {
637        id.into_crate_internal()
638    }
639}
640
641#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
642#[repr(C)]
643pub struct NodeHierarchyItem {
644    pub parent: usize,
645    pub previous_sibling: usize,
646    pub next_sibling: usize,
647    pub last_child: usize,
648}
649
650impl_option!(
651    NodeHierarchyItem,
652    OptionNodeHierarchyItem,
653    [Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash]
654);
655
656impl NodeHierarchyItem {
657    /// Creates a zeroed hierarchy item (no parent, siblings, or children).
658    #[must_use] pub const fn zeroed() -> Self {
659        Self {
660            parent: 0,
661            previous_sibling: 0,
662            next_sibling: 0,
663            last_child: 0,
664        }
665    }
666}
667
668impl From<Node> for NodeHierarchyItem {
669    fn from(node: Node) -> Self {
670        Self {
671            parent: NodeId::into_raw(&node.parent),
672            previous_sibling: NodeId::into_raw(&node.previous_sibling),
673            next_sibling: NodeId::into_raw(&node.next_sibling),
674            last_child: NodeId::into_raw(&node.last_child),
675        }
676    }
677}
678
679impl NodeHierarchyItem {
680    /// Returns the parent node ID, if any.
681    #[must_use] pub const fn parent_id(&self) -> Option<NodeId> {
682        NodeId::from_usize(self.parent)
683    }
684    /// Returns the previous sibling node ID, if any.
685    #[must_use] pub const fn previous_sibling_id(&self) -> Option<NodeId> {
686        NodeId::from_usize(self.previous_sibling)
687    }
688    /// Returns the next sibling node ID, if any.
689    #[must_use] pub const fn next_sibling_id(&self) -> Option<NodeId> {
690        NodeId::from_usize(self.next_sibling)
691    }
692    /// Returns the first child node ID (`current_node_id` + 1 if has children).
693    #[must_use] pub fn first_child_id(&self, current_node_id: NodeId) -> Option<NodeId> {
694        self.last_child_id().map(|_| current_node_id + 1)
695    }
696    /// Returns the last child node ID, if any.
697    #[must_use] pub const fn last_child_id(&self) -> Option<NodeId> {
698        NodeId::from_usize(self.last_child)
699    }
700}
701
702impl_vec!(NodeHierarchyItem, NodeHierarchyItemVec, NodeHierarchyItemVecDestructor, NodeHierarchyItemVecDestructorType, NodeHierarchyItemVecSlice, OptionNodeHierarchyItem);
703impl_vec_mut!(NodeHierarchyItem, NodeHierarchyItemVec);
704impl_vec_debug!(AzNode, NodeHierarchyItemVec);
705impl_vec_partialord!(AzNode, NodeHierarchyItemVec);
706impl_vec_clone!(
707    NodeHierarchyItem,
708    NodeHierarchyItemVec,
709    NodeHierarchyItemVecDestructor
710);
711impl_vec_partialeq!(AzNode, NodeHierarchyItemVec);
712
713impl NodeHierarchyItemVec {
714    /// Returns an immutable container reference for indexed access.
715    #[must_use] pub fn as_container(&self) -> NodeDataContainerRef<'_, NodeHierarchyItem> {
716        NodeDataContainerRef {
717            internal: self.as_ref(),
718        }
719    }
720    /// Returns a mutable container reference for indexed access.
721    pub fn as_container_mut(&mut self) -> NodeDataContainerRefMut<'_, NodeHierarchyItem> {
722        NodeDataContainerRefMut {
723            internal: self.as_mut(),
724        }
725    }
726}
727
728impl NodeDataContainerRef<'_, NodeHierarchyItem> {
729    /// Returns the number of descendant nodes under the given parent.
730    #[inline]
731    #[must_use] pub fn subtree_len(&self, parent_id: NodeId) -> usize {
732        let self_item_index = parent_id.index();
733        let next_item_index = self[parent_id].next_sibling_id().map_or_else(|| self.len(), |s| s.index());
734        // saturating: a malformed FastDom can leave next_sibling <= parent,
735        // which would underflow-panic the subtraction.
736        next_item_index.saturating_sub(self_item_index).saturating_sub(1)
737    }
738}
739
740#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
741#[repr(C)]
742pub struct ParentWithNodeDepth {
743    pub depth: usize,
744    pub node_id: NodeHierarchyItemId,
745}
746
747impl_option!(
748    ParentWithNodeDepth,
749    OptionParentWithNodeDepth,
750    [Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash]
751);
752
753impl fmt::Debug for ParentWithNodeDepth {
754    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
755        write!(
756            f,
757            "{{ depth: {}, node: {:?} }}",
758            self.depth,
759            self.node_id.into_crate_internal()
760        )
761    }
762}
763
764impl_vec!(ParentWithNodeDepth, ParentWithNodeDepthVec, ParentWithNodeDepthVecDestructor, ParentWithNodeDepthVecDestructorType, ParentWithNodeDepthVecSlice, OptionParentWithNodeDepth);
765impl_vec_mut!(ParentWithNodeDepth, ParentWithNodeDepthVec);
766impl_vec_debug!(ParentWithNodeDepth, ParentWithNodeDepthVec);
767impl_vec_partialord!(ParentWithNodeDepth, ParentWithNodeDepthVec);
768impl_vec_clone!(
769    ParentWithNodeDepth,
770    ParentWithNodeDepthVec,
771    ParentWithNodeDepthVecDestructor
772);
773impl_vec_partialeq!(ParentWithNodeDepth, ParentWithNodeDepthVec);
774
775#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
776#[repr(C)]
777pub struct TagIdToNodeIdMapping {
778    // Hit-testing tag ID (not all nodes have a tag, only nodes that are hit-testable)
779    pub tag_id: TagId,
780    /// Node ID of the node that has a tag
781    pub node_id: NodeHierarchyItemId,
782    /// Whether this node has a tab-index field
783    pub tab_index: OptionTabIndex,
784}
785
786impl_option!(
787    TagIdToNodeIdMapping,
788    OptionTagIdToNodeIdMapping,
789    copy = false,
790    [Debug, Clone, PartialEq, Eq, Ord, PartialOrd]
791);
792
793impl_vec!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec, TagIdToNodeIdMappingVecDestructor, TagIdToNodeIdMappingVecDestructorType, TagIdToNodeIdMappingVecSlice, OptionTagIdToNodeIdMapping);
794impl_vec_mut!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
795impl_vec_debug!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
796impl_vec_partialord!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
797impl_vec_clone!(
798    TagIdToNodeIdMapping,
799    TagIdToNodeIdMappingVec,
800    TagIdToNodeIdMappingVecDestructor
801);
802impl_vec_partialeq!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
803
804#[derive(Debug, Clone, PartialEq, PartialOrd)]
805#[repr(C)]
806pub struct ContentGroup {
807    /// The parent of the current node group, i.e. either the root node (0)
808    /// or the last positioned node ()
809    pub root: NodeHierarchyItemId,
810    /// Node ids in order of drawing
811    pub children: ContentGroupVec,
812}
813
814impl_option!(
815    ContentGroup,
816    OptionContentGroup,
817    copy = false,
818    [Debug, Clone, PartialEq, PartialOrd]
819);
820
821impl_vec!(ContentGroup, ContentGroupVec, ContentGroupVecDestructor, ContentGroupVecDestructorType, ContentGroupVecSlice, OptionContentGroup);
822impl_vec_mut!(ContentGroup, ContentGroupVec);
823impl_vec_debug!(ContentGroup, ContentGroupVec);
824impl_vec_partialord!(ContentGroup, ContentGroupVec);
825impl_vec_clone!(ContentGroup, ContentGroupVec, ContentGroupVecDestructor);
826impl_vec_partialeq!(ContentGroup, ContentGroupVec);
827
828#[derive(Debug, PartialEq, Clone)]
829#[repr(C)]
830pub struct StyledDom {
831    pub root: NodeHierarchyItemId,
832    pub node_hierarchy: NodeHierarchyItemVec,
833    pub node_data: NodeDataVec,
834    pub styled_nodes: StyledNodeVec,
835    pub cascade_info: CascadeInfoVec,
836    pub nodes_with_window_callbacks: NodeHierarchyItemIdVec,
837    pub nodes_with_datasets: NodeHierarchyItemIdVec,
838    pub tag_ids_to_node_ids: TagIdToNodeIdMappingVec,
839    pub non_leaf_nodes: ParentWithNodeDepthVec,
840    pub css_property_cache: CssPropertyCachePtr,
841    /// The ID of this DOM in the layout tree (for multi-DOM support with `VirtualViews`)
842    pub dom_id: DomId,
843}
844impl_option!(
845    StyledDom,
846    OptionStyledDom,
847    copy = false,
848    [Debug, Clone, PartialEq]
849);
850
851impl Default for StyledDom {
852    fn default() -> Self {
853        let root_node: NodeHierarchyItem = Node::ROOT.into();
854        let root_node_id: NodeHierarchyItemId =
855            NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO));
856        Self {
857            root: root_node_id,
858            node_hierarchy: vec![root_node].into(),
859            node_data: vec![NodeData::create_body()].into(),
860            styled_nodes: vec![StyledNode::default()].into(),
861            cascade_info: vec![CascadeInfo {
862                index_in_parent: 0,
863                is_last_child: true,
864            }]
865            .into(),
866            tag_ids_to_node_ids: Vec::new().into(),
867            non_leaf_nodes: vec![ParentWithNodeDepth {
868                depth: 0,
869                node_id: root_node_id,
870            }]
871            .into(),
872            nodes_with_window_callbacks: Vec::new().into(),
873            nodes_with_datasets: Vec::new().into(),
874            css_property_cache: CssPropertyCachePtr::new(CssPropertyCache::empty(1)),
875            dom_id: DomId::ROOT_ID,
876        }
877    }
878}
879
880/// Per-field heap-byte breakdown of a `StyledDom`.
881#[derive(Debug, Clone, Copy, Default)]
882pub struct StyledDomMemoryReport {
883    pub node_count: usize,
884    pub node_hierarchy_bytes: usize,
885    pub node_data_bytes: usize,
886    pub styled_nodes_bytes: usize,
887    pub cascade_info_bytes: usize,
888    pub tag_ids_bytes: usize,
889    pub non_leaf_nodes_bytes: usize,
890    pub callback_vecs_bytes: usize,
891    pub css_property_cache: crate::prop_cache::CssPropertyCacheBreakdown,
892}
893
894impl StyledDomMemoryReport {
895    #[must_use] pub const fn total_bytes(&self) -> usize {
896        self.node_hierarchy_bytes
897            + self.node_data_bytes
898            + self.styled_nodes_bytes
899            + self.cascade_info_bytes
900            + self.tag_ids_bytes
901            + self.non_leaf_nodes_bytes
902            + self.callback_vecs_bytes
903            + self.css_property_cache.total_bytes()
904    }
905}
906
907impl StyledDom {
908    /// Approximate heap bytes retained by this `StyledDom`, broken out by field.
909    #[must_use] pub fn memory_report(&self) -> StyledDomMemoryReport {
910        let n = self.node_data.len();
911        StyledDomMemoryReport {
912            node_count: n,
913            node_hierarchy_bytes: size_of_val(self.node_hierarchy.as_ref()),
914            node_data_bytes: {
915                let base = n * size_of::<NodeData>();
916                // NodeData contains inline Vecs (callbacks, css_props, datasets)
917                // that have their own heap allocations. Approximate:
918                let mut inner = 0usize;
919                for nd in self.node_data.as_ref() {
920                    inner += nd.get_callbacks().len() * 64; // rough per-callback
921                    // Each rule = path + decls Vec + conditions Vec + priority byte.
922                    // Approximate at 64 bytes per rule + the heap for declarations.
923                    inner += nd.style.rules.as_ref().len() * 64;
924                }
925                base + inner
926            },
927            styled_nodes_bytes: n * size_of::<StyledNode>(),
928            cascade_info_bytes: n * size_of::<CascadeInfo>(),
929            tag_ids_bytes: size_of_val(self.tag_ids_to_node_ids.as_ref()),
930            non_leaf_nodes_bytes: size_of_val(self.non_leaf_nodes.as_ref()),
931            callback_vecs_bytes:
932                self.nodes_with_window_callbacks.as_ref().len() * 8
933                + self.nodes_with_datasets.as_ref().len() * 8,
934            css_property_cache: self.css_property_cache.ptr.memory_breakdown(),
935        }
936    }
937
938    /// Creates a new `StyledDom` by applying CSS styles to a DOM tree.
939    ///
940    /// NOTE: After calling this function, the DOM will be reset to an empty DOM.
941    // This is for memory optimization, so that the DOM does not need to be cloned.
942    //
943    // The CSS will be left in-place, but will be re-ordered
944    pub fn create(dom: &mut Dom, css: Css) -> Self {
945        use core::mem;
946
947        let mut swap_dom = Dom::create_body();
948        mem::swap(dom, &mut swap_dom);
949
950        let compact_dom: CompactDom = swap_dom.into();
951        let node_hierarchy: NodeHierarchyItemVec = compact_dom
952            .node_hierarchy
953            .as_ref()
954            .internal
955            .iter()
956            .map(|i| (*i).into())
957            .collect::<Vec<NodeHierarchyItem>>()
958            .into();
959
960        Self::create_from_compact_dom(compact_dom, css, node_hierarchy)
961    }
962
963    /// Creates a `StyledDom` from a `FastDom` (arena-based DOM).
964    ///
965    /// This skips the `convert_dom_into_compact_dom` tree→arena conversion
966    /// entirely since `FastDom` already has flat `NodeHierarchyItemVec` and
967    /// `NodeDataVec`. CSS is collected from `CssWithNodeIdVec`.
968    #[must_use] pub fn create_from_fast_dom(fast_dom: crate::dom::FastDom) -> Self {
969        use azul_css::css::Css;
970
971        // 1. Merge CSS from CssWithNodeIdVec into a single Css, scoping each
972        //    node-attached stylesheet to its owner's subtree (#47): push_front a
973        //    Root([owner, owner+subtree_len]) selector so inline/XML css can't leak
974        //    globally — the same scoping the recursive create_from_dom path applies
975        //    via scope_inline_css. `node_id` is the owner's flat id (0 = root).
976        let mut combined_rules: Vec<azul_css::css::CssRuleBlock> = Vec::new();
977        let css_entries = fast_dom.css.into_library_owned_vec();
978        {
979            let hierarchy = fast_dom.node_hierarchy.as_container();
980            for css_with_id in css_entries {
981                let owner = css_with_id.node_id;
982                let end = if owner < hierarchy.len() {
983                    owner + hierarchy.subtree_len(NodeId::new(owner))
984                } else {
985                    owner
986                };
987                for mut rule in css_with_id.css.rules.into_library_owned_vec() {
988                    rule.path.push_front_scope(owner, end);
989                    combined_rules.push(rule);
990                }
991            }
992        }
993        let combined_css = if combined_rules.is_empty() {
994            Css::empty()
995        } else {
996            Css::new(combined_rules)
997        };
998
999        // 2. Convert NodeHierarchyItemVec → NodeHierarchy (Vec<Node>)
1000        //    for cascade tree computation
1001        let node_hierarchy_items = fast_dom.node_hierarchy;
1002        let nodes: Vec<Node> = node_hierarchy_items.as_ref()
1003            .iter()
1004            .map(|item| Node {
1005                parent: NodeId::from_usize(item.parent),
1006                previous_sibling: NodeId::from_usize(item.previous_sibling),
1007                next_sibling: NodeId::from_usize(item.next_sibling),
1008                last_child: NodeId::from_usize(item.last_child),
1009            })
1010            .collect();
1011        let node_hierarchy_internal = NodeHierarchy { internal: nodes };
1012
1013        // 3. Build CompactDom from the flat arenas (no conversion needed)
1014        let node_data_vec = fast_dom.node_data.into_library_owned_vec();
1015        let compact_dom = CompactDom {
1016            node_hierarchy: node_hierarchy_internal,
1017            node_data: NodeDataContainer { internal: node_data_vec },
1018            root: NodeId::ZERO,
1019        };
1020
1021        // 4. Delegate to create() which handles cascade, UA CSS, etc.
1022        //    We need a mutable Dom to pass to create(), but we already have CompactDom.
1023        //    Instead, inline the cascade logic from create() with our CompactDom.
1024        Self::create_from_compact_dom(compact_dom, combined_css, node_hierarchy_items)
1025    }
1026
1027    /// Internal: creates `StyledDom` from a `CompactDom` + CSS + pre-built hierarchy items.
1028    /// Shared by both the Slow path (create → `convert_dom_into_compact_dom` → this)
1029    /// and the Fast path (`create_from_fast_dom` → this).
1030    #[allow(clippy::similar_names)] // domain-standard coordinate/control-point names
1031    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
1032    fn create_from_compact_dom(
1033        compact_dom: CompactDom,
1034        mut css: Css,
1035        node_hierarchy: NodeHierarchyItemVec,
1036    ) -> Self {
1037        use crate::dom::EventFilter;
1038
1039        static CASCADE_BREAKDOWN: crate::sync::OnceLock<bool> = crate::sync::OnceLock::new();
1040        let cascade_dbg = *CASCADE_BREAKDOWN.get_or_init(crate::profile::memory_enabled);
1041
1042        let node_count = compact_dom.len();
1043
1044        let non_leaf_nodes = compact_dom
1045            .node_hierarchy
1046            .as_ref()
1047            .get_parents_sorted_by_depth();
1048
1049        let mut styled_nodes = vec![
1050            StyledNode {
1051                styled_node_state: StyledNodeState::new()
1052            };
1053            node_count
1054        ];
1055
1056        let mut css_property_cache = CssPropertyCache::empty(compact_dom.node_data.len());
1057
1058        let html_tree = construct_html_cascade_tree(
1059            &compact_dom.node_hierarchy.as_ref(),
1060            &non_leaf_nodes[..],
1061            &compact_dom.node_data.as_ref(),
1062        );
1063
1064        let non_leaf_nodes = non_leaf_nodes
1065            .iter()
1066            .map(|(depth, node_id)| ParentWithNodeDepth {
1067                depth: *depth,
1068                node_id: NodeHierarchyItemId::from_crate_internal(Some(*node_id)),
1069            })
1070            .collect::<Vec<_>>();
1071
1072        let non_leaf_nodes: ParentWithNodeDepthVec = non_leaf_nodes.into();
1073
1074        let _restyle_tag_ids = css_property_cache.restyle(
1075            &mut css,
1076            &compact_dom.node_data.as_ref(),
1077            &node_hierarchy,
1078            &non_leaf_nodes,
1079            &html_tree.as_ref(),
1080        );
1081
1082        // Drop the CSS object now — selectors/declarations are no longer needed
1083        // after restyle has populated css_props. This frees ~500 KiB of stylesheet
1084        // data structures (CssRuleBlock, CssPathSelector, CssDeclaration).
1085        drop(css);
1086
1087        // Apply UA defaults + compute inherited values so consumers that
1088        // read `css_property_cache.computed_values` (the web/HTML
1089        // renderer in `dll/src/web/html_render.rs`) see resolved
1090        // properties. The compact cache below stores the same info in
1091        // a different layout for the desktop renderer; computed_values
1092        // is the "tall" form that the web renderer's CSS emitter
1093        // (`emit_css_from_cache`) walks per node.
1094        css_property_cache.apply_ua_css(compact_dom.node_data.as_ref().internal);
1095        css_property_cache.compute_inherited_values(
1096            node_hierarchy.as_container().internal,
1097            compact_dom.node_data.as_ref().internal,
1098        );
1099
1100        let prev_font_hashes: Vec<u64> = css_property_cache.compact_cache
1101            .as_ref()
1102            .map(|c| c.prev_font_hashes.clone())
1103            .unwrap_or_default();
1104        let compact = css_property_cache.build_compact_cache_with_inheritance(
1105            compact_dom.node_data.as_ref().internal,
1106            node_hierarchy.as_container().internal,
1107            &prev_font_hashes,
1108        );
1109        css_property_cache.compact_cache = Some(compact);
1110        let pre_prune = if cascade_dbg {
1111            Some(css_property_cache.memory_breakdown())
1112        } else { None };
1113        css_property_cache.prune_compact_normal_props();
1114        if let Some(pre) = pre_prune {
1115            let post = css_property_cache.memory_breakdown();
1116            #[cfg(feature = "std")]
1117            eprintln!("[PRUNE] css_props {} → {} KiB  cascaded {} → {} KiB  (saved {} KiB)",
1118                pre.css_props_bytes / 1024, post.css_props_bytes / 1024,
1119                pre.cascaded_props_bytes / 1024, post.cascaded_props_bytes / 1024,
1120                (pre.total_bytes().saturating_sub(post.total_bytes())) / 1024);
1121            #[cfg(not(feature = "std"))]
1122            let _ = post;
1123        }
1124
1125        let tag_ids = css_property_cache.generate_tag_ids(
1126            &compact_dom.node_data.as_ref(),
1127            &node_hierarchy,
1128        );
1129
1130        if cascade_dbg {
1131            let bd = css_property_cache.memory_breakdown();
1132            #[cfg(feature = "std")]
1133            eprintln!("[CASCADE] {} nodes  cascaded_props={} KiB  css_props={} KiB  compact={} KiB  computed={} KiB  total={} KiB",
1134                node_count,
1135                bd.cascaded_props_bytes / 1024, bd.css_props_bytes / 1024,
1136                bd.compact_cache_bytes / 1024, bd.computed_values_bytes / 1024,
1137                bd.total_bytes() / 1024);
1138            #[cfg(not(feature = "std"))]
1139            let _ = bd;
1140        }
1141
1142        // Collect callback/dataset nodes in a single pass (avoids 3 separate 50K scans).
1143        // For XHTML-parsed DOMs with no callbacks, this early-exits immediately.
1144        let has_any_callbacks = compact_dom.node_data.as_ref().internal.iter()
1145            .any(|c| !c.get_callbacks().is_empty() || c.get_dataset().is_some());
1146
1147        let (nodes_with_window_callbacks, nodes_with_datasets) = if has_any_callbacks {
1148            let mut win_cbs = Vec::new();
1149            let mut datasets = Vec::new();
1150            for (node_id, c) in compact_dom.node_data.as_ref().internal.iter().enumerate() {
1151                let cbs = c.get_callbacks();
1152                let has_dataset = c.get_dataset().is_some();
1153                if !cbs.is_empty() || has_dataset {
1154                    datasets.push(NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_id))));
1155                }
1156                for cb in cbs {
1157                    if let EventFilter::Window(_) = cb.event {
1158                        win_cbs.push(NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_id))));
1159                        break;
1160                    }
1161                }
1162            }
1163            (win_cbs, datasets)
1164        } else {
1165            (Vec::new(), Vec::new())
1166        };
1167        let mut styled_dom = Self {
1168            root: NodeHierarchyItemId::from_crate_internal(Some(compact_dom.root)),
1169            node_hierarchy,
1170            node_data: compact_dom.node_data.internal.into(),
1171            cascade_info: html_tree.internal.into(),
1172            styled_nodes: styled_nodes.into(),
1173            tag_ids_to_node_ids: tag_ids.into(),
1174            nodes_with_window_callbacks: nodes_with_window_callbacks.into(),
1175            nodes_with_datasets: nodes_with_datasets.into(),
1176            non_leaf_nodes,
1177            css_property_cache: CssPropertyCachePtr::new(css_property_cache),
1178            dom_id: DomId::ROOT_ID,
1179        };
1180        #[cfg(feature = "table_layout")]
1181        if let Err(_e) = crate::dom_table::generate_anonymous_table_elements(&mut styled_dom) {
1182        }
1183
1184        styled_dom
1185    }
1186
1187    /// Creates a `StyledDom` from a recursive Dom tree with deferred CSS.
1188    ///
1189    /// This is the Phase 7.2 entry point: the layout callback returns a recursive
1190    /// `Dom` with `css: Vec<Css>` on each node. This function:
1191    ///
1192    /// 1. Collects all CSS objects from the recursive tree
1193    /// 2. Flattens the Dom into contiguous arrays (`CompactDom`)
1194    /// 3. Merges all CSS objects and runs a single cascade pass
1195    /// 4. Runs `apply_ua_css` → `compute_inherited_values` → `build_compact_cache`
1196    /// 5. Generates anonymous table elements
1197    #[must_use] pub fn create_from_dom(mut dom: Dom) -> Self {
1198        use azul_css::css::Css;
1199
1200        // #47: scope each node's inline css to its subtree BEFORE collecting, so a
1201        // non-root node's with_css cannot leak to the whole tree. Uses the same
1202        // pre-order ids the flatten (convert_dom_into_compact_dom) will assign;
1203        // needs estimated_total_children populated first.
1204        dom.fixup_children_estimated();
1205        let mut next_scope_id = 0usize;
1206        scope_inline_css(&mut dom, &mut next_scope_id);
1207
1208        // 1. Collect all CSS objects from the recursive Dom tree (now scoped)
1209        let mut all_css = Vec::new();
1210        collect_css_from_dom(&dom, &mut all_css);
1211
1212        // 2. Merge all CSS objects into one combined Css
1213        let mut combined_css = if all_css.is_empty() {
1214            Css::empty()
1215        } else {
1216            let mut combined_rules: Vec<azul_css::css::CssRuleBlock> = Vec::new();
1217            for css in all_css {
1218                combined_rules.extend(css.rules.into_library_owned_vec());
1219            }
1220            Css::new(combined_rules)
1221        };
1222
1223        // 3. Strip CSS from all Dom nodes before flattening
1224        //    (CSS is already collected, don't need it in the flat tree)
1225        strip_css_from_dom(&mut dom);
1226
1227        // 4. Use existing StyledDom::create to flatten + cascade
1228        Self::create(&mut dom, combined_css)
1229    }
1230
1231    /// Appends another `StyledDom` as a child to the `self.root`
1232    /// without re-styling the DOM itself
1233    pub fn append_child(&mut self, other: Self) {
1234        let self_root_id = self.root.into_crate_internal().unwrap_or(NodeId::ZERO);
1235        let current_root_children_count = self_root_id
1236            .az_children(&self.node_hierarchy.as_container())
1237            .count();
1238        self.append_child_with_index(other, current_root_children_count);
1239        self.finalize_non_leaf_nodes();
1240    }
1241
1242    /// Optimized version of `append_child` that takes the child index directly
1243    /// instead of counting existing children (O(1) instead of O(n))
1244    pub fn append_child_with_index(&mut self, mut other: Self, child_index: usize) {
1245        // shift all the node ids in other by self.len()
1246        let self_len = self.node_hierarchy.as_ref().len();
1247        let other_len = other.node_hierarchy.as_ref().len();
1248        let self_root_id = self.root.into_crate_internal().unwrap_or(NodeId::ZERO);
1249        let other_root_id = other.root.into_crate_internal().unwrap_or(NodeId::ZERO);
1250
1251        // Use provided index instead of counting children
1252        other.cascade_info.as_mut()[other_root_id.index()].index_in_parent =
1253            u32::try_from(child_index).unwrap_or(u32::MAX);
1254        other.cascade_info.as_mut()[other_root_id.index()].is_last_child = true;
1255
1256        self.cascade_info.append(&mut other.cascade_info);
1257
1258        // adjust node hierarchy
1259        for other in other.node_hierarchy.as_mut().iter_mut() {
1260            if other.parent != 0 {
1261                other.parent += self_len;
1262            }
1263            if other.previous_sibling != 0 {
1264                other.previous_sibling += self_len;
1265            }
1266            if other.next_sibling != 0 {
1267                other.next_sibling += self_len;
1268            }
1269            if other.last_child != 0 {
1270                other.last_child += self_len;
1271            }
1272        }
1273
1274        other.node_hierarchy.as_container_mut()[other_root_id].parent =
1275            NodeId::into_raw(&Some(self_root_id));
1276        let current_last_child = self.node_hierarchy.as_container()[self_root_id].last_child_id();
1277        other.node_hierarchy.as_container_mut()[other_root_id].previous_sibling =
1278            NodeId::into_raw(&current_last_child);
1279        if let Some(current_last) = current_last_child {
1280            if self.node_hierarchy.as_container_mut()[current_last]
1281                .next_sibling_id()
1282                .is_some()
1283            {
1284                self.node_hierarchy.as_container_mut()[current_last].next_sibling +=
1285                    other_root_id.index() + other_len;
1286            } else {
1287                self.node_hierarchy.as_container_mut()[current_last].next_sibling =
1288                    NodeId::into_raw(&Some(NodeId::new(self_len + other_root_id.index())));
1289            }
1290        }
1291        self.node_hierarchy.as_container_mut()[self_root_id].last_child =
1292            NodeId::into_raw(&Some(NodeId::new(self_len + other_root_id.index())));
1293
1294        self.node_hierarchy.append(&mut other.node_hierarchy);
1295        self.node_data.append(&mut other.node_data);
1296        self.styled_nodes.append(&mut other.styled_nodes);
1297        self.get_css_property_cache_mut()
1298            .append(other.get_css_property_cache_mut());
1299
1300        // Tag IDs are globally unique (AtomicUsize counter) and never collide,
1301        // so we only shift node_id (which changes when DOMs are merged).
1302        for tag_id_node_id in &mut other.tag_ids_to_node_ids {
1303            tag_id_node_id.node_id.inner += self_len;
1304        }
1305
1306        self.tag_ids_to_node_ids
1307            .append(&mut other.tag_ids_to_node_ids);
1308
1309        for nid in &mut other.nodes_with_window_callbacks {
1310            nid.inner += self_len;
1311        }
1312        self.nodes_with_window_callbacks
1313            .append(&mut other.nodes_with_window_callbacks);
1314
1315        for nid in &mut other.nodes_with_datasets {
1316            nid.inner += self_len;
1317        }
1318        self.nodes_with_datasets
1319            .append(&mut other.nodes_with_datasets);
1320
1321        // edge case: if the other StyledDom consists of only one node
1322        // then it is not a parent itself
1323        if other_len != 1 {
1324            for other_non_leaf_node in &mut other.non_leaf_nodes {
1325                other_non_leaf_node.node_id.inner += self_len;
1326                other_non_leaf_node.depth += 1;
1327            }
1328            self.non_leaf_nodes.append(&mut other.non_leaf_nodes);
1329            // NOTE: Sorting deferred - call finalize_non_leaf_nodes() after all appends
1330        }
1331    }
1332
1333    /// Call this after all `append_child_with_index` operations are complete
1334    /// to sort `non_leaf_nodes` by depth (required for correct rendering)
1335    pub fn finalize_non_leaf_nodes(&mut self) {
1336        self.non_leaf_nodes.sort_by(|a, b| a.depth.cmp(&b.depth));
1337    }
1338
1339    /// Same as `append_child()`, but as a builder method
1340    #[must_use] pub fn with_child(mut self, other: Self) -> Self {
1341        self.append_child(other);
1342        self
1343    }
1344
1345    /// Sets the context menu for the root node
1346    pub fn set_context_menu(&mut self, context_menu: Menu) {
1347        if let Some(root_id) = self.root.into_crate_internal() {
1348            self.node_data.as_container_mut()[root_id].set_context_menu(context_menu);
1349        }
1350    }
1351
1352    /// Builder method for setting the context menu
1353    #[must_use] pub fn with_context_menu(mut self, context_menu: Menu) -> Self {
1354        self.set_context_menu(context_menu);
1355        self
1356    }
1357
1358    /// Sets the menu bar for the root node
1359    pub fn set_menu_bar(&mut self, menu_bar: Menu) {
1360        if let Some(root_id) = self.root.into_crate_internal() {
1361            self.node_data.as_container_mut()[root_id].set_menu_bar(menu_bar);
1362        }
1363    }
1364
1365    /// Builder method for setting the menu bar
1366    #[must_use] pub fn with_menu_bar(mut self, menu_bar: Menu) -> Self {
1367        self.set_menu_bar(menu_bar);
1368        self
1369    }
1370
1371    /// Re-compute inherited CSS values and rebuild the compact layout cache.
1372    ///
1373    /// This MUST be called after `append_child()` merges multiple `StyledDom`s.
1374    /// `append_child()` concatenates the CSS property caches but does NOT
1375    /// re-run inheritance or rebuild the compact cache. This means:
1376    ///
1377    /// 1. **Broken inheritance**: Inherited properties (`color`, `font-size`,
1378    ///    `direction`) from the parent DOM do not flow into appended subtrees.
1379    /// 2. **Stale compact cache**: The child's tier 1/2/2b entries still reflect
1380    ///    the child's isolated cascade, not the composed tree.
1381    ///
1382    /// Calling this method after all `append_child()` calls fixes both issues
1383    /// by re-running a full depth-first inheritance pass and rebuilding the
1384    /// compact cache from scratch on the composed tree.
1385    pub fn recompute_inheritance_and_compact_cache(&mut self) {
1386        // Use the _with_inheritance variant: it does inheritance inline (via
1387        // parent-compact-field copy) AND populates hot_flags via
1388        // apply_css_property_to_compact.  The plain build_compact_cache would
1389        // leave HOT_FLAG_HAS_BACKGROUND / HAS_CLIP_PATH / extra_flags at 0,
1390        // causing renderer negative fast-paths to skip paint (regression
1391        // introduced by ff059052b).  No SIGABRT risk — _with_inheritance
1392        // never pushes to the flat cascaded_props storage.
1393        let prev_font_hashes: Vec<u64> = self.css_property_cache
1394            .downcast_mut()
1395            .compact_cache
1396            .as_ref()
1397            .map(|c| c.prev_font_hashes.clone())
1398            .unwrap_or_default();
1399        let compact = self.css_property_cache
1400            .downcast_mut()
1401            .build_compact_cache_with_inheritance(
1402                self.node_data.as_container().internal,
1403                self.node_hierarchy.as_container().internal,
1404                &prev_font_hashes,
1405            );
1406        self.css_property_cache.downcast_mut().compact_cache = Some(compact);
1407    }
1408
1409    /// Re-applies CSS styles to the existing DOM structure.
1410    pub fn restyle(&mut self, mut css: Css) {
1411        // NOTE: the tag_ids returned by `cache.restyle` here are generated from
1412        // the STALE `compact_cache` (display/overflow reads) and are intentionally
1413        // discarded — we regenerate them below AFTER the compact cache and
1414        // inheritance have been recomputed (audit styled_dom.rs:1404/1426).
1415        let _stale_tag_ids = self.css_property_cache.downcast_mut().restyle(
1416            &mut css,
1417            &self.node_data.as_container(),
1418            &self.node_hierarchy,
1419            &self.non_leaf_nodes,
1420            &self.cascade_info.as_container(),
1421        );
1422
1423        // Apply UA CSS properties before computing inheritance
1424        self.css_property_cache
1425            .downcast_mut()
1426            .apply_ua_css(self.node_data.as_container().internal);
1427
1428        // Compute inherited values after restyle and apply_ua_css (resolves em, %, etc.)
1429        self.css_property_cache
1430            .downcast_mut()
1431            .compute_inherited_values(
1432                self.node_hierarchy.as_container().internal,
1433                self.node_data.as_container().internal,
1434            );
1435
1436        // The old compact_cache was built from the pre-restyle CSS. If we do not
1437        // rebuild it, layout-hot properties (display/overflow/background/clip,
1438        // resolved font sizes) keep their stale values and the restyle silently
1439        // no-ops for them. Drop it, rebuild via the _with_inheritance path (which
1440        // repopulates hot_flags), and invalidate the cached resolved font sizes.
1441        let prev_font_hashes: Vec<u64> = self
1442            .css_property_cache
1443            .downcast_mut()
1444            .compact_cache
1445            .as_ref()
1446            .map(|c| c.prev_font_hashes.clone())
1447            .unwrap_or_default();
1448        self.css_property_cache.downcast_mut().compact_cache = None;
1449        let compact = self
1450            .css_property_cache
1451            .downcast_mut()
1452            .build_compact_cache_with_inheritance(
1453                self.node_data.as_container().internal,
1454                self.node_hierarchy.as_container().internal,
1455                &prev_font_hashes,
1456            );
1457        self.css_property_cache.downcast_mut().compact_cache = Some(compact);
1458        self.css_property_cache
1459            .downcast_mut()
1460            .invalidate_resolved_font_sizes();
1461
1462        // Regenerate tag_ids from the freshly rebuilt compact cache so the
1463        // hit-test map reflects the post-restyle display/overflow values.
1464        let new_tag_ids = self.css_property_cache.downcast_mut().generate_tag_ids(
1465            &self.node_data.as_container(),
1466            &self.node_hierarchy,
1467        );
1468        self.tag_ids_to_node_ids = new_tag_ids.into();
1469    }
1470
1471    /// Returns the total number of nodes in this `StyledDom`.
1472    #[inline]
1473    #[must_use] pub const fn node_count(&self) -> usize {
1474        self.node_data.len()
1475    }
1476
1477    /// Returns an immutable reference to the CSS property cache.
1478    #[inline]
1479    #[must_use] pub fn get_css_property_cache(&self) -> &CssPropertyCache {
1480        &self.css_property_cache.ptr
1481    }
1482
1483    /// Returns a mutable reference to the CSS property cache.
1484    #[inline]
1485    pub fn get_css_property_cache_mut(&mut self) -> &mut CssPropertyCache {
1486        &mut self.css_property_cache.ptr
1487    }
1488
1489    /// Returns the current state (hover, active, focus) of a styled node.
1490    #[inline]
1491    #[must_use] pub fn get_styled_node_state(&self, node_id: &NodeId) -> StyledNodeState {
1492        self.styled_nodes.as_container()[*node_id]
1493            .styled_node_state
1494    }
1495
1496    /// Updates hover state for nodes and returns changed CSS properties.
1497    #[must_use]
1498    pub fn restyle_nodes_hover(
1499        &mut self,
1500        nodes: &[NodeId],
1501        new_hover_state: bool,
1502    ) -> RestyleNodes {
1503        self.restyle_nodes_state(
1504            nodes,
1505            new_hover_state,
1506            |state, val| state.hover = val,
1507            azul_css::dynamic_selector::PseudoStateType::Hover,
1508        )
1509    }
1510
1511    /// Updates active state for nodes and returns changed CSS properties.
1512    #[must_use]
1513    pub fn restyle_nodes_active(
1514        &mut self,
1515        nodes: &[NodeId],
1516        new_active_state: bool,
1517    ) -> RestyleNodes {
1518        self.restyle_nodes_state(
1519            nodes,
1520            new_active_state,
1521            |state, val| state.active = val,
1522            azul_css::dynamic_selector::PseudoStateType::Active,
1523        )
1524    }
1525
1526    /// Updates focus state for nodes and returns changed CSS properties.
1527    #[must_use]
1528    pub fn restyle_nodes_focus(
1529        &mut self,
1530        nodes: &[NodeId],
1531        new_focus_state: bool,
1532    ) -> RestyleNodes {
1533        self.restyle_nodes_state(
1534            nodes,
1535            new_focus_state,
1536            |state, val| state.focused = val,
1537            azul_css::dynamic_selector::PseudoStateType::Focus,
1538        )
1539    }
1540
1541    /// Generic restyle method parameterized by the state field and pseudo-state type.
1542    fn restyle_nodes_state(
1543        &mut self,
1544        nodes: &[NodeId],
1545        new_state_value: bool,
1546        set_state: impl Fn(&mut StyledNodeState, bool),
1547        pseudo_state_type: azul_css::dynamic_selector::PseudoStateType,
1548    ) -> RestyleNodes {
1549        // Drop any stale NodeIds that no longer index into this DOM (e.g. left
1550        // over from a previous, larger tree). Indexing styled_nodes / node_data
1551        // with an out-of-range id would panic. Filtering here keeps the
1552        // downstream zip with `old_node_states` aligned.
1553        let node_count = self.node_count();
1554        let nodes: Vec<NodeId> = nodes
1555            .iter()
1556            .copied()
1557            .filter(|nid| nid.index() < node_count)
1558            .collect();
1559
1560        // save the old node state
1561        let old_node_states = nodes
1562            .iter()
1563            .map(|nid| {
1564                self.styled_nodes.as_container()[*nid]
1565                    .styled_node_state
1566            })
1567            .collect::<Vec<_>>();
1568
1569        for nid in &nodes {
1570            set_state(
1571                &mut self.styled_nodes.as_container_mut()[*nid].styled_node_state,
1572                new_state_value,
1573            );
1574        }
1575
1576        let css_property_cache = self.get_css_property_cache();
1577        let styled_nodes = self.styled_nodes.as_container();
1578        let node_data = self.node_data.as_container();
1579
1580        // scan all properties that could have changed because of addition / removal
1581        let v = nodes
1582            .iter()
1583            .zip(old_node_states.iter())
1584            .filter_map(|(node_id, old_node_state)| {
1585                let mut keys_normal: Vec<_> = CssPropertyCache::prop_types_for_state(
1586                    css_property_cache.css_props.get_slice(node_id.index()),
1587                    pseudo_state_type,
1588                ).collect();
1589                let mut keys_inherited: Vec<_> = CssPropertyCache::prop_types_for_state(
1590                    css_property_cache.cascaded_props.get_slice(node_id.index()),
1591                    pseudo_state_type,
1592                ).collect();
1593                let keys_inline: Vec<CssPropertyType> = {
1594                    use azul_css::dynamic_selector::DynamicSelector;
1595                    node_data[*node_id]
1596                        .style
1597                        .iter_inline_properties()
1598                        .filter_map(|(prop, conds)| {
1599                            let matches = conds.as_slice().iter().any(|c| {
1600                                matches!(c, DynamicSelector::PseudoState(pst) if *pst == pseudo_state_type)
1601                            });
1602                            if matches {
1603                                Some(prop.get_type())
1604                            } else {
1605                                None
1606                            }
1607                        })
1608                        .collect()
1609                };
1610                let mut keys_inline_ref: Vec<_> = keys_inline.iter().collect();
1611
1612                keys_normal.append(&mut keys_inherited);
1613                keys_normal.append(&mut keys_inline_ref);
1614
1615                let node_properties_that_could_have_changed = keys_normal;
1616
1617                if node_properties_that_could_have_changed.is_empty() {
1618                    return None;
1619                }
1620
1621                let new_node_state = &styled_nodes[*node_id].styled_node_state;
1622                let node_data = &node_data[*node_id];
1623
1624                let changes = node_properties_that_could_have_changed
1625                    .into_iter()
1626                    .filter_map(|prop| {
1627                        // calculate both the old and the new state
1628                        let old = css_property_cache.get_property_slow(
1629                            node_data,
1630                            node_id,
1631                            old_node_state,
1632                            prop,
1633                        );
1634                        let new = css_property_cache.get_property_slow(
1635                            node_data,
1636                            node_id,
1637                            new_node_state,
1638                            prop,
1639                        );
1640                        if old == new {
1641                            None
1642                        } else {
1643                            Some(ChangedCssProperty {
1644                                previous_state: *old_node_state,
1645                                previous_prop: old.map_or_else(|| CssProperty::auto(*prop), Clone::clone),
1646                                current_state: *new_node_state,
1647                                current_prop: new.map_or_else(|| CssProperty::auto(*prop), Clone::clone),
1648                            })
1649                        }
1650                    })
1651                    .collect::<Vec<_>>();
1652
1653                if changes.is_empty() {
1654                    None
1655                } else {
1656                    Some((*node_id, changes))
1657                }
1658            })
1659            .collect::<Vec<_>>();
1660
1661        v.into_iter().collect()
1662    }
1663
1664    /// Unified entry point for all CSS restyle operations.
1665    ///
1666    /// This function synchronizes the `StyledNodeState` with runtime state
1667    /// and computes which CSS properties have changed. It determines whether
1668    /// layout, display list, or GPU-only updates are needed.
1669    ///
1670    /// # Arguments
1671    /// * `focus_changes` - Nodes gaining/losing focus
1672    /// * `hover_changes` - Nodes gaining/losing hover
1673    /// * `active_changes` - Nodes gaining/losing active (mouse down)
1674    ///
1675    /// # Returns
1676    /// * `RestyleResult` containing changed nodes and what needs updating
1677    #[must_use]
1678    pub fn restyle_on_state_change(
1679        &mut self,
1680        focus_changes: Option<FocusChange>,
1681        hover_changes: Option<HoverChange>,
1682        active_changes: Option<ActiveChange>,
1683    ) -> RestyleResult {
1684        
1685        // Start with GPU-only assumption; refined below as changes are analyzed.
1686        let mut result = RestyleResult {
1687            gpu_only_changes: true,
1688            ..RestyleResult::default()
1689        };
1690
1691        // Helper closure to merge changes and analyze property categories
1692        let mut process_changes = |changes: RestyleNodes| {
1693            for (node_id, props) in changes {
1694                for change in &props {
1695                    let prop_type = change.current_prop.get_type();
1696
1697                    // Use the granular RelayoutScope instead of the binary
1698                    // can_trigger_relayout(). We pass node_is_ifc_member = true
1699                    // conservatively: this means font/text property changes will
1700                    // produce IfcOnly (rather than None). Phase 2c can refine
1701                    // this by checking whether the node actually participates
1702                    // in an IFC.
1703                    let scope = prop_type.relayout_scope(/* node_is_ifc_member */ true);
1704
1705                    // Track the highest scope seen
1706                    if scope > result.max_relayout_scope {
1707                        result.max_relayout_scope = scope;
1708                    }
1709
1710                    // Any scope above None triggers layout
1711                    if scope != RelayoutScope::None {
1712                        result.needs_layout = true;
1713                        result.gpu_only_changes = false;
1714                    }
1715                    
1716                    // Check if this is a GPU-only property
1717                    if !prop_type.is_gpu_only_property() {
1718                        result.gpu_only_changes = false;
1719                    }
1720                    
1721                    // Any visual change needs display list update (unless GPU-only)
1722                    result.needs_display_list = true;
1723                }
1724                
1725                result.changed_nodes.entry(node_id).or_default().extend(props);
1726            }
1727        };
1728
1729        // 1. Process focus changes
1730        if let Some(focus) = focus_changes {
1731            if let Some(old) = focus.lost_focus {
1732                let changes = self.restyle_nodes_focus(&[old], false);
1733                process_changes(changes);
1734            }
1735            if let Some(new) = focus.gained_focus {
1736                let changes = self.restyle_nodes_focus(&[new], true);
1737                process_changes(changes);
1738            }
1739        }
1740
1741        // 2. Process hover changes
1742        if let Some(hover) = hover_changes {
1743            if !hover.left_nodes.is_empty() {
1744                let changes = self.restyle_nodes_hover(&hover.left_nodes, false);
1745                process_changes(changes);
1746            }
1747            if !hover.entered_nodes.is_empty() {
1748                let changes = self.restyle_nodes_hover(&hover.entered_nodes, true);
1749                process_changes(changes);
1750            }
1751        }
1752
1753        // 3. Process active changes
1754        if let Some(active) = active_changes {
1755            if !active.deactivated.is_empty() {
1756                let changes = self.restyle_nodes_active(&active.deactivated, false);
1757                process_changes(changes);
1758            }
1759            if !active.activated.is_empty() {
1760                let changes = self.restyle_nodes_active(&active.activated, true);
1761                process_changes(changes);
1762            }
1763        }
1764
1765        // If no changes, reset display_list flag
1766        if result.changed_nodes.is_empty() {
1767            result.needs_display_list = false;
1768            result.gpu_only_changes = false;
1769        }
1770        
1771        // If layout is needed, display list is also needed
1772        if result.needs_layout {
1773            result.needs_display_list = true;
1774            result.gpu_only_changes = false;
1775        }
1776
1777        result
1778    }
1779
1780    /// Overrides CSS properties for a single node from user code (typically a
1781    /// callback). Writes into `CssPropertyCache::user_overridden_properties`,
1782    /// which `get_property_slow` / `get_property_fast` / `get_computed_value`
1783    /// consult at higher priority than the static CSS cascade — making this
1784    /// the fast path for animating a handful of properties per frame.
1785    ///
1786    /// Passing `CssProperty::Initial` for a property removes any override for
1787    /// that type, restoring the cascaded value. Returns the set of
1788    /// `ChangedCssProperty` entries the caller can feed into the incremental
1789    /// restyle pipeline.
1790    #[must_use]
1791    pub fn restyle_user_property(
1792        &mut self,
1793        node_id: &NodeId,
1794        new_properties: &[CssProperty],
1795    ) -> RestyleNodes {
1796        let mut map = BTreeMap::default();
1797
1798        if new_properties.is_empty() {
1799            return map;
1800        }
1801
1802        let node_count = self.node_data.as_ref().len();
1803        if node_id.index() >= node_count {
1804            return map;
1805        }
1806
1807        let node_data = self.node_data.as_container();
1808        let node_data = &node_data[*node_id];
1809
1810        let node_states = &self.styled_nodes.as_container();
1811        let old_node_state = &node_states[*node_id].styled_node_state;
1812
1813        let changes: Vec<ChangedCssProperty> = {
1814            let css_property_cache = self.get_css_property_cache();
1815
1816            new_properties
1817                .iter()
1818                .filter_map(|new_prop| {
1819                    let old_prop = css_property_cache.get_property_slow(
1820                        node_data,
1821                        node_id,
1822                        old_node_state,
1823                        &new_prop.get_type(),
1824                    );
1825
1826                    let old_prop = old_prop.map_or_else(|| CssProperty::auto(new_prop.get_type()), Clone::clone);
1827
1828                    if old_prop == *new_prop {
1829                        None
1830                    } else {
1831                        Some(ChangedCssProperty {
1832                            previous_state: *old_node_state,
1833                            previous_prop: old_prop,
1834                            // overriding a user property does not change the state
1835                            current_state: *old_node_state,
1836                            current_prop: new_prop.clone(),
1837                        })
1838                    }
1839                })
1840                .collect()
1841        };
1842
1843        let css_property_cache_mut = self.get_css_property_cache_mut();
1844
1845        // user_overridden_properties is built lazily (empty after StyledDom
1846        // construction). Grow to cover this node_id before indexing so the
1847        // override path works on any DOM, not just ones that already have
1848        // overrides from a prior mutation.
1849        if css_property_cache_mut.user_overridden_properties.len() < node_count {
1850            css_property_cache_mut
1851                .user_overridden_properties
1852                .resize(node_count, Vec::new());
1853        }
1854
1855        for new_prop in new_properties {
1856            let prop_type = new_prop.get_type();
1857            let vec = &mut css_property_cache_mut
1858                .user_overridden_properties[node_id.index()];
1859            if new_prop.is_initial() {
1860                // CssProperty::Initial = remove overridden property
1861                if let Ok(idx) = vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
1862                    vec.remove(idx);
1863                }
1864            } else {
1865                match vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
1866                    Ok(idx) => vec[idx].1 = new_prop.clone(),
1867                    Err(idx) => vec.insert(idx, (prop_type, new_prop.clone())),
1868                }
1869            }
1870        }
1871
1872        if !changes.is_empty() {
1873            map.insert(*node_id, changes);
1874        }
1875
1876        map
1877    }
1878
1879    /// Returns a HTML-formatted version of the DOM for easier debugging.
1880    ///
1881    /// For example, a DOM with a parent div containing a child div would return:
1882    ///
1883    /// ```xml,no_run,ignore
1884    /// <div id="hello">
1885    ///      <div id="test" />
1886    /// </div>
1887    /// ```
1888    #[must_use] pub fn get_html_string(&self, custom_head: &str, custom_body: &str, test_mode: bool) -> String {
1889        let css_property_cache = self.get_css_property_cache();
1890
1891        let mut output = String::new();
1892
1893        // After which nodes should a close tag be printed?
1894        let mut should_print_close_tag_after_node: BTreeMap<NodeId, Vec<(NodeId, usize)>> = BTreeMap::new();
1895
1896        let should_print_close_tag_debug = self
1897            .non_leaf_nodes
1898            .iter()
1899            .filter_map(|p| {
1900                let parent_node_id = p.node_id.into_crate_internal()?;
1901                let mut total_last_child = None;
1902                recursive_get_last_child(
1903                    parent_node_id,
1904                    self.node_hierarchy.as_ref(),
1905                    &mut total_last_child,
1906                );
1907                let total_last_child = total_last_child?;
1908                Some((parent_node_id, (total_last_child, p.depth)))
1909            })
1910            .collect::<BTreeMap<_, _>>();
1911
1912        for (parent_id, (last_child, parent_depth)) in should_print_close_tag_debug {
1913            should_print_close_tag_after_node
1914                .entry(last_child)
1915                .or_default()
1916                .push((parent_id, parent_depth));
1917        }
1918
1919        let mut all_node_depths = self
1920            .non_leaf_nodes
1921            .iter()
1922            .filter_map(|p| {
1923                let parent_node_id = p.node_id.into_crate_internal()?;
1924                Some((parent_node_id, p.depth))
1925            })
1926            .collect::<BTreeMap<_, _>>();
1927
1928        for (parent_node_id, parent_depth) in self
1929            .non_leaf_nodes
1930            .iter()
1931            .filter_map(|p| Some((p.node_id.into_crate_internal()?, p.depth)))
1932        {
1933            for child_id in parent_node_id.az_children(&self.node_hierarchy.as_container()) {
1934                all_node_depths.insert(child_id, parent_depth + 1);
1935            }
1936        }
1937
1938        for node_id in self.node_hierarchy.as_container().linear_iter() {
1939            // A single-node DOM (or any node not reached as a non-leaf parent or
1940            // one of their children, e.g. a lone root) has no entry here; treat
1941            // its depth as 0 instead of panic-indexing the map.
1942            let depth = all_node_depths.get(&node_id).copied().unwrap_or(0);
1943
1944            let node_data = &self.node_data.as_container()[node_id];
1945            let node_state = &self.styled_nodes.as_container()[node_id].styled_node_state;
1946            let tabs = String::from("    ").repeat(depth);
1947
1948            output.push_str("\r\n");
1949            output.push_str(&tabs);
1950            output.push_str(&node_data.debug_print_start(css_property_cache, &node_id, node_state));
1951
1952            if let Some(content) = node_data.get_node_type().format().as_ref() {
1953                output.push_str(content);
1954            }
1955
1956            let node_has_children = self.node_hierarchy.as_container()[node_id]
1957                .first_child_id(node_id)
1958                .is_some();
1959            if !node_has_children {
1960                let node_data = &self.node_data.as_container()[node_id];
1961                output.push_str(&node_data.debug_print_end());
1962            }
1963
1964            if let Some(close_tag_vec) = should_print_close_tag_after_node.get(&node_id) {
1965                let mut close_tag_vec = close_tag_vec.clone();
1966                close_tag_vec.sort_by(|a, b| b.1.cmp(&a.1)); // sort by depth descending
1967                for (close_tag_parent_id, close_tag_depth) in close_tag_vec {
1968                    let node_data = &self.node_data.as_container()[close_tag_parent_id];
1969                    let tabs = String::from("    ").repeat(close_tag_depth);
1970                    output.push_str("\r\n");
1971                    output.push_str(&tabs);
1972                    output.push_str(&node_data.debug_print_end());
1973                }
1974            }
1975        }
1976
1977        if test_mode {
1978            output
1979        } else {
1980            format!(
1981                "
1982                <html>
1983                    <head>
1984                    <style>* {{ margin:0px; padding:0px; }}</style>
1985                    {custom_head}
1986                    </head>
1987                {output}
1988                {custom_body}
1989                </html>
1990            "
1991            )
1992        }
1993    }
1994
1995    /// Returns nodes grouped by their rendering order (respects z-index and position).
1996    #[must_use] pub fn get_rects_in_rendering_order(&self) -> ContentGroup {
1997        Self::determine_rendering_order(
1998            self.non_leaf_nodes.as_ref(),
1999            &self.node_hierarchy.as_container(),
2000            &self.styled_nodes.as_container(),
2001            &self.node_data.as_container(),
2002            self.get_css_property_cache(),
2003        )
2004    }
2005
2006    /// Returns the rendering order of the items (the rendering
2007    /// order doesn't have to be the original order)
2008    fn determine_rendering_order(
2009        non_leaf_nodes: &[ParentWithNodeDepth],
2010        node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
2011        styled_nodes: &NodeDataContainerRef<'_, StyledNode>,
2012        node_data_container: &NodeDataContainerRef<'_, NodeData>,
2013        css_property_cache: &CssPropertyCache,
2014    ) -> ContentGroup {
2015        let children_sorted = non_leaf_nodes
2016            .iter()
2017            .filter_map(|parent| {
2018                Some((
2019                    parent.node_id,
2020                    sort_children_by_position(
2021                        parent.node_id.into_crate_internal()?,
2022                        node_hierarchy,
2023                        styled_nodes,
2024                        node_data_container,
2025                        css_property_cache,
2026                    ),
2027                ))
2028            })
2029            .collect::<Vec<_>>();
2030
2031        let children_sorted: BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>> =
2032            children_sorted.into_iter().collect();
2033
2034        let mut root_content_group = ContentGroup {
2035            root: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
2036            children: Vec::new().into(),
2037        };
2038
2039        fill_content_group_children(&mut root_content_group, &children_sorted);
2040
2041        root_content_group
2042    }
2043
2044    /// Replaces this `StyledDom` with default and returns the old value.
2045    #[must_use] pub fn swap_with_default(&mut self) -> Self {
2046        let mut new = Self::default();
2047        core::mem::swap(self, &mut new);
2048        new
2049    }
2050
2051}
2052
2053/// Same as `Dom`, but arena-based for more efficient memory layout and faster traversal.
2054#[derive(Debug, PartialEq, PartialOrd, Eq)]
2055pub struct CompactDom {
2056    /// The arena containing the hierarchical relationships (parent, child, sibling) of all nodes.
2057    pub node_hierarchy: NodeHierarchy,
2058    /// The arena containing the actual data (`NodeData`) for each node.
2059    pub node_data: NodeDataContainer<NodeData>,
2060    /// The ID of the root node of the DOM tree.
2061    pub root: NodeId,
2062}
2063
2064impl CompactDom {
2065    /// Returns the number of nodes in this DOM.
2066    #[inline]
2067    #[must_use] pub fn len(&self) -> usize {
2068        self.node_hierarchy.as_ref().len()
2069    }
2070
2071    /// Returns `true` if this DOM has no nodes.
2072    #[inline]
2073    #[must_use] pub fn is_empty(&self) -> bool {
2074        self.node_hierarchy.as_ref().is_empty()
2075    }
2076}
2077
2078impl From<Dom> for CompactDom {
2079    fn from(dom: Dom) -> Self {
2080        convert_dom_into_compact_dom(dom)
2081    }
2082}
2083
2084/// Converts a tree-based Dom into an arena-based `CompactDom` for efficient traversal.
2085#[must_use] pub fn convert_dom_into_compact_dom(mut dom: Dom) -> CompactDom {
2086    // note: somehow convert this into a non-recursive form later on!
2087    fn convert_dom_into_compact_dom_internal(
2088        dom: &mut Dom,
2089        node_hierarchy: &mut [Node],
2090        node_data: &mut Vec<NodeData>,
2091        parent_node_id: NodeId,
2092        node: Node,
2093        cur_node_id: &mut usize,
2094    ) {
2095        // - parent [0]
2096        //    - child [1]
2097        //    - child [2]
2098        //        - child of child 2 [2]
2099        //        - child of child 2 [4]
2100        //    - child [5]
2101        //    - child [6]
2102        //        - child of child 4 [7]
2103
2104        // Write node into the arena here!
2105        node_hierarchy[parent_node_id.index()] = node;
2106
2107        // MOVE the node's inline `style` AND its `extra` (NodeDataExt) box instead of relying on
2108        // copy_special's `self.style.clone()` / `self.extra.clone()`. Both derived Clones lower to
2109        // indirect-jump jump tables that remill mis-lifts on the web backend: CssProperty's clone
2110        // comes back with discriminant 0 (drops simple inline CSS) and for COMPLEX values (AzButton's
2111        // gradient; the NodeDataExt attributes Vec) the mis-lifted clone reads/writes wrong-sized data,
2112        // which clobbers the adjacent `style` temporary → "memory access out of bounds" later in the
2113        // cascade (StyledDom::create → restyle's inheritance loop reads the corrupted style). 2026-06-02:
2114        // copy_special_moving_complex mem::takes BOTH style+extra before copy_special, so copy_special
2115        // clones an EMPTY style + None extra (no broken clone runs) and restores them after. (Extra was
2116        // added after the AzButton ids/classes node — which lazily allocates NodeDataExt — OOB'd even
2117        // with the style-only take.) The Dom is consumed here, so the move is correct.
2118        let copy = dom.root.copy_special_moving_complex();
2119
2120        node_data[parent_node_id.index()] = copy;
2121
2122        *cur_node_id += 1;
2123
2124        let mut previous_sibling_id = None;
2125        let children_len = dom.children.len();
2126        for (child_index, child_dom) in dom.children.as_mut().iter_mut().enumerate() {
2127            let child_node_id = NodeId::new(*cur_node_id);
2128            let is_last_child = (child_index + 1) == children_len;
2129            let child_dom_is_empty = child_dom.children.is_empty();
2130            let child_node = Node {
2131                parent: Some(parent_node_id),
2132                previous_sibling: previous_sibling_id,
2133                next_sibling: if is_last_child {
2134                    None
2135                } else {
2136                    Some(child_node_id + child_dom.estimated_total_children + 1)
2137                },
2138                last_child: if child_dom_is_empty {
2139                    None
2140                } else {
2141                    Some(child_node_id + child_dom.estimated_total_children)
2142                },
2143            };
2144            previous_sibling_id = Some(child_node_id);
2145            // recurse BEFORE adding the next child
2146            convert_dom_into_compact_dom_internal(
2147                child_dom,
2148                node_hierarchy,
2149                node_data,
2150                child_node_id,
2151                child_node,
2152                cur_node_id,
2153            );
2154        }
2155    }
2156
2157    // Pre-allocate all nodes (+ 1 root node)
2158    let sum_nodes = dom.fixup_children_estimated();
2159
2160    let mut node_hierarchy = vec![Node::ROOT; sum_nodes + 1];
2161    let mut node_data = vec![NodeData::create_div(); sum_nodes + 1];
2162    let mut cur_node_id = 0;
2163
2164    let root_node_id = NodeId::ZERO;
2165    let root_node = Node {
2166        parent: None,
2167        previous_sibling: None,
2168        next_sibling: None,
2169        last_child: if dom.children.is_empty() {
2170            None
2171        } else {
2172            Some(root_node_id + dom.estimated_total_children)
2173        },
2174    };
2175
2176    convert_dom_into_compact_dom_internal(
2177        &mut dom,
2178        &mut node_hierarchy,
2179        &mut node_data,
2180        root_node_id,
2181        root_node,
2182        &mut cur_node_id,
2183    );
2184
2185    CompactDom {
2186        node_hierarchy: NodeHierarchy {
2187            internal: node_hierarchy,
2188        },
2189        node_data: NodeDataContainer {
2190            internal: node_data,
2191        },
2192        root: root_node_id,
2193    }
2194}
2195
2196/// #47: scope every node's inline css to its own subtree. Walks the tree in the
2197/// SAME pre-order `convert_dom_into_compact_dom` uses to assign flat `NodeIds`, so the
2198/// `[flat_id, flat_id + estimated_total_children]` range pushed onto each rule (via
2199/// `CssPath::push_front_scope`) matches the ids the cascade will later see. After
2200/// this, a node's `with_css`/`set_css` rules can only match nodes inside its subtree
2201/// — they can no longer leak to the whole tree. `fixup_children_estimated()` must
2202/// have run first so `estimated_total_children` is populated/exact.
2203fn scope_inline_css(dom: &mut Dom, next_id: &mut usize) {
2204    let start = *next_id;
2205    let end = start + dom.estimated_total_children;
2206    for css in dom.css.as_mut().iter_mut() {
2207        for rule in css.rules.as_mut().iter_mut() {
2208            // push_front_scope picks node-only ([start,start]) for bare `*` rules
2209            // (with_css inline decls) and subtree ([start,end]) for rules with a real
2210            // selector (add_component_css), so descendant selectors match the subtree.
2211            rule.path.push_front_scope(start, end);
2212        }
2213    }
2214    *next_id += 1;
2215    for child in dom.children.as_mut().iter_mut() {
2216        scope_inline_css(child, next_id);
2217    }
2218}
2219
2220/// Recursively collect all CSS objects from a Dom tree (depth-first).
2221/// Inner (deeper) CSS objects come first, outer (shallower) CSS objects come last.
2222/// This means outer CSS has higher cascade priority when applied in order.
2223fn collect_css_from_dom(dom: &Dom, out: &mut Vec<Css>) {
2224    // First, recurse into children (inner CSS = lower priority)
2225    for child in &dom.children {
2226        collect_css_from_dom(child, out);
2227    }
2228    // Then, add this node's CSS objects (outer CSS = higher priority)
2229    for css in &dom.css {
2230        out.push(css.clone());
2231    }
2232}
2233
2234/// Recursively strip CSS from all Dom nodes (sets css to empty vec).
2235/// Called after collecting CSS so the `CompactDom` doesn't carry CSS data.
2236fn strip_css_from_dom(dom: &mut Dom) {
2237    dom.css = Vec::new().into();
2238    for child in dom.children.as_mut().iter_mut() {
2239        strip_css_from_dom(child);
2240    }
2241}
2242
2243fn fill_content_group_children(
2244    group: &mut ContentGroup,
2245    children_sorted: &BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>>,
2246) {
2247    if let Some(c) = children_sorted.get(&group.root) {
2248        // returns None for leaf nodes
2249        group.children = c
2250            .iter()
2251            .map(|child| ContentGroup {
2252                root: *child,
2253                children: Vec::new().into(),
2254            })
2255            .collect::<Vec<ContentGroup>>()
2256            .into();
2257
2258        for c in group.children.as_mut() {
2259            fill_content_group_children(c, children_sorted);
2260        }
2261    }
2262}
2263
2264fn sort_children_by_position(
2265    parent: NodeId,
2266    node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
2267    rectangles: &NodeDataContainerRef<'_, StyledNode>,
2268    node_data_container: &NodeDataContainerRef<'_, NodeData>,
2269    css_property_cache: &CssPropertyCache,
2270) -> Vec<NodeHierarchyItemId> {
2271    use azul_css::props::layout::LayoutPosition::Absolute;
2272
2273    let children_positions = parent
2274        .az_children(node_hierarchy)
2275        .map(|nid| {
2276            let position = css_property_cache
2277                .get_position(
2278                    &node_data_container[nid],
2279                    &nid,
2280                    &rectangles[nid].styled_node_state,
2281                )
2282                .and_then(|p| (*p).get_property_or_default())
2283                .unwrap_or_default();
2284            let id = NodeHierarchyItemId::from_crate_internal(Some(nid));
2285            (id, position)
2286        })
2287        .collect::<Vec<_>>();
2288
2289    let mut not_absolute_children = children_positions
2290        .iter()
2291        .filter_map(|(node_id, position)| {
2292            if *position == Absolute {
2293                None
2294            } else {
2295                Some(*node_id)
2296            }
2297        })
2298        .collect::<Vec<_>>();
2299
2300    let mut absolute_children = children_positions
2301        .iter()
2302        .filter_map(|(node_id, position)| {
2303            if *position == Absolute {
2304                Some(*node_id)
2305            } else {
2306                None
2307            }
2308        })
2309        .collect::<Vec<_>>();
2310
2311    // Append the position:absolute children after the regular children
2312    not_absolute_children.append(&mut absolute_children);
2313    not_absolute_children
2314}
2315
2316// calls get_last_child() recursively until the last child of the last child of the ... has been
2317// found
2318fn recursive_get_last_child(
2319    node_id: NodeId,
2320    node_hierarchy: &[NodeHierarchyItem],
2321    target: &mut Option<NodeId>,
2322) {
2323    match node_hierarchy[node_id.index()].last_child_id() {
2324        None => (),
2325        Some(s) => {
2326            *target = Some(s);
2327            recursive_get_last_child(s, node_hierarchy, target);
2328        }
2329    }
2330}
2331
2332// ============================================================================
2333// DOM TRAVERSAL FOR MULTI-NODE SELECTION
2334// ============================================================================
2335
2336/// Determine if `node_a` comes before `node_b` in document order.
2337///
2338/// Document order is defined as pre-order depth-first traversal order.
2339/// This is equivalent to the order nodes appear in HTML source.
2340///
2341/// ## Algorithm
2342/// 1. Find the path from root to each node
2343/// 2. Find the Lowest Common Ancestor (LCA)
2344/// 3. At the divergence point, the child that appears first in sibling order comes first
2345#[must_use] pub fn is_before_in_document_order(
2346    hierarchy: &NodeHierarchyItemVec,
2347    node_a: NodeId,
2348    node_b: NodeId,
2349) -> bool {
2350    if node_a == node_b {
2351        return false;
2352    }
2353    
2354    let hierarchy = hierarchy.as_container();
2355    
2356    // Get paths from root to each node (stored as root-first order)
2357    let path_a = get_path_to_root(&hierarchy, node_a);
2358    let path_b = get_path_to_root(&hierarchy, node_b);
2359    
2360    // Find divergence point (last common ancestor)
2361    let min_len = path_a.len().min(path_b.len());
2362    
2363    for i in 0..min_len {
2364        if path_a[i] != path_b[i] {
2365            // Found divergence - check which sibling comes first
2366            let child_towards_a = path_a[i];
2367            let child_towards_b = path_b[i];
2368            
2369            // A smaller NodeId index means it was created earlier in DOM construction,
2370            // which means it comes first in document order for siblings
2371            return child_towards_a.index() < child_towards_b.index();
2372        }
2373    }
2374    
2375    // One path is a prefix of the other - the shorter path (ancestor) comes first
2376    path_a.len() < path_b.len()
2377}
2378
2379/// Get the path from root to a node, returned in root-first order.
2380fn get_path_to_root(
2381    hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
2382    node: NodeId,
2383) -> Vec<NodeId> {
2384    let mut path = Vec::new();
2385    let mut current = Some(node);
2386    
2387    while let Some(node_id) = current {
2388        path.push(node_id);
2389        current = hierarchy.get(node_id).and_then(NodeHierarchyItem::parent_id);
2390    }
2391    
2392    // Reverse to get root-first order
2393    path.reverse();
2394    path
2395}
2396
2397/// Collect all nodes between start and end (inclusive) in document order.
2398///
2399/// This performs a pre-order depth-first traversal starting from the root,
2400/// collecting nodes once we've seen `start` and stopping at `end`.
2401///
2402/// ## Parameters
2403/// * `hierarchy` - The node hierarchy
2404/// * `start_node` - First node in document order
2405/// * `end_node` - Last node in document order
2406///
2407/// ## Returns
2408/// Vector of `NodeIds` in document order, from start to end (inclusive)
2409#[must_use] pub fn collect_nodes_in_document_order(
2410    hierarchy: &NodeHierarchyItemVec,
2411    start_node: NodeId,
2412    end_node: NodeId,
2413) -> Vec<NodeId> {
2414    if start_node == end_node {
2415        return vec![start_node];
2416    }
2417    
2418    let hierarchy_container = hierarchy.as_container();
2419    let hierarchy_slice = hierarchy.as_ref();
2420    
2421    let mut result = Vec::new();
2422    let mut in_range = false;
2423    
2424    // Pre-order DFS using a stack
2425    // We need to traverse in document order, which is pre-order DFS
2426    let mut stack: Vec<NodeId> = vec![NodeId::ZERO]; // Start from root
2427    
2428    while let Some(current) = stack.pop() {
2429        // Check if we've entered the range
2430        if current == start_node {
2431            in_range = true;
2432        }
2433        
2434        // Collect if in range
2435        if in_range {
2436            result.push(current);
2437        }
2438        
2439        // Check if we've exited the range
2440        if current == end_node {
2441            break;
2442        }
2443        
2444        // Push children in reverse order so they pop in correct order
2445        // (first child should be processed first)
2446        if let Some(item) = hierarchy_container.get(current) {
2447            // Get first child
2448            if let Some(first_child) = item.first_child_id(current) {
2449                // Collect all children by following next_sibling
2450                let mut children = Vec::new();
2451                let mut child = Some(first_child);
2452                while let Some(child_id) = child {
2453                    children.push(child_id);
2454                    child = hierarchy_container.get(child_id).and_then(NodeHierarchyItem::next_sibling_id);
2455                }
2456                // Push in reverse order for correct DFS order
2457                for child_id in children.into_iter().rev() {
2458                    stack.push(child_id);
2459                }
2460            }
2461        }
2462    }
2463    
2464    result
2465}
2466
2467/// Check if two `StyledDom`s are structurally equivalent for layout purposes.
2468///
2469/// Returns `true` if the DOMs have the same structure, node types, classes,
2470/// IDs, inline styles, and callback event registrations — meaning the
2471/// layout output would be identical.
2472///
2473/// Image callback nodes are compared by function pointer and `RefAny` type ID
2474/// rather than heap pointer, since each `layout()` call creates new `ImageRef`
2475/// allocations even when the callback is the same.
2476///
2477/// This is used to short-circuit the expensive layout pipeline when the DOM
2478/// hasn't actually changed (e.g., an animation timer fires but only the GL
2479/// texture content changed, not the DOM structure).
2480#[must_use] pub fn is_layout_equivalent(old: &StyledDom, new: &StyledDom) -> bool {
2481    use crate::dom::NodeType;
2482    use crate::resources::DecodedImage;
2483
2484    // Quick check: node count must match
2485    let old_nodes = old.node_data.as_ref();
2486    let new_nodes = new.node_data.as_ref();
2487    if old_nodes.len() != new_nodes.len() {
2488        return false;
2489    }
2490
2491    // Check hierarchy (parent/child/sibling structure)
2492    let old_hier = old.node_hierarchy.as_ref();
2493    let new_hier = new.node_hierarchy.as_ref();
2494    if old_hier.len() != new_hier.len() {
2495        return false;
2496    }
2497    if old_hier != new_hier {
2498        return false;
2499    }
2500
2501    // Per-node comparison
2502    for (old_node, new_node) in old_nodes.iter().zip(new_nodes.iter()) {
2503
2504        // Compare node type discriminant
2505        if core::mem::discriminant(&old_node.node_type)
2506            != core::mem::discriminant(&new_node.node_type)
2507        {
2508            return false;
2509        }
2510
2511        // Compare node type content (with special handling for image callbacks)
2512        match (&old_node.node_type, &new_node.node_type) {
2513            (NodeType::Image(old_img), NodeType::Image(new_img)) => {
2514                match (old_img.get_data(), new_img.get_data()) {
2515                    (DecodedImage::Callback(old_cb), DecodedImage::Callback(new_cb)) => {
2516                        // Compare callback function pointer (stable across frames)
2517                        if old_cb.callback.cb != new_cb.callback.cb {
2518                            return false;
2519                        }
2520                        // Compare RefAny type ID (not instance pointer)
2521                        if old_cb.refany.get_type_id() != new_cb.refany.get_type_id() {
2522                            return false;
2523                        }
2524                    }
2525                    _ => {
2526                        // Raw images / GL textures: compare by pointer identity
2527                        if old_img != new_img {
2528                            return false;
2529                        }
2530                    }
2531                }
2532            }
2533            _ => {
2534                if old_node.node_type != new_node.node_type {
2535                    return false;
2536                }
2537            }
2538        }
2539
2540        // Compare IDs and classes (now stored in attributes as AttributeType::Id/Class)
2541        {
2542            use crate::dom::AttributeType;
2543            let old_ids_classes: Vec<_> = old_node.attributes().as_ref().iter()
2544                .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
2545                .collect();
2546            let new_ids_classes: Vec<_> = new_node.attributes().as_ref().iter()
2547                .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
2548                .collect();
2549            if old_ids_classes != new_ids_classes {
2550                return false;
2551            }
2552        }
2553
2554        // Compare inline CSS (direct layout input)
2555        if old_node.style != new_node.style {
2556            return false;
2557        }
2558
2559        // Compare callback event types (affects hit-test tags)
2560        // We compare only event types, not function pointers or data
2561        let old_cbs = old_node.callbacks.as_ref();
2562        let new_cbs = new_node.callbacks.as_ref();
2563        if old_cbs.len() != new_cbs.len() {
2564            return false;
2565        }
2566        for (old_cb, new_cb) in old_cbs.iter().zip(new_cbs.iter()) {
2567            if old_cb.event != new_cb.event {
2568                return false;
2569            }
2570        }
2571
2572        // Compare attributes (some affect layout, e.g. colspan)
2573        if old_node.attributes().as_ref() != new_node.attributes().as_ref() {
2574            return false;
2575        }
2576    }
2577
2578    // Compare styled node states (hover/focus/active flags affect CSS resolution)
2579    let old_styled = old.styled_nodes.as_ref();
2580    let new_styled = new.styled_nodes.as_ref();
2581    if old_styled.len() != new_styled.len() {
2582        return false;
2583    }
2584    if old_styled != new_styled {
2585        return false;
2586    }
2587
2588    true
2589}
2590
2591#[cfg(test)]
2592mod audit_tests {
2593    use super::*;
2594    use azul_css::props::basic::StyleFontFamily;
2595
2596    fn fam(name: &str) -> StyleFontFamily {
2597        StyleFontFamily::System(name.to_string().into())
2598    }
2599
2600    #[test]
2601    fn style_font_families_hash_is_length_sensitive() {
2602        // The length prefix guarantees that lists of different lengths cannot
2603        // collide, and that hashing is deterministic.
2604        let a = StyleFontFamiliesHash::new(&[fam("Arial")]);
2605        let a2 = StyleFontFamiliesHash::new(&[fam("Arial")]);
2606        assert_eq!(a, a2, "hash must be deterministic");
2607
2608        let two = StyleFontFamiliesHash::new(&[fam("Arial"), fam("Helvetica")]);
2609        assert_ne!(a, two, "different-length family lists must not collide");
2610
2611        let empty = StyleFontFamiliesHash::new(&[]);
2612        assert_ne!(empty, a);
2613        assert_ne!(empty, two);
2614
2615        // Order still matters.
2616        let rev = StyleFontFamiliesHash::new(&[fam("Helvetica"), fam("Arial")]);
2617        assert_ne!(two, rev);
2618    }
2619}
2620
2621#[cfg(test)]
2622#[allow(clippy::too_many_lines)]
2623mod autotest_generated {
2624    use azul_css::{
2625        dynamic_selector::PseudoStateFlags,
2626        props::basic::StyleFontFamily,
2627    };
2628
2629    use super::*;
2630
2631    // ---------------------------------------------------------------------
2632    // helpers
2633    // ---------------------------------------------------------------------
2634
2635    /// Builds a `NodeHierarchyItem` directly from the RAW (1-based) encoding:
2636    /// `0` = none, `n` = `NodeId(n - 1)`.
2637    const fn raw_item(parent: usize, prev: usize, next: usize, last: usize) -> NodeHierarchyItem {
2638        NodeHierarchyItem {
2639            parent,
2640            previous_sibling: prev,
2641            next_sibling: next,
2642            last_child: last,
2643        }
2644    }
2645
2646    /// `<body>` with `n` leaf `<div>` children, cascaded against an empty stylesheet.
2647    /// Node ids are `0 = body`, `1..=n` = the children.
2648    fn flat_body(n: usize) -> StyledDom {
2649        let children: Vec<Dom> = (0..n).map(|_| Dom::create_div()).collect();
2650        let mut dom = Dom::create_body().with_children(children.into());
2651        StyledDom::create(&mut dom, Css::empty())
2652    }
2653
2654    /// `<body> > <div> > <div>` — the last direct child of the root is itself a parent.
2655    fn nested_body() -> StyledDom {
2656        let mut dom = Dom::create_body().with_children(
2657            vec![Dom::create_div().with_children(vec![Dom::create_div()].into())].into(),
2658        );
2659        StyledDom::create(&mut dom, Css::empty())
2660    }
2661
2662    fn parse_css(s: &str) -> Css {
2663        azul_css::parser2::new_from_str(s).0
2664    }
2665
2666    fn family(name: &str) -> StyleFontFamily {
2667        StyleFontFamily::System(name.to_string().into())
2668    }
2669
2670    const fn pseudo_flags(all: bool) -> PseudoStateFlags {
2671        PseudoStateFlags {
2672            hover: all,
2673            active: all,
2674            focused: all,
2675            disabled: all,
2676            checked: all,
2677            focus_within: all,
2678            visited: all,
2679            backdrop: all,
2680            dragging: all,
2681            drag_over: all,
2682        }
2683    }
2684
2685    fn empty_menu() -> Menu {
2686        let items: Vec<crate::menu::MenuItem> = Vec::new();
2687        Menu::create(items.into())
2688    }
2689
2690    // ---------------------------------------------------------------------
2691    // RestyleResult (predicate + merge)
2692    // ---------------------------------------------------------------------
2693
2694    #[test]
2695    fn restyle_result_default_reports_no_changes() {
2696        let r = RestyleResult::default();
2697        assert!(!r.has_changes());
2698        assert!(!r.needs_layout);
2699        assert!(!r.needs_display_list);
2700        assert!(!r.gpu_only_changes);
2701        assert_eq!(r.max_relayout_scope, RelayoutScope::None);
2702    }
2703
2704    #[test]
2705    fn restyle_result_has_changes_keys_off_node_map_not_property_count() {
2706        // A node entry with an EMPTY change list still counts as "changed":
2707        // has_changes() only looks at the node map, never at the inner Vec.
2708        let mut r = RestyleResult::default();
2709        r.changed_nodes.insert(NodeId::ZERO, Vec::new());
2710        assert!(r.has_changes());
2711
2712        r.changed_nodes.clear();
2713        assert!(!r.has_changes());
2714    }
2715
2716    #[test]
2717    fn restyle_result_merge_ors_layout_flags_and_ands_gpu_only() {
2718        let mut a = RestyleResult {
2719            needs_layout: false,
2720            needs_display_list: false,
2721            gpu_only_changes: true,
2722            ..RestyleResult::default()
2723        };
2724        let b = RestyleResult {
2725            needs_layout: true,
2726            needs_display_list: true,
2727            gpu_only_changes: true,
2728            ..RestyleResult::default()
2729        };
2730        a.merge(b);
2731        assert!(a.needs_layout, "needs_layout is OR-ed");
2732        assert!(a.needs_display_list, "needs_display_list is OR-ed");
2733        assert!(a.gpu_only_changes, "true && true stays true");
2734
2735        // ...and a single non-GPU-only participant clears the flag.
2736        let mut c = RestyleResult {
2737            gpu_only_changes: true,
2738            ..RestyleResult::default()
2739        };
2740        c.merge(RestyleResult {
2741            gpu_only_changes: false,
2742            ..RestyleResult::default()
2743        });
2744        assert!(!c.gpu_only_changes, "gpu_only_changes is AND-ed");
2745    }
2746
2747    #[test]
2748    fn restyle_result_merge_keeps_the_most_expensive_scope() {
2749        let mut low = RestyleResult {
2750            max_relayout_scope: RelayoutScope::None,
2751            ..RestyleResult::default()
2752        };
2753        low.merge(RestyleResult {
2754            max_relayout_scope: RelayoutScope::Full,
2755            ..RestyleResult::default()
2756        });
2757        assert_eq!(low.max_relayout_scope, RelayoutScope::Full);
2758
2759        // ...and merging a cheaper scope must NOT downgrade it.
2760        let mut high = RestyleResult {
2761            max_relayout_scope: RelayoutScope::Full,
2762            ..RestyleResult::default()
2763        };
2764        high.merge(RestyleResult {
2765            max_relayout_scope: RelayoutScope::IfcOnly,
2766            ..RestyleResult::default()
2767        });
2768        assert_eq!(high.max_relayout_scope, RelayoutScope::Full);
2769    }
2770
2771    #[test]
2772    fn restyle_result_merge_of_default_is_not_the_identity_for_gpu_only() {
2773        // `RestyleResult::default()` has gpu_only_changes == false, and merge()
2774        // AND-s that flag — so merging an EMPTY result still clears it. Pinned
2775        // here because it is a genuine footgun for callers that merge in a loop.
2776        let mut a = RestyleResult {
2777            gpu_only_changes: true,
2778            ..RestyleResult::default()
2779        };
2780        a.merge(RestyleResult::default());
2781        assert!(!a.gpu_only_changes);
2782        assert!(!a.has_changes());
2783    }
2784
2785    #[test]
2786    fn restyle_result_merge_concatenates_changes_for_the_same_node() {
2787        let prop = |t| ChangedCssProperty {
2788            previous_state: StyledNodeState::new(),
2789            previous_prop: CssProperty::auto(t),
2790            current_state: StyledNodeState::new(),
2791            current_prop: CssProperty::initial(t),
2792        };
2793
2794        let mut a = RestyleResult::default();
2795        a.changed_nodes
2796            .insert(NodeId::ZERO, vec![prop(CssPropertyType::Width)]);
2797
2798        let mut b = RestyleResult::default();
2799        b.changed_nodes
2800            .insert(NodeId::ZERO, vec![prop(CssPropertyType::Height)]);
2801        b.changed_nodes
2802            .insert(NodeId::new(1), vec![prop(CssPropertyType::Opacity)]);
2803
2804        a.merge(b);
2805
2806        assert_eq!(a.changed_nodes.len(), 2);
2807        assert_eq!(
2808            a.changed_nodes[&NodeId::ZERO].len(),
2809            2,
2810            "changes for the same node are appended, not replaced"
2811        );
2812        assert_eq!(a.changed_nodes[&NodeId::new(1)].len(), 1);
2813        assert!(a.has_changes());
2814    }
2815
2816    // ---------------------------------------------------------------------
2817    // StyledNodeState (constructor + predicates)
2818    // ---------------------------------------------------------------------
2819
2820    #[test]
2821    fn styled_node_state_new_is_all_false_and_normal() {
2822        let s = StyledNodeState::new();
2823        assert!(s.is_normal());
2824        assert!(!s.hover);
2825        assert!(!s.active);
2826        assert!(!s.focused);
2827        assert!(!s.disabled);
2828        assert!(!s.checked);
2829        assert!(!s.focus_within);
2830        assert!(!s.visited);
2831        assert!(!s.backdrop);
2832        assert!(!s.dragging);
2833        assert!(!s.drag_over);
2834        assert_eq!(s, StyledNodeState::default());
2835    }
2836
2837    #[test]
2838    fn styled_node_state_has_state_zero_is_always_true() {
2839        // 0 == "Normal", which is active regardless of the other flags.
2840        assert!(StyledNodeState::new().has_state(0));
2841        assert!(StyledNodeState::from_pseudo_state_flags(&pseudo_flags(true)).has_state(0));
2842    }
2843
2844    #[test]
2845    fn styled_node_state_has_state_maps_every_index_exactly_once() {
2846        // Each setter must light up exactly one state index in 1..=10.
2847        let setters: [(u8, fn(&mut StyledNodeState)); 10] = [
2848            (1, |s| s.hover = true),
2849            (2, |s| s.active = true),
2850            (3, |s| s.focused = true),
2851            (4, |s| s.disabled = true),
2852            (5, |s| s.checked = true),
2853            (6, |s| s.focus_within = true),
2854            (7, |s| s.visited = true),
2855            (8, |s| s.backdrop = true),
2856            (9, |s| s.dragging = true),
2857            (10, |s| s.drag_over = true),
2858        ];
2859
2860        for (expected_idx, set) in setters {
2861            let mut s = StyledNodeState::new();
2862            set(&mut s);
2863            assert!(!s.is_normal(), "state {expected_idx} must not be 'normal'");
2864            for idx in 1..=10u8 {
2865                assert_eq!(
2866                    s.has_state(idx),
2867                    idx == expected_idx,
2868                    "state index {idx} misreported for setter {expected_idx}"
2869                );
2870            }
2871        }
2872    }
2873
2874    #[test]
2875    fn styled_node_state_has_state_is_false_for_every_out_of_range_u8() {
2876        let all_on = StyledNodeState::from_pseudo_state_flags(&pseudo_flags(true));
2877        for idx in 11..=u8::MAX {
2878            assert!(!StyledNodeState::new().has_state(idx));
2879            assert!(
2880                !all_on.has_state(idx),
2881                "unknown state index {idx} must be inactive even when every flag is set"
2882            );
2883        }
2884    }
2885
2886    #[test]
2887    fn styled_node_state_from_pseudo_state_flags_roundtrips_every_field() {
2888        let all_on = StyledNodeState::from_pseudo_state_flags(&pseudo_flags(true));
2889        assert!(!all_on.is_normal());
2890        for idx in 0..=10u8 {
2891            assert!(all_on.has_state(idx), "state {idx} should be active");
2892        }
2893
2894        let all_off = StyledNodeState::from_pseudo_state_flags(&pseudo_flags(false));
2895        assert!(all_off.is_normal());
2896        assert_eq!(all_off, StyledNodeState::new());
2897    }
2898
2899    #[test]
2900    fn styled_node_state_debug_lists_active_states_and_normal_when_empty() {
2901        assert_eq!(format!("{:?}", StyledNodeState::new()), "[\"normal\"]");
2902
2903        let mut s = StyledNodeState::new();
2904        s.hover = true;
2905        s.drag_over = true;
2906        let dbg = format!("{s:?}");
2907        assert!(dbg.contains("hover"), "{dbg}");
2908        assert!(dbg.contains("drag_over"), "{dbg}");
2909        assert!(!dbg.contains("normal"), "{dbg}");
2910    }
2911
2912    // ---------------------------------------------------------------------
2913    // StyledNodeVec containers
2914    // ---------------------------------------------------------------------
2915
2916    #[test]
2917    fn styled_node_vec_empty_container_is_empty_and_get_returns_none() {
2918        let v: StyledNodeVec = Vec::new().into();
2919        let c = v.as_container();
2920        assert_eq!(c.len(), 0);
2921        assert!(c.is_empty());
2922        assert!(c.get(NodeId::ZERO).is_none());
2923        assert!(c.get(NodeId::new(usize::MAX)).is_none());
2924    }
2925
2926    #[test]
2927    fn styled_node_vec_container_mut_writes_are_visible_through_container() {
2928        let mut v: StyledNodeVec = vec![StyledNode::default(), StyledNode::default()].into();
2929        {
2930            let mut c = v.as_container_mut();
2931            c[NodeId::new(1)].styled_node_state.hover = true;
2932        }
2933        let c = v.as_container();
2934        assert_eq!(c.len(), 2);
2935        assert!(!c[NodeId::ZERO].styled_node_state.hover);
2936        assert!(c[NodeId::new(1)].styled_node_state.hover);
2937        assert!(c.get(NodeId::new(2)).is_none());
2938    }
2939
2940    // ---------------------------------------------------------------------
2941    // Font family hashes
2942    // ---------------------------------------------------------------------
2943
2944    #[test]
2945    fn style_font_family_hash_is_deterministic_and_input_sensitive() {
2946        assert_eq!(
2947            StyleFontFamilyHash::new(&family("Arial")),
2948            StyleFontFamilyHash::new(&family("Arial"))
2949        );
2950        assert_ne!(
2951            StyleFontFamilyHash::new(&family("Arial")),
2952            StyleFontFamilyHash::new(&family("Ariaĺ"))
2953        );
2954        // Same string, different variant → different cache key.
2955        assert_ne!(
2956            StyleFontFamilyHash::new(&StyleFontFamily::System("x".to_string().into())),
2957            StyleFontFamilyHash::new(&StyleFontFamily::File("x".to_string().into()))
2958        );
2959    }
2960
2961    #[test]
2962    fn style_font_family_hash_handles_empty_unicode_and_huge_names() {
2963        let empty = family("");
2964        let unicode = family("🦀 ノート ﷽ عربى");
2965        let huge = family(&"A".repeat(100_000));
2966
2967        // No panic, and each distinct input is stable across calls.
2968        assert_eq!(StyleFontFamilyHash::new(&empty), StyleFontFamilyHash::new(&empty));
2969        assert_eq!(
2970            StyleFontFamilyHash::new(&unicode),
2971            StyleFontFamilyHash::new(&unicode)
2972        );
2973        assert_eq!(StyleFontFamilyHash::new(&huge), StyleFontFamilyHash::new(&huge));
2974        assert_ne!(StyleFontFamilyHash::new(&empty), StyleFontFamilyHash::new(&unicode));
2975        assert_ne!(StyleFontFamilyHash::new(&empty), StyleFontFamilyHash::new(&huge));
2976    }
2977
2978    #[test]
2979    fn style_font_families_hash_empty_slice_is_stable_and_distinct() {
2980        let empty = StyleFontFamiliesHash::new(&[]);
2981        assert_eq!(empty, StyleFontFamiliesHash::new(&[]));
2982        assert_ne!(empty, StyleFontFamiliesHash::new(&[family("")]));
2983    }
2984
2985    #[test]
2986    fn style_font_families_hash_scales_to_large_lists_and_is_length_sensitive() {
2987        let big: Vec<StyleFontFamily> = (0..1000).map(|i| family(&format!("font-{i}"))).collect();
2988        let one_shorter = &big[..999];
2989
2990        assert_eq!(
2991            StyleFontFamiliesHash::new(&big),
2992            StyleFontFamiliesHash::new(&big),
2993            "hashing 1000 families must be deterministic"
2994        );
2995        assert_ne!(
2996            StyleFontFamiliesHash::new(&big),
2997            StyleFontFamiliesHash::new(one_shorter),
2998            "the length prefix must separate [0..1000) from [0..999)"
2999        );
3000    }
3001
3002    // ---------------------------------------------------------------------
3003    // NodeHierarchyItemId: 1-based encode/decode round-trip
3004    // ---------------------------------------------------------------------
3005
3006    #[test]
3007    fn node_hierarchy_item_id_none_is_zero() {
3008        assert_eq!(NodeHierarchyItemId::NONE.into_raw(), 0);
3009        assert_eq!(NodeHierarchyItemId::NONE.into_crate_internal(), None);
3010        assert_eq!(NodeHierarchyItemId::from_crate_internal(None).into_raw(), 0);
3011        assert_eq!(NodeHierarchyItemId::from_raw(0).into_crate_internal(), None);
3012        assert_eq!(NodeHierarchyItemId::from_crate_internal(None), NodeHierarchyItemId::NONE);
3013    }
3014
3015    #[test]
3016    fn node_hierarchy_item_id_encode_decode_roundtrip_at_boundaries() {
3017        // usize::MAX - 1 is the largest index that survives the +1 encoding.
3018        for idx in [0usize, 1, 2, 1023, usize::MAX / 2, usize::MAX - 1] {
3019            let id = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(idx)));
3020            assert_eq!(id.into_raw(), idx + 1, "1-based encoding for {idx}");
3021            assert_eq!(
3022                id.into_crate_internal(),
3023                Some(NodeId::new(idx)),
3024                "decode(encode(x)) == x for {idx}"
3025            );
3026        }
3027    }
3028
3029    #[test]
3030    fn node_hierarchy_item_id_raw_roundtrip_is_identity_even_at_usize_max() {
3031        for raw in [0usize, 1, 2, 7, u32::MAX as usize, usize::MAX] {
3032            let decoded = NodeHierarchyItemId::from_raw(raw).into_crate_internal();
3033            let reencoded = NodeHierarchyItemId::from_crate_internal(decoded).into_raw();
3034            assert_eq!(reencoded, raw, "encode(decode(raw)) must be identity for {raw}");
3035        }
3036    }
3037
3038    #[test]
3039    fn node_hierarchy_item_id_from_raw_decodes_one_based() {
3040        assert_eq!(
3041            NodeHierarchyItemId::from_raw(1).into_crate_internal(),
3042            Some(NodeId::ZERO),
3043            "raw 1 is NodeId(0), NOT NodeId(1)"
3044        );
3045        assert_eq!(
3046            NodeHierarchyItemId::from_raw(usize::MAX).into_crate_internal(),
3047            Some(NodeId::new(usize::MAX - 1))
3048        );
3049    }
3050
3051    #[test]
3052    fn node_hierarchy_item_id_debug_and_display_agree() {
3053        let none = NodeHierarchyItemId::NONE;
3054        assert_eq!(format!("{none:?}"), "None");
3055        assert_eq!(format!("{none}"), format!("{none:?}"));
3056
3057        let some = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(5)));
3058        assert_eq!(format!("{some:?}"), "Some(NodeId(5))");
3059        assert_eq!(format!("{some}"), format!("{some:?}"));
3060
3061        // Extreme value: must not panic and must stay non-empty.
3062        let max = NodeHierarchyItemId::from_raw(usize::MAX);
3063        assert!(!format!("{max:?}").is_empty());
3064    }
3065
3066    #[test]
3067    fn node_hierarchy_item_id_ordering_follows_raw_value() {
3068        let a = NodeHierarchyItemId::from_raw(0);
3069        let b = NodeHierarchyItemId::from_raw(1);
3070        let c = NodeHierarchyItemId::from_raw(usize::MAX);
3071        assert!(a < b);
3072        assert!(b < c);
3073        assert_eq!(a, NodeHierarchyItemId::NONE);
3074    }
3075
3076    #[test]
3077    fn node_hierarchy_item_id_from_impls_match_the_explicit_ones() {
3078        let opt = Some(NodeId::new(41));
3079        let via_from: NodeHierarchyItemId = opt.into();
3080        assert_eq!(via_from, NodeHierarchyItemId::from_crate_internal(opt));
3081
3082        let back: Option<NodeId> = via_from.into();
3083        assert_eq!(back, opt);
3084
3085        let none: NodeHierarchyItemId = None.into();
3086        assert_eq!(none.into_raw(), 0);
3087    }
3088
3089    // ---------------------------------------------------------------------
3090    // NodeHierarchyItem getters
3091    // ---------------------------------------------------------------------
3092
3093    #[test]
3094    fn node_hierarchy_item_zeroed_has_no_links() {
3095        let z = NodeHierarchyItem::zeroed();
3096        assert_eq!(z.parent_id(), None);
3097        assert_eq!(z.previous_sibling_id(), None);
3098        assert_eq!(z.next_sibling_id(), None);
3099        assert_eq!(z.last_child_id(), None);
3100        assert_eq!(z.first_child_id(NodeId::ZERO), None);
3101        assert_eq!(z.first_child_id(NodeId::new(usize::MAX)), None);
3102        assert_eq!(z, NodeHierarchyItem::from(Node::ROOT));
3103    }
3104
3105    #[test]
3106    fn node_hierarchy_item_getters_decode_the_one_based_fields() {
3107        let item = raw_item(1, 2, 3, 4);
3108        assert_eq!(item.parent_id(), Some(NodeId::new(0)));
3109        assert_eq!(item.previous_sibling_id(), Some(NodeId::new(1)));
3110        assert_eq!(item.next_sibling_id(), Some(NodeId::new(2)));
3111        assert_eq!(item.last_child_id(), Some(NodeId::new(3)));
3112
3113        // first_child is derived: parent + 1, but only if the node has children.
3114        assert_eq!(item.first_child_id(NodeId::new(7)), Some(NodeId::new(8)));
3115    }
3116
3117    #[test]
3118    fn node_hierarchy_item_getters_at_usize_max_do_not_overflow() {
3119        let item = raw_item(usize::MAX, usize::MAX, usize::MAX, usize::MAX);
3120        assert_eq!(item.parent_id(), Some(NodeId::new(usize::MAX - 1)));
3121        assert_eq!(item.previous_sibling_id(), Some(NodeId::new(usize::MAX - 1)));
3122        assert_eq!(item.next_sibling_id(), Some(NodeId::new(usize::MAX - 1)));
3123        assert_eq!(item.last_child_id(), Some(NodeId::new(usize::MAX - 1)));
3124
3125        // NodeId's Add is saturating, so `current + 1` clamps instead of wrapping
3126        // to 0 (which would alias the root node).
3127        assert_eq!(
3128            item.first_child_id(NodeId::new(usize::MAX)),
3129            Some(NodeId::new(usize::MAX)),
3130            "first_child_id must saturate, never wrap to NodeId(0)"
3131        );
3132    }
3133
3134    #[test]
3135    fn node_hierarchy_item_from_node_preserves_every_link() {
3136        let node = Node {
3137            parent: Some(NodeId::new(3)),
3138            previous_sibling: None,
3139            next_sibling: Some(NodeId::new(9)),
3140            last_child: Some(NodeId::new(12)),
3141        };
3142        let item: NodeHierarchyItem = node.into();
3143        assert_eq!(item.parent_id(), node.parent);
3144        assert_eq!(item.previous_sibling_id(), node.previous_sibling);
3145        assert_eq!(item.next_sibling_id(), node.next_sibling);
3146        assert_eq!(item.last_child_id(), node.last_child);
3147    }
3148
3149    // ---------------------------------------------------------------------
3150    // NodeHierarchyItemVec container + subtree_len
3151    // ---------------------------------------------------------------------
3152
3153    #[test]
3154    fn node_hierarchy_item_vec_containers_read_and_write() {
3155        let mut v: NodeHierarchyItemVec = vec![NodeHierarchyItem::zeroed(); 2].into();
3156        {
3157            let mut c = v.as_container_mut();
3158            c[NodeId::new(1)].parent = 1; // raw 1 == NodeId(0)
3159        }
3160        let c = v.as_container();
3161        assert_eq!(c.len(), 2);
3162        assert_eq!(c[NodeId::new(1)].parent_id(), Some(NodeId::ZERO));
3163        assert!(c.get(NodeId::new(2)).is_none());
3164
3165        let empty: NodeHierarchyItemVec = Vec::new().into();
3166        assert!(empty.as_container().is_empty());
3167    }
3168
3169    #[test]
3170    fn subtree_len_counts_descendants_of_a_real_tree() {
3171        // body(0) > div(1) > div(2)
3172        let sd = nested_body();
3173        let h = sd.node_hierarchy.as_container();
3174        assert_eq!(h.len(), 3);
3175        assert_eq!(h.subtree_len(NodeId::ZERO), 2, "root has 2 descendants");
3176        assert_eq!(h.subtree_len(NodeId::new(1)), 1);
3177        assert_eq!(h.subtree_len(NodeId::new(2)), 0, "a leaf has no descendants");
3178    }
3179
3180    #[test]
3181    fn subtree_len_saturates_on_a_malformed_backwards_next_sibling() {
3182        // Node 2 claims its next sibling is node 0 — a backwards link a malformed
3183        // FastDom can produce. The subtraction must saturate, not underflow-panic.
3184        let v: NodeHierarchyItemVec = vec![
3185            raw_item(0, 0, 0, 0),
3186            raw_item(0, 0, 0, 0),
3187            raw_item(0, 0, /* next = NodeId(0) */ 1, 0),
3188        ]
3189        .into();
3190        let c = v.as_container();
3191        assert_eq!(c.subtree_len(NodeId::new(2)), 0);
3192
3193        // Self-referential next_sibling (node 1 -> node 1) must also saturate.
3194        let v2: NodeHierarchyItemVec = vec![raw_item(0, 0, 0, 0), raw_item(0, 0, 2, 0)].into();
3195        assert_eq!(v2.as_container().subtree_len(NodeId::new(1)), 0);
3196    }
3197
3198    // ---------------------------------------------------------------------
3199    // StyledDomMemoryReport
3200    // ---------------------------------------------------------------------
3201
3202    #[test]
3203    fn memory_report_default_total_is_zero() {
3204        assert_eq!(StyledDomMemoryReport::default().total_bytes(), 0);
3205    }
3206
3207    #[test]
3208    fn memory_report_total_bytes_sums_every_field() {
3209        let r = StyledDomMemoryReport {
3210            node_count: 3,
3211            node_hierarchy_bytes: 1,
3212            node_data_bytes: 2,
3213            styled_nodes_bytes: 4,
3214            cascade_info_bytes: 8,
3215            tag_ids_bytes: 16,
3216            non_leaf_nodes_bytes: 32,
3217            callback_vecs_bytes: 64,
3218            ..StyledDomMemoryReport::default()
3219        };
3220        assert_eq!(r.total_bytes(), 127, "node_count must NOT be part of the sum");
3221
3222        // A single saturated field must not overflow the running sum.
3223        let extreme = StyledDomMemoryReport {
3224            node_data_bytes: usize::MAX,
3225            ..StyledDomMemoryReport::default()
3226        };
3227        assert_eq!(extreme.total_bytes(), usize::MAX);
3228    }
3229
3230    #[test]
3231    fn memory_report_tracks_node_count_and_is_monotonic_in_dom_size() {
3232        let small = flat_body(1).memory_report();
3233        let large = flat_body(50).memory_report();
3234        assert_eq!(small.node_count, 2);
3235        assert_eq!(large.node_count, 51);
3236        assert!(large.total_bytes() > small.total_bytes());
3237        assert!(small.total_bytes() >= small.node_hierarchy_bytes + small.node_data_bytes);
3238
3239        // Also fine on the smallest possible DOM.
3240        let d = StyledDom::default().memory_report();
3241        assert_eq!(d.node_count, 1);
3242        assert!(d.total_bytes() > 0);
3243    }
3244
3245    // ---------------------------------------------------------------------
3246    // StyledDom construction
3247    // ---------------------------------------------------------------------
3248
3249    #[test]
3250    fn default_styled_dom_is_a_single_rooted_body() {
3251        let sd = StyledDom::default();
3252        assert_eq!(sd.node_count(), 1);
3253        assert_eq!(sd.root.into_crate_internal(), Some(NodeId::ZERO));
3254        assert_eq!(sd.node_hierarchy.as_ref().len(), 1);
3255        assert_eq!(sd.styled_nodes.as_ref().len(), 1);
3256        assert_eq!(sd.cascade_info.as_ref().len(), 1);
3257        assert_eq!(sd.non_leaf_nodes.as_ref().len(), 1);
3258        assert_eq!(sd.non_leaf_nodes.as_ref()[0].depth, 0);
3259        assert!(sd.tag_ids_to_node_ids.as_ref().is_empty());
3260        assert!(sd.get_styled_node_state(&NodeId::ZERO).is_normal());
3261    }
3262
3263    #[test]
3264    fn create_empties_the_source_dom() {
3265        // Documented: "After calling this function, the DOM will be reset to an empty DOM."
3266        let mut dom = Dom::create_body().with_children(vec![Dom::create_div(); 3].into());
3267        let sd = StyledDom::create(&mut dom, Css::empty());
3268        assert_eq!(sd.node_count(), 4);
3269        assert!(
3270            dom.children.as_ref().is_empty(),
3271            "the source Dom must be left empty (it is swapped out, not cloned)"
3272        );
3273    }
3274
3275    #[test]
3276    fn create_keeps_every_parallel_array_the_same_length() {
3277        for n in [0usize, 1, 3, 64] {
3278            let sd = flat_body(n);
3279            let count = sd.node_count();
3280            assert_eq!(count, n + 1);
3281            assert_eq!(sd.node_hierarchy.as_ref().len(), count);
3282            assert_eq!(sd.styled_nodes.as_ref().len(), count);
3283            assert_eq!(sd.cascade_info.as_ref().len(), count);
3284        }
3285    }
3286
3287    #[test]
3288    fn create_survives_malformed_truncated_and_unicode_css() {
3289        let cases: Vec<String> = vec![
3290            String::new(),
3291            "}}}{{{".to_string(),
3292            "div {".to_string(),
3293            "div { color: }".to_string(),
3294            "div { : red; }".to_string(),
3295            "@media".to_string(),
3296            "/* unterminated comment".to_string(),
3297            "div { width: 99999999999999999999999px; }".to_string(),
3298            "div { width: -0px; opacity: 1e400; }".to_string(),
3299            "div { width: NaNpx; height: infpx; }".to_string(),
3300            "* { color: #ZZZZZZ; }".to_string(),
3301            "日本語 { content: \"🦀\"; }".to_string(),
3302            ".\u{202e}rtl { color: red; }".to_string(),
3303            "a".repeat(10_000),
3304            "div { color: red; }".repeat(500),
3305        ];
3306
3307        for case in &cases {
3308            let css = parse_css(case);
3309            let mut dom = Dom::create_body().with_children(vec![Dom::create_div()].into());
3310            let sd = StyledDom::create(&mut dom, css);
3311            assert_eq!(
3312                sd.node_count(),
3313                2,
3314                "CSS must never change the node count; failing input: {case:?}"
3315            );
3316        }
3317    }
3318
3319    #[test]
3320    fn create_handles_deep_and_wide_doms() {
3321        // deep: 64 nested divs under a body
3322        let mut deep = Dom::create_div();
3323        for _ in 0..63 {
3324            deep = Dom::create_div().with_children(vec![deep].into());
3325        }
3326        let mut deep_body = Dom::create_body().with_children(vec![deep].into());
3327        let sd = StyledDom::create(&mut deep_body, Css::empty());
3328        assert_eq!(sd.node_count(), 65);
3329        assert_eq!(
3330            sd.non_leaf_nodes.as_ref().len(),
3331            64,
3332            "every node except the innermost leaf is a parent"
3333        );
3334
3335        // wide: 1000 siblings
3336        let wide = flat_body(1000);
3337        assert_eq!(wide.node_count(), 1001);
3338        assert_eq!(wide.node_hierarchy.as_container().subtree_len(NodeId::ZERO), 1000);
3339        assert_eq!(wide.non_leaf_nodes.as_ref().len(), 1);
3340    }
3341
3342    #[test]
3343    fn create_from_dom_collects_scoped_css_without_changing_the_tree() {
3344        let dom = Dom::create_body().with_children(
3345            vec![
3346                Dom::create_div().with_css("color: red"),
3347                Dom::create_div().with_children(vec![Dom::create_div().with_css("width: 5px")].into()),
3348            ]
3349            .into(),
3350        );
3351        let sd = StyledDom::create_from_dom(dom);
3352        assert_eq!(sd.node_count(), 4);
3353        assert_eq!(sd.node_hierarchy.as_ref().len(), 4);
3354        assert!(sd.get_css_property_cache().compact_cache.is_some());
3355    }
3356
3357    #[test]
3358    fn create_from_dom_on_a_bare_leaf_produces_one_node() {
3359        let sd = StyledDom::create_from_dom(Dom::create_div());
3360        assert_eq!(sd.node_count(), 1);
3361        assert_eq!(sd.root.into_crate_internal(), Some(NodeId::ZERO));
3362    }
3363
3364    // ---------------------------------------------------------------------
3365    // append_child / append_child_with_index / finalize / with_child
3366    // ---------------------------------------------------------------------
3367
3368    #[test]
3369    fn append_child_grows_the_node_count_by_the_child_dom_size() {
3370        let mut base = flat_body(2);
3371        base.append_child(flat_body(3));
3372        assert_eq!(base.node_count(), 3 + 4);
3373        assert_eq!(base.node_hierarchy.as_ref().len(), 7);
3374        assert_eq!(base.styled_nodes.as_ref().len(), 7);
3375        assert_eq!(base.cascade_info.as_ref().len(), 7);
3376    }
3377
3378    #[test]
3379    fn append_child_links_the_new_root_as_the_last_sibling() {
3380        // Flat parent: body(0) > [div(1), div(2)], then append a 1-node StyledDom.
3381        let mut base = flat_body(2);
3382        base.append_child(StyledDom::default());
3383
3384        let h = base.node_hierarchy.as_container();
3385        let children: Vec<NodeId> = NodeId::ZERO.az_children(&h).collect();
3386        assert_eq!(
3387            children,
3388            vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)],
3389            "the appended root must become the last direct child"
3390        );
3391        assert_eq!(h[NodeId::new(3)].parent_id(), Some(NodeId::ZERO));
3392        assert_eq!(h[NodeId::new(3)].previous_sibling_id(), Some(NodeId::new(2)));
3393        assert_eq!(h[NodeId::new(3)].next_sibling_id(), None);
3394    }
3395
3396    /// ADVERSARIAL: `append_child` reads `last_child_id()` to find the current
3397    /// last sibling. If `last_child` names a *descendant* rather than the last
3398    /// *direct child*, the appended root is spliced into the wrong sibling chain
3399    /// and disappears from the root's children.
3400    #[test]
3401    fn append_child_keeps_the_root_children_reachable_for_a_nested_dom() {
3402        let mut base = nested_body(); // body(0) > div(1) > div(2)
3403        base.append_child(StyledDom::default());
3404        assert_eq!(base.node_count(), 4);
3405
3406        let h = base.node_hierarchy.as_container();
3407        let children: Vec<NodeId> = NodeId::ZERO.az_children(&h).collect();
3408        assert_eq!(
3409            children,
3410            vec![NodeId::new(1), NodeId::new(3)],
3411            "after append_child the root must have exactly its old child plus the appended root"
3412        );
3413    }
3414
3415    #[test]
3416    fn append_child_with_index_saturates_the_u32_cascade_index() {
3417        for (child_index, expected) in [
3418            (0usize, 0u32),
3419            (7, 7),
3420            (u32::MAX as usize, u32::MAX),
3421            (u32::MAX as usize + 1, u32::MAX),
3422            (usize::MAX, u32::MAX),
3423        ] {
3424            let mut base = flat_body(0); // single body node
3425            base.append_child_with_index(StyledDom::default(), child_index);
3426
3427            // The appended root lands at index self_len == 1 in the merged arrays.
3428            assert_eq!(
3429                base.cascade_info.as_ref()[1].index_in_parent,
3430                expected,
3431                "child_index {child_index} must saturate to {expected}, never wrap"
3432            );
3433            assert!(base.cascade_info.as_ref()[1].is_last_child);
3434            assert_eq!(base.node_count(), 2);
3435        }
3436    }
3437
3438    #[test]
3439    fn finalize_non_leaf_nodes_sorts_by_depth_and_is_idempotent() {
3440        let mut base = flat_body(1);
3441        base.append_child_with_index(flat_body(2), 1);
3442        base.append_child_with_index(flat_body(2), 2);
3443        base.finalize_non_leaf_nodes();
3444
3445        let depths: Vec<usize> = base.non_leaf_nodes.as_ref().iter().map(|p| p.depth).collect();
3446        let mut sorted = depths.clone();
3447        sorted.sort_unstable();
3448        assert_eq!(depths, sorted, "non_leaf_nodes must be depth-ordered");
3449
3450        base.finalize_non_leaf_nodes();
3451        let again: Vec<usize> = base.non_leaf_nodes.as_ref().iter().map(|p| p.depth).collect();
3452        assert_eq!(depths, again, "finalize must be idempotent");
3453    }
3454
3455    #[test]
3456    fn with_child_matches_append_child() {
3457        let mut appended = flat_body(2);
3458        appended.append_child(flat_body(1));
3459
3460        let built = flat_body(2).with_child(flat_body(1));
3461
3462        assert_eq!(built.node_count(), appended.node_count());
3463        assert_eq!(
3464            built.node_hierarchy.as_ref(),
3465            appended.node_hierarchy.as_ref()
3466        );
3467    }
3468
3469    #[test]
3470    fn swap_with_default_returns_the_old_dom_and_resets_self() {
3471        let mut sd = flat_body(3);
3472        let old = sd.swap_with_default();
3473        assert_eq!(old.node_count(), 4);
3474        assert_eq!(sd.node_count(), 1, "self must be left as the default StyledDom");
3475        assert_eq!(sd.root.into_crate_internal(), Some(NodeId::ZERO));
3476    }
3477
3478    // ---------------------------------------------------------------------
3479    // Menus
3480    // ---------------------------------------------------------------------
3481
3482    #[test]
3483    fn context_menu_and_menu_bar_are_stored_on_the_root_node() {
3484        let mut sd = flat_body(1);
3485        assert!(sd.node_data.as_container()[NodeId::ZERO].get_context_menu().is_none());
3486
3487        sd.set_context_menu(empty_menu());
3488        sd.set_menu_bar(empty_menu());
3489
3490        let data = sd.node_data.as_container();
3491        assert!(data[NodeId::ZERO].get_context_menu().is_some());
3492        assert!(data[NodeId::ZERO].get_menu_bar().is_some());
3493
3494        // ...and the child must not have inherited either of them.
3495        assert!(data[NodeId::new(1)].get_context_menu().is_none());
3496        assert!(data[NodeId::new(1)].get_menu_bar().is_none());
3497    }
3498
3499    #[test]
3500    fn menu_builders_are_equivalent_to_the_setters_and_dont_touch_the_tree() {
3501        let sd = StyledDom::default()
3502            .with_context_menu(empty_menu())
3503            .with_menu_bar(empty_menu());
3504        assert_eq!(sd.node_count(), 1);
3505        let data = sd.node_data.as_container();
3506        assert!(data[NodeId::ZERO].get_context_menu().is_some());
3507        assert!(data[NodeId::ZERO].get_menu_bar().is_some());
3508    }
3509
3510    // ---------------------------------------------------------------------
3511    // restyle_nodes_* / restyle_on_state_change / restyle_user_property
3512    // ---------------------------------------------------------------------
3513
3514    #[test]
3515    fn restyle_nodes_hover_sets_and_clears_the_state_flag() {
3516        let mut sd = flat_body(2);
3517        let _ = sd.restyle_nodes_hover(&[NodeId::new(1)], true);
3518        assert!(sd.get_styled_node_state(&NodeId::new(1)).hover);
3519        assert!(!sd.get_styled_node_state(&NodeId::new(2)).hover);
3520
3521        let _ = sd.restyle_nodes_hover(&[NodeId::new(1)], false);
3522        assert!(!sd.get_styled_node_state(&NodeId::new(1)).hover);
3523        assert!(sd.get_styled_node_state(&NodeId::new(1)).is_normal());
3524    }
3525
3526    #[test]
3527    fn restyle_nodes_active_and_focus_set_independent_flags() {
3528        let mut sd = flat_body(1);
3529        let _ = sd.restyle_nodes_active(&[NodeId::ZERO], true);
3530        let _ = sd.restyle_nodes_focus(&[NodeId::ZERO], true);
3531
3532        let state = sd.get_styled_node_state(&NodeId::ZERO);
3533        assert!(state.active);
3534        assert!(state.focused);
3535        assert!(!state.hover, "hover must be untouched");
3536        assert!(!state.is_normal());
3537    }
3538
3539    #[test]
3540    fn restyle_nodes_ignores_out_of_range_node_ids_instead_of_panicking() {
3541        let mut sd = flat_body(1); // valid ids: 0, 1
3542        let changed = sd.restyle_nodes_hover(&[NodeId::new(2), NodeId::new(usize::MAX)], true);
3543        assert!(changed.is_empty());
3544        assert!(!sd.get_styled_node_state(&NodeId::ZERO).hover);
3545        assert!(!sd.get_styled_node_state(&NodeId::new(1)).hover);
3546
3547        // A mix of valid and stale ids must still apply the valid ones.
3548        let _ = sd.restyle_nodes_hover(&[NodeId::new(1), NodeId::new(999)], true);
3549        assert!(sd.get_styled_node_state(&NodeId::new(1)).hover);
3550    }
3551
3552    #[test]
3553    fn restyle_nodes_handles_empty_and_duplicated_input() {
3554        let mut sd = flat_body(1);
3555        assert!(sd.restyle_nodes_focus(&[], true).is_empty());
3556
3557        // Duplicates must be idempotent, not double-applied or panicking.
3558        let _ = sd.restyle_nodes_focus(&[NodeId::ZERO, NodeId::ZERO, NodeId::ZERO], true);
3559        assert!(sd.get_styled_node_state(&NodeId::ZERO).focused);
3560    }
3561
3562    #[test]
3563    #[should_panic(expected = "index out of bounds")]
3564    fn get_styled_node_state_panics_on_an_out_of_range_node_id() {
3565        // Documents the contract: unlike restyle_nodes_*, this getter does NOT
3566        // bounds-check — callers must pass an id that indexes into this DOM.
3567        let sd = flat_body(1);
3568        let _ = sd.get_styled_node_state(&NodeId::new(99));
3569    }
3570
3571    #[test]
3572    fn restyle_on_state_change_with_no_changes_reports_nothing_to_do() {
3573        let mut sd = flat_body(2);
3574        let r = sd.restyle_on_state_change(None, None, None);
3575        assert!(!r.has_changes());
3576        assert!(!r.needs_layout);
3577        assert!(!r.needs_display_list);
3578        assert!(!r.gpu_only_changes);
3579        assert_eq!(r.max_relayout_scope, RelayoutScope::None);
3580    }
3581
3582    #[test]
3583    fn restyle_on_state_change_tolerates_stale_node_ids() {
3584        let mut sd = flat_body(1);
3585        let r = sd.restyle_on_state_change(
3586            Some(FocusChange {
3587                lost_focus: Some(NodeId::new(500)),
3588                gained_focus: Some(NodeId::new(usize::MAX)),
3589            }),
3590            Some(HoverChange {
3591                left_nodes: vec![NodeId::new(700)],
3592                entered_nodes: vec![NodeId::new(800)],
3593            }),
3594            Some(ActiveChange {
3595                deactivated: vec![NodeId::new(900)],
3596                activated: vec![NodeId::new(1000)],
3597            }),
3598        );
3599        assert!(!r.has_changes(), "stale ids must be filtered, not applied");
3600        assert_eq!(sd.node_count(), 2);
3601    }
3602
3603    #[test]
3604    fn restyle_on_state_change_applies_state_to_valid_nodes() {
3605        let mut sd = flat_body(1);
3606        let r = sd.restyle_on_state_change(
3607            None,
3608            Some(HoverChange {
3609                left_nodes: Vec::new(),
3610                entered_nodes: vec![NodeId::new(1)],
3611            }),
3612            None,
3613        );
3614        assert!(sd.get_styled_node_state(&NodeId::new(1)).hover);
3615        assert!(
3616            r.changed_nodes.keys().all(|n| *n == NodeId::new(1)),
3617            "only the node whose state actually changed may be reported"
3618        );
3619    }
3620
3621    #[test]
3622    fn restyle_user_property_rejects_empty_lists_and_stale_nodes() {
3623        let mut sd = flat_body(1);
3624        assert!(sd.restyle_user_property(&NodeId::ZERO, &[]).is_empty());
3625        assert!(
3626            sd.restyle_user_property(
3627                &NodeId::new(50),
3628                &[CssProperty::auto(CssPropertyType::Width)]
3629            )
3630            .is_empty(),
3631            "an out-of-range node id must be a no-op, not a panic"
3632        );
3633        assert!(
3634            sd.get_css_property_cache()
3635                .user_overridden_properties
3636                .iter()
3637                .all(Vec::is_empty),
3638            "a rejected call must not record an override"
3639        );
3640    }
3641
3642    #[test]
3643    fn restyle_user_property_stores_the_override_and_initial_removes_it() {
3644        let mut sd = flat_body(1);
3645        let node = NodeId::ZERO;
3646
3647        let _ = sd.restyle_user_property(&node, &[CssProperty::auto(CssPropertyType::Width)]);
3648        {
3649            let overrides = &sd.get_css_property_cache().user_overridden_properties;
3650            assert_eq!(overrides.len(), sd.node_count(), "table grows to cover the DOM");
3651            assert_eq!(overrides[0].len(), 1);
3652            assert_eq!(overrides[0][0].0, CssPropertyType::Width);
3653        }
3654
3655        // Re-setting the same type replaces rather than duplicating.
3656        let _ = sd.restyle_user_property(&node, &[CssProperty::none(CssPropertyType::Width)]);
3657        assert_eq!(sd.get_css_property_cache().user_overridden_properties[0].len(), 1);
3658
3659        // CssProperty::Initial removes the override again.
3660        let _ = sd.restyle_user_property(&node, &[CssProperty::initial(CssPropertyType::Width)]);
3661        assert!(sd.get_css_property_cache().user_overridden_properties[0].is_empty());
3662
3663        // Removing a property that was never set must not panic.
3664        let _ = sd.restyle_user_property(&node, &[CssProperty::initial(CssPropertyType::Height)]);
3665        assert!(sd.get_css_property_cache().user_overridden_properties[0].is_empty());
3666    }
3667
3668    #[test]
3669    fn restyle_and_recompute_preserve_the_tree_and_rebuild_the_compact_cache() {
3670        let mut sd = flat_body(3);
3671        let before = sd.node_count();
3672
3673        sd.restyle(parse_css("div { color: red; } body > div:hover { color: blue; }"));
3674        assert_eq!(sd.node_count(), before);
3675        assert!(sd.get_css_property_cache().compact_cache.is_some());
3676
3677        // A second restyle with garbage CSS must not corrupt the structure.
3678        sd.restyle(parse_css("}}} div { : ; }"));
3679        assert_eq!(sd.node_count(), before);
3680
3681        sd.recompute_inheritance_and_compact_cache();
3682        assert_eq!(sd.node_count(), before);
3683        assert!(sd.get_css_property_cache().compact_cache.is_some());
3684    }
3685
3686    #[test]
3687    fn get_css_property_cache_mut_sees_the_same_cache_as_the_shared_getter() {
3688        let mut sd = flat_body(1);
3689        let node_count = sd.node_count();
3690        sd.get_css_property_cache_mut()
3691            .user_overridden_properties
3692            .resize(node_count, Vec::new());
3693        assert_eq!(
3694            sd.get_css_property_cache().user_overridden_properties.len(),
3695            node_count
3696        );
3697    }
3698
3699    // ---------------------------------------------------------------------
3700    // get_html_string
3701    // ---------------------------------------------------------------------
3702
3703    #[test]
3704    fn get_html_string_test_mode_omits_the_html_wrapper() {
3705        let sd = flat_body(2);
3706        let out = sd.get_html_string("HEAD_MARK", "BODY_MARK", true);
3707        assert!(!out.is_empty());
3708        assert!(!out.contains("HEAD_MARK"), "test_mode must not emit the custom head");
3709        assert!(!out.contains("BODY_MARK"), "test_mode must not emit the custom body");
3710        assert!(!out.contains("<html>"));
3711    }
3712
3713    #[test]
3714    fn get_html_string_embeds_custom_head_and_body_verbatim() {
3715        let sd = flat_body(1);
3716        let head = "🦀 <meta charset=\"utf-8\"> & ünïcödé";
3717        let body = "x".repeat(10_000);
3718        let out = sd.get_html_string(head, &body, false);
3719        assert!(out.contains("<html>"));
3720        assert!(out.contains(head));
3721        assert!(out.contains(&body));
3722    }
3723
3724    #[test]
3725    fn get_html_string_does_not_panic_on_extreme_doms() {
3726        // A single-node DOM has no non_leaf parent entry for its root — the depth
3727        // lookup must fall back to 0 rather than panic-indexing the map.
3728        assert!(!StyledDom::default().get_html_string("", "", true).is_empty());
3729        assert!(!flat_body(0).get_html_string("", "", true).is_empty());
3730        assert!(!nested_body().get_html_string("", "", true).is_empty());
3731        assert!(!flat_body(200).get_html_string("", "", true).is_empty());
3732    }
3733
3734    // ---------------------------------------------------------------------
3735    // rendering order
3736    // ---------------------------------------------------------------------
3737
3738    #[test]
3739    fn get_rects_in_rendering_order_is_a_permutation_of_the_children() {
3740        let sd = flat_body(3);
3741        let group = sd.get_rects_in_rendering_order();
3742        assert_eq!(group.root.into_crate_internal(), Some(NodeId::ZERO));
3743
3744        let mut ids: Vec<usize> = group
3745            .children
3746            .as_ref()
3747            .iter()
3748            .filter_map(|c| c.root.into_crate_internal())
3749            .map(|n| n.index())
3750            .collect();
3751        ids.sort_unstable();
3752        assert_eq!(ids, vec![1, 2, 3], "every child appears exactly once");
3753    }
3754
3755    #[test]
3756    fn get_rects_in_rendering_order_nests_grandchildren() {
3757        let sd = nested_body(); // body(0) > div(1) > div(2)
3758        let group = sd.get_rects_in_rendering_order();
3759        assert_eq!(group.children.as_ref().len(), 1);
3760
3761        let child = &group.children.as_ref()[0];
3762        assert_eq!(child.root.into_crate_internal(), Some(NodeId::new(1)));
3763        assert_eq!(child.children.as_ref().len(), 1);
3764        assert_eq!(
3765            child.children.as_ref()[0].root.into_crate_internal(),
3766            Some(NodeId::new(2))
3767        );
3768    }
3769
3770    #[test]
3771    fn determine_rendering_order_with_no_parents_yields_a_childless_root() {
3772        let sd = StyledDom::default();
3773        let hierarchy = sd.node_hierarchy.as_container();
3774        let styled = sd.styled_nodes.as_container();
3775        let data = sd.node_data.as_container();
3776
3777        let group = StyledDom::determine_rendering_order(
3778            &[],
3779            &hierarchy,
3780            &styled,
3781            &data,
3782            sd.get_css_property_cache(),
3783        );
3784        assert_eq!(group.root.into_crate_internal(), Some(NodeId::ZERO));
3785        assert!(group.children.as_ref().is_empty());
3786    }
3787
3788    #[test]
3789    fn sort_children_by_position_returns_every_child_of_a_leaf_free_parent() {
3790        let sd = flat_body(3);
3791        let hierarchy = sd.node_hierarchy.as_container();
3792        let styled = sd.styled_nodes.as_container();
3793        let data = sd.node_data.as_container();
3794
3795        let sorted = sort_children_by_position(
3796            NodeId::ZERO,
3797            &hierarchy,
3798            &styled,
3799            &data,
3800            sd.get_css_property_cache(),
3801        );
3802        assert_eq!(sorted.len(), 3);
3803
3804        // A leaf parent has no children at all.
3805        let leaf = sort_children_by_position(
3806            NodeId::new(3),
3807            &hierarchy,
3808            &styled,
3809            &data,
3810            sd.get_css_property_cache(),
3811        );
3812        assert!(leaf.is_empty());
3813    }
3814
3815    #[test]
3816    fn fill_content_group_children_builds_the_nested_group_tree() {
3817        let id = |i: usize| NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(i)));
3818
3819        let mut sorted: BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>> = BTreeMap::new();
3820        sorted.insert(id(0), vec![id(1), id(2)]);
3821        sorted.insert(id(1), vec![id(3)]);
3822
3823        let mut group = ContentGroup {
3824            root: id(0),
3825            children: Vec::new().into(),
3826        };
3827        fill_content_group_children(&mut group, &sorted);
3828
3829        assert_eq!(group.children.as_ref().len(), 2);
3830        assert_eq!(group.children.as_ref()[0].root, id(1));
3831        assert_eq!(group.children.as_ref()[0].children.as_ref().len(), 1);
3832        assert_eq!(group.children.as_ref()[0].children.as_ref()[0].root, id(3));
3833        assert!(
3834            group.children.as_ref()[1].children.as_ref().is_empty(),
3835            "a node with no entry in the map is a leaf"
3836        );
3837    }
3838
3839    #[test]
3840    fn fill_content_group_children_leaves_an_unknown_root_untouched() {
3841        let sorted: BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>> = BTreeMap::new();
3842        let mut group = ContentGroup {
3843            root: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(9))),
3844            children: Vec::new().into(),
3845        };
3846        fill_content_group_children(&mut group, &sorted);
3847        assert!(group.children.as_ref().is_empty());
3848    }
3849
3850    // ---------------------------------------------------------------------
3851    // recursive_get_last_child / get_path_to_root
3852    // ---------------------------------------------------------------------
3853
3854    #[test]
3855    fn recursive_get_last_child_descends_to_the_deepest_last_child() {
3856        // 0 -> 1 -> 2 (2 is a leaf)
3857        let items = vec![
3858            raw_item(0, 0, 0, 2), // node 0, last_child = NodeId(1)
3859            raw_item(1, 0, 0, 3), // node 1, last_child = NodeId(2)
3860            raw_item(2, 0, 0, 0), // node 2, leaf
3861        ];
3862
3863        let mut target = None;
3864        recursive_get_last_child(NodeId::ZERO, &items, &mut target);
3865        assert_eq!(target, Some(NodeId::new(2)));
3866    }
3867
3868    #[test]
3869    fn recursive_get_last_child_leaves_the_target_untouched_for_a_leaf() {
3870        let items = vec![raw_item(0, 0, 0, 0)];
3871        let mut target = None;
3872        recursive_get_last_child(NodeId::ZERO, &items, &mut target);
3873        assert_eq!(target, None);
3874
3875        // A pre-set target is also left alone.
3876        let mut preset = Some(NodeId::new(7));
3877        recursive_get_last_child(NodeId::ZERO, &items, &mut preset);
3878        assert_eq!(preset, Some(NodeId::new(7)));
3879    }
3880
3881    #[test]
3882    fn get_path_to_root_is_root_first_and_tolerates_unknown_nodes() {
3883        let sd = nested_body(); // body(0) > div(1) > div(2)
3884        let h = sd.node_hierarchy.as_container();
3885
3886        assert_eq!(get_path_to_root(&h, NodeId::ZERO), vec![NodeId::ZERO]);
3887        assert_eq!(
3888            get_path_to_root(&h, NodeId::new(2)),
3889            vec![NodeId::ZERO, NodeId::new(1), NodeId::new(2)]
3890        );
3891
3892        // An id outside the arena yields a one-element path instead of panicking.
3893        assert_eq!(
3894            get_path_to_root(&h, NodeId::new(9999)),
3895            vec![NodeId::new(9999)]
3896        );
3897    }
3898
3899    // ---------------------------------------------------------------------
3900    // document order
3901    // ---------------------------------------------------------------------
3902
3903    #[test]
3904    fn is_before_in_document_order_is_false_for_identical_nodes() {
3905        let sd = flat_body(2);
3906        assert!(!is_before_in_document_order(
3907            &sd.node_hierarchy,
3908            NodeId::new(1),
3909            NodeId::new(1)
3910        ));
3911    }
3912
3913    #[test]
3914    fn is_before_in_document_order_orders_ancestors_and_siblings() {
3915        let sd = flat_body(3); // body(0) > [1, 2, 3]
3916        let h = &sd.node_hierarchy;
3917
3918        assert!(is_before_in_document_order(h, NodeId::ZERO, NodeId::new(1)));
3919        assert!(!is_before_in_document_order(h, NodeId::new(1), NodeId::ZERO));
3920        assert!(is_before_in_document_order(h, NodeId::new(1), NodeId::new(3)));
3921        assert!(!is_before_in_document_order(h, NodeId::new(3), NodeId::new(1)));
3922    }
3923
3924    #[test]
3925    fn is_before_in_document_order_is_antisymmetric_across_a_nested_tree() {
3926        let sd = nested_body();
3927        let h = &sd.node_hierarchy;
3928        for a in 0..3 {
3929            for b in 0..3 {
3930                let ab = is_before_in_document_order(h, NodeId::new(a), NodeId::new(b));
3931                let ba = is_before_in_document_order(h, NodeId::new(b), NodeId::new(a));
3932                if a == b {
3933                    assert!(!ab && !ba, "a node is never before itself");
3934                } else {
3935                    assert_ne!(ab, ba, "exactly one of ({a},{b}) / ({b},{a}) must hold");
3936                }
3937            }
3938        }
3939    }
3940
3941    #[test]
3942    fn is_before_in_document_order_is_deterministic_for_unknown_nodes() {
3943        let sd = flat_body(1);
3944        let h = &sd.node_hierarchy;
3945        // Out-of-range ids fall back to a single-element path; the comparison must
3946        // still terminate and return a stable answer instead of panicking.
3947        assert!(is_before_in_document_order(h, NodeId::ZERO, NodeId::new(usize::MAX)));
3948        assert!(!is_before_in_document_order(h, NodeId::new(usize::MAX), NodeId::ZERO));
3949    }
3950
3951    #[test]
3952    fn collect_nodes_in_document_order_start_equals_end() {
3953        let sd = flat_body(2);
3954        assert_eq!(
3955            collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::new(2), NodeId::new(2)),
3956            vec![NodeId::new(2)]
3957        );
3958        // Even a bogus id short-circuits to itself (documented start == end path).
3959        assert_eq!(
3960            collect_nodes_in_document_order(
3961                &sd.node_hierarchy,
3962                NodeId::new(usize::MAX),
3963                NodeId::new(usize::MAX)
3964            ),
3965            vec![NodeId::new(usize::MAX)]
3966        );
3967    }
3968
3969    #[test]
3970    fn collect_nodes_in_document_order_walks_the_tree_in_pre_order() {
3971        let sd = flat_body(3); // body(0) > [1, 2, 3]
3972        assert_eq!(
3973            collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::ZERO, NodeId::new(3)),
3974            vec![NodeId::ZERO, NodeId::new(1), NodeId::new(2), NodeId::new(3)]
3975        );
3976        assert_eq!(
3977            collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::new(1), NodeId::new(2)),
3978            vec![NodeId::new(1), NodeId::new(2)]
3979        );
3980
3981        // Nested: body(0) > div(1) > div(2) — pre-order is 0, 1, 2.
3982        let nested = nested_body();
3983        assert_eq!(
3984            collect_nodes_in_document_order(&nested.node_hierarchy, NodeId::ZERO, NodeId::new(2)),
3985            vec![NodeId::ZERO, NodeId::new(1), NodeId::new(2)]
3986        );
3987    }
3988
3989    #[test]
3990    fn collect_nodes_in_document_order_terminates_when_end_precedes_start() {
3991        // The traversal hits `end` before it ever enters the range, so it bails
3992        // out with an empty result rather than looping forever.
3993        let sd = flat_body(3);
3994        let out = collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::new(2), NodeId::new(1));
3995        assert!(out.is_empty());
3996    }
3997
3998    #[test]
3999    fn collect_nodes_in_document_order_with_an_unreachable_end_stops_at_the_tree_end() {
4000        let sd = flat_body(3);
4001        let out = collect_nodes_in_document_order(
4002            &sd.node_hierarchy,
4003            NodeId::new(1),
4004            NodeId::new(usize::MAX),
4005        );
4006        assert_eq!(
4007            out,
4008            vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)],
4009            "an end node that is never reached must terminate at the end of the traversal"
4010        );
4011    }
4012
4013    // ---------------------------------------------------------------------
4014    // is_layout_equivalent
4015    // ---------------------------------------------------------------------
4016
4017    #[test]
4018    fn is_layout_equivalent_holds_for_independently_built_identical_doms() {
4019        assert!(is_layout_equivalent(&flat_body(3), &flat_body(3)));
4020        assert!(is_layout_equivalent(
4021            &StyledDom::default(),
4022            &StyledDom::default()
4023        ));
4024        assert!(is_layout_equivalent(&nested_body(), &nested_body()));
4025    }
4026
4027    #[test]
4028    fn is_layout_equivalent_rejects_a_different_node_count() {
4029        assert!(!is_layout_equivalent(&flat_body(3), &flat_body(4)));
4030        assert!(!is_layout_equivalent(&flat_body(0), &flat_body(1)));
4031    }
4032
4033    #[test]
4034    fn is_layout_equivalent_rejects_a_different_structure() {
4035        // Same node count (3), different shape: [body > div > div] vs [body > div, div]
4036        assert!(!is_layout_equivalent(&nested_body(), &flat_body(2)));
4037    }
4038
4039    #[test]
4040    fn is_layout_equivalent_rejects_a_changed_class() {
4041        let build = |class: &str| {
4042            let mut dom = Dom::create_body().with_children(
4043                vec![Dom::create_div().with_class(class.to_string().into())].into(),
4044            );
4045            StyledDom::create(&mut dom, Css::empty())
4046        };
4047        assert!(is_layout_equivalent(&build("a"), &build("a")));
4048        assert!(!is_layout_equivalent(&build("a"), &build("b")));
4049    }
4050
4051    #[test]
4052    fn is_layout_equivalent_rejects_a_changed_pseudo_state() {
4053        let base = flat_body(2);
4054        let mut hovered = flat_body(2);
4055        let _ = hovered.restyle_nodes_hover(&[NodeId::new(1)], true);
4056        assert!(
4057            !is_layout_equivalent(&base, &hovered),
4058            ":hover changes CSS resolution, so the DOMs are not layout-equivalent"
4059        );
4060    }
4061
4062    // ---------------------------------------------------------------------
4063    // CompactDom + convert_dom_into_compact_dom
4064    // ---------------------------------------------------------------------
4065
4066    #[test]
4067    fn compact_dom_len_and_is_empty() {
4068        let single = convert_dom_into_compact_dom(Dom::create_div());
4069        assert_eq!(single.len(), 1);
4070        assert!(!single.is_empty());
4071
4072        let tree = convert_dom_into_compact_dom(
4073            Dom::create_body().with_children(vec![Dom::create_div(); 4].into()),
4074        );
4075        assert_eq!(tree.len(), 5);
4076        assert!(!tree.is_empty());
4077
4078        // A hand-built zero-node arena is the only way to observe is_empty() == true.
4079        let empty = CompactDom {
4080            node_hierarchy: NodeHierarchy {
4081                internal: Vec::new(),
4082            },
4083            node_data: NodeDataContainer {
4084                internal: Vec::new(),
4085            },
4086            root: NodeId::ZERO,
4087        };
4088        assert_eq!(empty.len(), 0);
4089        assert!(empty.is_empty());
4090    }
4091
4092    #[test]
4093    fn convert_dom_into_compact_dom_links_flat_siblings() {
4094        let compact = convert_dom_into_compact_dom(
4095            Dom::create_body().with_children(vec![Dom::create_div(); 3].into()),
4096        );
4097        assert_eq!(compact.len(), 4);
4098        assert_eq!(compact.root, NodeId::ZERO);
4099
4100        let h = compact.node_hierarchy.as_ref();
4101        assert_eq!(h[NodeId::ZERO].parent, None);
4102        assert_eq!(h[NodeId::ZERO].last_child, Some(NodeId::new(3)));
4103
4104        for i in 1..=3usize {
4105            assert_eq!(h[NodeId::new(i)].parent, Some(NodeId::ZERO));
4106            let expected_next = if i == 3 { None } else { Some(NodeId::new(i + 1)) };
4107            assert_eq!(h[NodeId::new(i)].next_sibling, expected_next);
4108            let expected_prev = if i == 1 { None } else { Some(NodeId::new(i - 1)) };
4109            assert_eq!(h[NodeId::new(i)].previous_sibling, expected_prev);
4110            assert_eq!(h[NodeId::new(i)].last_child, None, "the children are leaves");
4111        }
4112    }
4113
4114    /// ADVERSARIAL: `last_child` must name the last DIRECT child — that is the
4115    /// contract `NodeHierarchyItem::last_child_id()` documents, the one
4116    /// `az_reverse_children` walks backwards from, and the one `append_child`
4117    /// splices new siblings onto. The flat encoding computes it as
4118    /// `node_id + estimated_total_children`, which is the last node of the whole
4119    /// SUBTREE — those coincide only when the last direct child is a leaf.
4120    #[test]
4121    fn convert_dom_into_compact_dom_last_child_is_the_last_direct_child() {
4122        // body(0) > div(1) > div(2): the body's only direct child is node 1.
4123        let sd = nested_body();
4124        let h = sd.node_hierarchy.as_container();
4125
4126        let last_direct_child = NodeId::ZERO.az_children(&h).last();
4127        assert_eq!(last_direct_child, Some(NodeId::new(1)));
4128        assert_eq!(
4129            h[NodeId::ZERO].last_child_id(),
4130            last_direct_child,
4131            "last_child_id() must agree with the forward child iteration"
4132        );
4133    }
4134
4135    #[test]
4136    fn convert_dom_into_compact_dom_handles_an_empty_and_a_deep_tree() {
4137        assert_eq!(convert_dom_into_compact_dom(Dom::create_body()).len(), 1);
4138
4139        let mut deep = Dom::create_div();
4140        for _ in 0..64 {
4141            deep = Dom::create_div().with_children(vec![deep].into());
4142        }
4143        let compact = convert_dom_into_compact_dom(deep);
4144        assert_eq!(compact.len(), 65);
4145        // Pre-order ids: every node's parent is the node right before it.
4146        let h = compact.node_hierarchy.as_ref();
4147        for i in 1..65usize {
4148            assert_eq!(h[NodeId::new(i)].parent, Some(NodeId::new(i - 1)));
4149        }
4150    }
4151
4152    // ---------------------------------------------------------------------
4153    // scope_inline_css / collect_css_from_dom / strip_css_from_dom
4154    // ---------------------------------------------------------------------
4155
4156    #[test]
4157    fn scope_inline_css_advances_next_id_once_per_node() {
4158        let mut dom = Dom::create_body().with_children(
4159            vec![
4160                Dom::create_div().with_children(vec![Dom::create_div()].into()),
4161                Dom::create_div(),
4162            ]
4163            .into(),
4164        );
4165        let _ = dom.fixup_children_estimated();
4166
4167        let mut next = 0usize;
4168        scope_inline_css(&mut dom, &mut next);
4169        assert_eq!(next, 4, "4 nodes → the counter must land on 4 (pre-order ids 0..3)");
4170    }
4171
4172    #[test]
4173    fn scope_inline_css_from_zero_and_from_a_large_offset() {
4174        let mut leaf = Dom::create_div();
4175        let _ = leaf.fixup_children_estimated();
4176        let mut next = 0usize;
4177        scope_inline_css(&mut leaf, &mut next);
4178        assert_eq!(next, 1, "a single leaf consumes exactly one id");
4179
4180        // A large (but non-saturating) starting id must not panic or wrap.
4181        let mut dom = Dom::create_body().with_children(vec![Dom::create_div(); 2].into());
4182        let _ = dom.fixup_children_estimated();
4183        let mut big = 1_000_000usize;
4184        scope_inline_css(&mut dom, &mut big);
4185        assert_eq!(big, 1_000_003);
4186    }
4187
4188    #[test]
4189    fn scope_inline_css_preserves_the_rule_count_of_every_node() {
4190        let mut dom = Dom::create_body()
4191            .with_css("color: red")
4192            .with_children(vec![Dom::create_div().with_css("width: 5px")].into());
4193        let _ = dom.fixup_children_estimated();
4194
4195        let rules_before: usize = dom
4196            .css
4197            .as_ref()
4198            .iter()
4199            .map(|c| c.rules.as_ref().len())
4200            .sum::<usize>()
4201            + dom.children.as_ref()[0]
4202                .css
4203                .as_ref()
4204                .iter()
4205                .map(|c| c.rules.as_ref().len())
4206                .sum::<usize>();
4207        assert!(rules_before > 0, "with_css must produce at least one rule");
4208
4209        let mut next = 0usize;
4210        scope_inline_css(&mut dom, &mut next);
4211
4212        let rules_after: usize = dom
4213            .css
4214            .as_ref()
4215            .iter()
4216            .map(|c| c.rules.as_ref().len())
4217            .sum::<usize>()
4218            + dom.children.as_ref()[0]
4219                .css
4220                .as_ref()
4221                .iter()
4222                .map(|c| c.rules.as_ref().len())
4223                .sum::<usize>();
4224        assert_eq!(
4225            rules_before, rules_after,
4226            "scoping rewrites paths in place; it must not add or drop rules"
4227        );
4228        assert_eq!(next, 2);
4229    }
4230
4231    #[test]
4232    fn collect_css_from_dom_yields_inner_css_before_outer_css() {
4233        let outer = parse_css("div { color: red; } span { color: blue; }");
4234        let inner = parse_css("p { color: green; }");
4235        let outer_rules = outer.rules.as_ref().len();
4236        let inner_rules = inner.rules.as_ref().len();
4237        assert_ne!(
4238            outer_rules, inner_rules,
4239            "the two stylesheets must be distinguishable by rule count"
4240        );
4241
4242        let mut child = Dom::create_div();
4243        child.add_component_css(inner);
4244        let mut dom = Dom::create_body().with_children(vec![child].into());
4245        dom.add_component_css(outer);
4246
4247        let mut out = Vec::new();
4248        collect_css_from_dom(&dom, &mut out);
4249
4250        assert_eq!(out.len(), 2);
4251        assert_eq!(
4252            out[0].rules.as_ref().len(),
4253            inner_rules,
4254            "deeper CSS is collected first (lower cascade priority)"
4255        );
4256        assert_eq!(out[1].rules.as_ref().len(), outer_rules);
4257    }
4258
4259    #[test]
4260    fn collect_css_from_dom_on_a_css_free_tree_appends_nothing() {
4261        let dom = Dom::create_body().with_children(vec![Dom::create_div(); 3].into());
4262        let mut out = Vec::new();
4263        collect_css_from_dom(&dom, &mut out);
4264        assert!(out.is_empty());
4265
4266        // ...and an already-populated `out` is appended to, not replaced.
4267        let mut prefilled = vec![Css::empty()];
4268        collect_css_from_dom(&dom, &mut prefilled);
4269        assert_eq!(prefilled.len(), 1);
4270    }
4271
4272    #[test]
4273    fn strip_css_from_dom_clears_every_node_recursively() {
4274        let mut dom = Dom::create_body()
4275            .with_css("color: red")
4276            .with_children(
4277                vec![Dom::create_div()
4278                    .with_css("width: 5px")
4279                    .with_children(vec![Dom::create_div().with_css("height: 5px")].into())]
4280                .into(),
4281            );
4282        assert!(!dom.css.as_ref().is_empty());
4283
4284        strip_css_from_dom(&mut dom);
4285
4286        assert!(dom.css.as_ref().is_empty());
4287        let child = &dom.children.as_ref()[0];
4288        assert!(child.css.as_ref().is_empty());
4289        assert!(child.children.as_ref()[0].css.as_ref().is_empty());
4290
4291        // Idempotent.
4292        strip_css_from_dom(&mut dom);
4293        assert!(dom.css.as_ref().is_empty());
4294    }
4295}