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        // Retain the author stylesheet on the cache (this used to `drop(css)` to
1083        // save ~500 KiB, but that made runtime-inserted nodes unstyleable: the
1084        // rules were gone, so nothing could ever re-run the cascade for them —
1085        // see e2e/bug-inserted-node-no-author-css.json).
1086        css_property_cache.retained_author_css = css;
1087
1088        // Apply UA defaults + compute inherited values so consumers that
1089        // read `css_property_cache.computed_values` (the web/HTML
1090        // renderer in `dll/src/web/html_render.rs`) see resolved
1091        // properties. The compact cache below stores the same info in
1092        // a different layout for the desktop renderer; computed_values
1093        // is the "tall" form that the web renderer's CSS emitter
1094        // (`emit_css_from_cache`) walks per node.
1095        css_property_cache.apply_ua_css(compact_dom.node_data.as_ref().internal);
1096        css_property_cache.compute_inherited_values(
1097            node_hierarchy.as_container().internal,
1098            compact_dom.node_data.as_ref().internal,
1099        );
1100
1101        let prev_font_hashes: Vec<u64> = css_property_cache.compact_cache
1102            .as_ref()
1103            .map(|c| c.prev_font_hashes.clone())
1104            .unwrap_or_default();
1105        let compact = css_property_cache.build_compact_cache_with_inheritance(
1106            compact_dom.node_data.as_ref().internal,
1107            node_hierarchy.as_container().internal,
1108            &prev_font_hashes,
1109        );
1110        css_property_cache.compact_cache = Some(compact);
1111        let pre_prune = if cascade_dbg {
1112            Some(css_property_cache.memory_breakdown())
1113        } else { None };
1114        css_property_cache.prune_compact_normal_props();
1115        if let Some(pre) = pre_prune {
1116            let post = css_property_cache.memory_breakdown();
1117            #[cfg(feature = "std")]
1118            eprintln!("[PRUNE] css_props {} → {} KiB  cascaded {} → {} KiB  (saved {} KiB)",
1119                pre.css_props_bytes / 1024, post.css_props_bytes / 1024,
1120                pre.cascaded_props_bytes / 1024, post.cascaded_props_bytes / 1024,
1121                (pre.total_bytes().saturating_sub(post.total_bytes())) / 1024);
1122            #[cfg(not(feature = "std"))]
1123            let _ = post;
1124        }
1125
1126        let tag_ids = css_property_cache.generate_tag_ids(
1127            &compact_dom.node_data.as_ref(),
1128            &node_hierarchy,
1129        );
1130
1131        if cascade_dbg {
1132            let bd = css_property_cache.memory_breakdown();
1133            #[cfg(feature = "std")]
1134            eprintln!("[CASCADE] {} nodes  cascaded_props={} KiB  css_props={} KiB  compact={} KiB  computed={} KiB  total={} KiB",
1135                node_count,
1136                bd.cascaded_props_bytes / 1024, bd.css_props_bytes / 1024,
1137                bd.compact_cache_bytes / 1024, bd.computed_values_bytes / 1024,
1138                bd.total_bytes() / 1024);
1139            #[cfg(not(feature = "std"))]
1140            let _ = bd;
1141        }
1142
1143        // Collect callback/dataset nodes in a single pass (avoids 3 separate 50K scans).
1144        // For XHTML-parsed DOMs with no callbacks, this early-exits immediately.
1145        let has_any_callbacks = compact_dom.node_data.as_ref().internal.iter()
1146            .any(|c| !c.get_callbacks().is_empty() || c.get_dataset().is_some());
1147
1148        let (nodes_with_window_callbacks, nodes_with_datasets) = if has_any_callbacks {
1149            let mut win_cbs = Vec::new();
1150            let mut datasets = Vec::new();
1151            for (node_id, c) in compact_dom.node_data.as_ref().internal.iter().enumerate() {
1152                let cbs = c.get_callbacks();
1153                let has_dataset = c.get_dataset().is_some();
1154                if !cbs.is_empty() || has_dataset {
1155                    datasets.push(NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_id))));
1156                }
1157                for cb in cbs {
1158                    if let EventFilter::Window(_) = cb.event {
1159                        win_cbs.push(NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_id))));
1160                        break;
1161                    }
1162                }
1163            }
1164            (win_cbs, datasets)
1165        } else {
1166            (Vec::new(), Vec::new())
1167        };
1168        let mut styled_dom = Self {
1169            root: NodeHierarchyItemId::from_crate_internal(Some(compact_dom.root)),
1170            node_hierarchy,
1171            node_data: compact_dom.node_data.internal.into(),
1172            cascade_info: html_tree.internal.into(),
1173            styled_nodes: styled_nodes.into(),
1174            tag_ids_to_node_ids: tag_ids.into(),
1175            nodes_with_window_callbacks: nodes_with_window_callbacks.into(),
1176            nodes_with_datasets: nodes_with_datasets.into(),
1177            non_leaf_nodes,
1178            css_property_cache: CssPropertyCachePtr::new(css_property_cache),
1179            dom_id: DomId::ROOT_ID,
1180        };
1181        #[cfg(feature = "table_layout")]
1182        if let Err(_e) = crate::dom_table::generate_anonymous_table_elements(&mut styled_dom) {
1183        }
1184
1185        styled_dom
1186    }
1187
1188    /// Creates a `StyledDom` from a recursive Dom tree with deferred CSS.
1189    ///
1190    /// This is the Phase 7.2 entry point: the layout callback returns a recursive
1191    /// `Dom` with `css: Vec<Css>` on each node. This function:
1192    ///
1193    /// 1. Collects all CSS objects from the recursive tree
1194    /// 2. Flattens the Dom into contiguous arrays (`CompactDom`)
1195    /// 3. Merges all CSS objects and runs a single cascade pass
1196    /// 4. Runs `apply_ua_css` → `compute_inherited_values` → `build_compact_cache`
1197    /// 5. Generates anonymous table elements
1198    #[must_use] pub fn create_from_dom(mut dom: Dom) -> Self {
1199        use azul_css::css::Css;
1200
1201        // #47: scope each node's inline css to its subtree BEFORE collecting, so a
1202        // non-root node's with_css cannot leak to the whole tree. Uses the same
1203        // pre-order ids the flatten (convert_dom_into_compact_dom) will assign;
1204        // needs estimated_total_children populated first.
1205        dom.fixup_children_estimated();
1206        let mut next_scope_id = 0usize;
1207        scope_inline_css(&mut dom, &mut next_scope_id);
1208
1209        // 1. Collect all CSS objects from the recursive Dom tree (now scoped)
1210        let mut all_css = Vec::new();
1211        collect_css_from_dom(&dom, &mut all_css);
1212
1213        // 2. Merge all CSS objects into one combined Css
1214        let mut combined_css = if all_css.is_empty() {
1215            Css::empty()
1216        } else {
1217            let mut combined_rules: Vec<azul_css::css::CssRuleBlock> = Vec::new();
1218            for css in all_css {
1219                combined_rules.extend(css.rules.into_library_owned_vec());
1220            }
1221            Css::new(combined_rules)
1222        };
1223
1224        // 3. Strip CSS from all Dom nodes before flattening
1225        //    (CSS is already collected, don't need it in the flat tree)
1226        strip_css_from_dom(&mut dom);
1227
1228        // 4. Use existing StyledDom::create to flatten + cascade
1229        Self::create(&mut dom, combined_css)
1230    }
1231
1232    /// Appends another `StyledDom` as a child to the `self.root`
1233    /// without re-styling the DOM itself
1234    pub fn append_child(&mut self, other: Self) {
1235        let self_root_id = self.root.into_crate_internal().unwrap_or(NodeId::ZERO);
1236        let current_root_children_count = self_root_id
1237            .az_children(&self.node_hierarchy.as_container())
1238            .count();
1239        self.append_child_with_index(other, current_root_children_count);
1240        self.finalize_non_leaf_nodes();
1241    }
1242
1243    /// Optimized version of `append_child` that takes the child index directly
1244    /// instead of counting existing children (O(1) instead of O(n))
1245    pub fn append_child_with_index(&mut self, mut other: Self, child_index: usize) {
1246        // shift all the node ids in other by self.len()
1247        let self_len = self.node_hierarchy.as_ref().len();
1248        let other_len = other.node_hierarchy.as_ref().len();
1249        let self_root_id = self.root.into_crate_internal().unwrap_or(NodeId::ZERO);
1250        let other_root_id = other.root.into_crate_internal().unwrap_or(NodeId::ZERO);
1251
1252        // Use provided index instead of counting children
1253        other.cascade_info.as_mut()[other_root_id.index()].index_in_parent =
1254            u32::try_from(child_index).unwrap_or(u32::MAX);
1255        other.cascade_info.as_mut()[other_root_id.index()].is_last_child = true;
1256
1257        self.cascade_info.append(&mut other.cascade_info);
1258
1259        // adjust node hierarchy
1260        for other in other.node_hierarchy.as_mut().iter_mut() {
1261            if other.parent != 0 {
1262                other.parent += self_len;
1263            }
1264            if other.previous_sibling != 0 {
1265                other.previous_sibling += self_len;
1266            }
1267            if other.next_sibling != 0 {
1268                other.next_sibling += self_len;
1269            }
1270            if other.last_child != 0 {
1271                other.last_child += self_len;
1272            }
1273        }
1274
1275        other.node_hierarchy.as_container_mut()[other_root_id].parent =
1276            NodeId::into_raw(&Some(self_root_id));
1277        let current_last_child = self.node_hierarchy.as_container()[self_root_id].last_child_id();
1278        other.node_hierarchy.as_container_mut()[other_root_id].previous_sibling =
1279            NodeId::into_raw(&current_last_child);
1280        if let Some(current_last) = current_last_child {
1281            if self.node_hierarchy.as_container_mut()[current_last]
1282                .next_sibling_id()
1283                .is_some()
1284            {
1285                self.node_hierarchy.as_container_mut()[current_last].next_sibling +=
1286                    other_root_id.index() + other_len;
1287            } else {
1288                self.node_hierarchy.as_container_mut()[current_last].next_sibling =
1289                    NodeId::into_raw(&Some(NodeId::new(self_len + other_root_id.index())));
1290            }
1291        }
1292        self.node_hierarchy.as_container_mut()[self_root_id].last_child =
1293            NodeId::into_raw(&Some(NodeId::new(self_len + other_root_id.index())));
1294
1295        self.node_hierarchy.append(&mut other.node_hierarchy);
1296        self.node_data.append(&mut other.node_data);
1297        self.styled_nodes.append(&mut other.styled_nodes);
1298        self.get_css_property_cache_mut()
1299            .append(other.get_css_property_cache_mut());
1300
1301        // Tag IDs are globally unique (AtomicUsize counter) and never collide,
1302        // so we only shift node_id (which changes when DOMs are merged).
1303        for tag_id_node_id in &mut other.tag_ids_to_node_ids {
1304            tag_id_node_id.node_id.inner += self_len;
1305        }
1306
1307        self.tag_ids_to_node_ids
1308            .append(&mut other.tag_ids_to_node_ids);
1309
1310        for nid in &mut other.nodes_with_window_callbacks {
1311            nid.inner += self_len;
1312        }
1313        self.nodes_with_window_callbacks
1314            .append(&mut other.nodes_with_window_callbacks);
1315
1316        for nid in &mut other.nodes_with_datasets {
1317            nid.inner += self_len;
1318        }
1319        self.nodes_with_datasets
1320            .append(&mut other.nodes_with_datasets);
1321
1322        // edge case: if the other StyledDom consists of only one node
1323        // then it is not a parent itself
1324        if other_len != 1 {
1325            for other_non_leaf_node in &mut other.non_leaf_nodes {
1326                other_non_leaf_node.node_id.inner += self_len;
1327                other_non_leaf_node.depth += 1;
1328            }
1329            self.non_leaf_nodes.append(&mut other.non_leaf_nodes);
1330            // NOTE: Sorting deferred - call finalize_non_leaf_nodes() after all appends
1331        }
1332    }
1333
1334    /// Call this after all `append_child_with_index` operations are complete
1335    /// to sort `non_leaf_nodes` by depth (required for correct rendering)
1336    pub fn finalize_non_leaf_nodes(&mut self) {
1337        self.non_leaf_nodes.sort_by(|a, b| a.depth.cmp(&b.depth));
1338    }
1339
1340    /// Same as `append_child()`, but as a builder method
1341    #[must_use] pub fn with_child(mut self, other: Self) -> Self {
1342        self.append_child(other);
1343        self
1344    }
1345
1346    /// Sets the context menu for the root node
1347    pub fn set_context_menu(&mut self, context_menu: Menu) {
1348        if let Some(root_id) = self.root.into_crate_internal() {
1349            self.node_data.as_container_mut()[root_id].set_context_menu(context_menu);
1350        }
1351    }
1352
1353    /// Builder method for setting the context menu
1354    #[must_use] pub fn with_context_menu(mut self, context_menu: Menu) -> Self {
1355        self.set_context_menu(context_menu);
1356        self
1357    }
1358
1359    /// Sets the menu bar for the root node
1360    pub fn set_menu_bar(&mut self, menu_bar: Menu) {
1361        if let Some(root_id) = self.root.into_crate_internal() {
1362            self.node_data.as_container_mut()[root_id].set_menu_bar(menu_bar);
1363        }
1364    }
1365
1366    /// Builder method for setting the menu bar
1367    #[must_use] pub fn with_menu_bar(mut self, menu_bar: Menu) -> Self {
1368        self.set_menu_bar(menu_bar);
1369        self
1370    }
1371
1372    /// Re-compute inherited CSS values and rebuild the compact layout cache.
1373    ///
1374    /// This MUST be called after `append_child()` merges multiple `StyledDom`s.
1375    /// `append_child()` concatenates the CSS property caches but does NOT
1376    /// re-run inheritance or rebuild the compact cache. This means:
1377    ///
1378    /// 1. **Broken inheritance**: Inherited properties (`color`, `font-size`,
1379    ///    `direction`) from the parent DOM do not flow into appended subtrees.
1380    /// 2. **Stale compact cache**: The child's tier 1/2/2b entries still reflect
1381    ///    the child's isolated cascade, not the composed tree.
1382    ///
1383    /// Calling this method after all `append_child()` calls fixes both issues
1384    /// by re-running a full depth-first inheritance pass and rebuilding the
1385    /// compact cache from scratch on the composed tree.
1386    pub fn recompute_inheritance_and_compact_cache(&mut self) {
1387        // Use the _with_inheritance variant: it does inheritance inline (via
1388        // parent-compact-field copy) AND populates hot_flags via
1389        // apply_css_property_to_compact.  The plain build_compact_cache would
1390        // leave HOT_FLAG_HAS_BACKGROUND / HAS_CLIP_PATH / extra_flags at 0,
1391        // causing renderer negative fast-paths to skip paint (regression
1392        // introduced by ff059052b).  No SIGABRT risk — _with_inheritance
1393        // never pushes to the flat cascaded_props storage.
1394        let prev_font_hashes: Vec<u64> = self.css_property_cache
1395            .downcast_mut()
1396            .compact_cache
1397            .as_ref()
1398            .map(|c| c.prev_font_hashes.clone())
1399            .unwrap_or_default();
1400        let compact = self.css_property_cache
1401            .downcast_mut()
1402            .build_compact_cache_with_inheritance(
1403                self.node_data.as_container().internal,
1404                self.node_hierarchy.as_container().internal,
1405                &prev_font_hashes,
1406            );
1407        self.css_property_cache.downcast_mut().compact_cache = Some(compact);
1408    }
1409
1410    /// Re-applies CSS styles to the existing DOM structure.
1411    /// Grow retained author-CSS subtree scopes to cover a node just appended under
1412    /// `parent`. Mount/`with_css` rules carry a `Root([start, end])` scope
1413    /// (`push_front_scope`) that only matches nodes within a node's ORIGINAL subtree
1414    /// range, so a node appended afterwards falls outside every scope and
1415    /// `restyle_retained` cannot match it. Appending under `parent` (rightmost-spine
1416    /// only, so subtrees stay contiguous in the flat arena) grows `parent`'s and its
1417    /// ancestors' subtrees; bump the inclusive `end` of every scope that already
1418    /// covers `parent` out to the new node.
1419    #[allow(clippy::similar_names)] // new_node/parent and the p/n index locals read clearly in context
1420    pub fn extend_author_scopes_for_appended(&mut self, new_node: NodeId, parent: NodeId) {
1421        use azul_css::css::CssPathSelector;
1422        let p = parent.index();
1423        let n = new_node.index();
1424        let cache = self.css_property_cache.downcast_mut();
1425        for rule in cache.retained_author_css.rules.as_mut() {
1426            let mut sels = rule.path.selectors.as_ref().to_vec();
1427            let mut changed = false;
1428            for sel in &mut sels {
1429                if let CssPathSelector::Root(range) = sel {
1430                    if range.contains(p) && range.end < n {
1431                        range.end = n;
1432                        changed = true;
1433                    }
1434                }
1435            }
1436            if changed {
1437                rule.path.selectors = sels.into();
1438            }
1439        }
1440    }
1441
1442    /// Re-run the author cascade from the stylesheet retained at creation /
1443    /// last `restyle` (`CssPropertyCache::retained_author_css`). Call after a
1444    /// structural DOM mutation (e.g. inserting a node) so new nodes receive
1445    /// author CSS; a no-op when no author stylesheet was ever attached.
1446    pub fn restyle_retained(&mut self) {
1447        let css = self
1448            .css_property_cache
1449            .downcast_mut()
1450            .retained_author_css
1451            .clone();
1452        if css.is_empty() {
1453            return;
1454        }
1455        self.restyle(css);
1456    }
1457
1458    pub fn restyle(&mut self, mut css: Css) {
1459        // NOTE: the tag_ids returned by `cache.restyle` here are generated from
1460        // the STALE `compact_cache` (display/overflow reads) and are intentionally
1461        // discarded — we regenerate them below AFTER the compact cache and
1462        // inheritance have been recomputed (audit styled_dom.rs:1404/1426).
1463        let _stale_tag_ids = self.css_property_cache.downcast_mut().restyle(
1464            &mut css,
1465            &self.node_data.as_container(),
1466            &self.node_hierarchy,
1467            &self.non_leaf_nodes,
1468            &self.cascade_info.as_container(),
1469        );
1470
1471        // Keep the stylesheet for later structural restyles (inserted nodes).
1472        self.css_property_cache.downcast_mut().retained_author_css = css;
1473
1474        // Apply UA CSS properties before computing inheritance
1475        self.css_property_cache
1476            .downcast_mut()
1477            .apply_ua_css(self.node_data.as_container().internal);
1478
1479        // Compute inherited values after restyle and apply_ua_css (resolves em, %, etc.)
1480        self.css_property_cache
1481            .downcast_mut()
1482            .compute_inherited_values(
1483                self.node_hierarchy.as_container().internal,
1484                self.node_data.as_container().internal,
1485            );
1486
1487        // The old compact_cache was built from the pre-restyle CSS. If we do not
1488        // rebuild it, layout-hot properties (display/overflow/background/clip,
1489        // resolved font sizes) keep their stale values and the restyle silently
1490        // no-ops for them. Drop it, rebuild via the _with_inheritance path (which
1491        // repopulates hot_flags), and invalidate the cached resolved font sizes.
1492        let prev_font_hashes: Vec<u64> = self
1493            .css_property_cache
1494            .downcast_mut()
1495            .compact_cache
1496            .as_ref()
1497            .map(|c| c.prev_font_hashes.clone())
1498            .unwrap_or_default();
1499        self.css_property_cache.downcast_mut().compact_cache = None;
1500        let compact = self
1501            .css_property_cache
1502            .downcast_mut()
1503            .build_compact_cache_with_inheritance(
1504                self.node_data.as_container().internal,
1505                self.node_hierarchy.as_container().internal,
1506                &prev_font_hashes,
1507            );
1508        self.css_property_cache.downcast_mut().compact_cache = Some(compact);
1509        self.css_property_cache
1510            .downcast_mut()
1511            .invalidate_resolved_font_sizes();
1512
1513        // Regenerate tag_ids from the freshly rebuilt compact cache so the
1514        // hit-test map reflects the post-restyle display/overflow values.
1515        let new_tag_ids = self.css_property_cache.downcast_mut().generate_tag_ids(
1516            &self.node_data.as_container(),
1517            &self.node_hierarchy,
1518        );
1519        self.tag_ids_to_node_ids = new_tag_ids.into();
1520    }
1521
1522    /// Returns the total number of nodes in this `StyledDom`.
1523    #[inline]
1524    #[must_use] pub const fn node_count(&self) -> usize {
1525        self.node_data.len()
1526    }
1527
1528    /// Returns an immutable reference to the CSS property cache.
1529    #[inline]
1530    #[must_use] pub fn get_css_property_cache(&self) -> &CssPropertyCache {
1531        &self.css_property_cache.ptr
1532    }
1533
1534    /// Returns a mutable reference to the CSS property cache.
1535    #[inline]
1536    pub fn get_css_property_cache_mut(&mut self) -> &mut CssPropertyCache {
1537        &mut self.css_property_cache.ptr
1538    }
1539
1540    /// Returns the current state (hover, active, focus) of a styled node.
1541    #[inline]
1542    #[must_use] pub fn get_styled_node_state(&self, node_id: &NodeId) -> StyledNodeState {
1543        self.styled_nodes.as_container()[*node_id]
1544            .styled_node_state
1545    }
1546
1547    /// Updates hover state for nodes and returns changed CSS properties.
1548    #[must_use]
1549    pub fn restyle_nodes_hover(
1550        &mut self,
1551        nodes: &[NodeId],
1552        new_hover_state: bool,
1553    ) -> RestyleNodes {
1554        self.restyle_nodes_state(
1555            nodes,
1556            new_hover_state,
1557            |state, val| state.hover = val,
1558            azul_css::dynamic_selector::PseudoStateType::Hover,
1559        )
1560    }
1561
1562    /// Updates active state for nodes and returns changed CSS properties.
1563    #[must_use]
1564    pub fn restyle_nodes_active(
1565        &mut self,
1566        nodes: &[NodeId],
1567        new_active_state: bool,
1568    ) -> RestyleNodes {
1569        self.restyle_nodes_state(
1570            nodes,
1571            new_active_state,
1572            |state, val| state.active = val,
1573            azul_css::dynamic_selector::PseudoStateType::Active,
1574        )
1575    }
1576
1577    /// Updates focus state for nodes and returns changed CSS properties.
1578    #[must_use]
1579    pub fn restyle_nodes_focus(
1580        &mut self,
1581        nodes: &[NodeId],
1582        new_focus_state: bool,
1583    ) -> RestyleNodes {
1584        self.restyle_nodes_state(
1585            nodes,
1586            new_focus_state,
1587            |state, val| state.focused = val,
1588            azul_css::dynamic_selector::PseudoStateType::Focus,
1589        )
1590    }
1591
1592    /// Generic restyle method parameterized by the state field and pseudo-state type.
1593    fn restyle_nodes_state(
1594        &mut self,
1595        nodes: &[NodeId],
1596        new_state_value: bool,
1597        set_state: impl Fn(&mut StyledNodeState, bool),
1598        pseudo_state_type: azul_css::dynamic_selector::PseudoStateType,
1599    ) -> RestyleNodes {
1600        // Drop any stale NodeIds that no longer index into this DOM (e.g. left
1601        // over from a previous, larger tree). Indexing styled_nodes / node_data
1602        // with an out-of-range id would panic. Filtering here keeps the
1603        // downstream zip with `old_node_states` aligned.
1604        let node_count = self.node_count();
1605        let nodes: Vec<NodeId> = nodes
1606            .iter()
1607            .copied()
1608            .filter(|nid| nid.index() < node_count)
1609            .collect();
1610
1611        // save the old node state
1612        let old_node_states = nodes
1613            .iter()
1614            .map(|nid| {
1615                self.styled_nodes.as_container()[*nid]
1616                    .styled_node_state
1617            })
1618            .collect::<Vec<_>>();
1619
1620        for nid in &nodes {
1621            set_state(
1622                &mut self.styled_nodes.as_container_mut()[*nid].styled_node_state,
1623                new_state_value,
1624            );
1625        }
1626
1627        let css_property_cache = self.get_css_property_cache();
1628        let styled_nodes = self.styled_nodes.as_container();
1629        let node_data = self.node_data.as_container();
1630
1631        // scan all properties that could have changed because of addition / removal
1632        let v = nodes
1633            .iter()
1634            .zip(old_node_states.iter())
1635            .filter_map(|(node_id, old_node_state)| {
1636                let mut keys_normal: Vec<_> = CssPropertyCache::prop_types_for_state(
1637                    css_property_cache.css_props.get_slice(node_id.index()),
1638                    pseudo_state_type,
1639                ).collect();
1640                let mut keys_inherited: Vec<_> = CssPropertyCache::prop_types_for_state(
1641                    css_property_cache.cascaded_props.get_slice(node_id.index()),
1642                    pseudo_state_type,
1643                ).collect();
1644                let keys_inline: Vec<CssPropertyType> = {
1645                    use azul_css::dynamic_selector::DynamicSelector;
1646                    node_data[*node_id]
1647                        .style
1648                        .iter_inline_properties()
1649                        .filter_map(|(prop, conds)| {
1650                            let matches = conds.as_slice().iter().any(|c| {
1651                                matches!(c, DynamicSelector::PseudoState(pst) if *pst == pseudo_state_type)
1652                            });
1653                            if matches {
1654                                Some(prop.get_type())
1655                            } else {
1656                                None
1657                            }
1658                        })
1659                        .collect()
1660                };
1661                let mut keys_inline_ref: Vec<_> = keys_inline.iter().collect();
1662
1663                keys_normal.append(&mut keys_inherited);
1664                keys_normal.append(&mut keys_inline_ref);
1665
1666                let node_properties_that_could_have_changed = keys_normal;
1667
1668                if node_properties_that_could_have_changed.is_empty() {
1669                    return None;
1670                }
1671
1672                let new_node_state = &styled_nodes[*node_id].styled_node_state;
1673                let node_data = &node_data[*node_id];
1674
1675                let changes = node_properties_that_could_have_changed
1676                    .into_iter()
1677                    .filter_map(|prop| {
1678                        // calculate both the old and the new state
1679                        let old = css_property_cache.get_property_slow(
1680                            node_data,
1681                            node_id,
1682                            old_node_state,
1683                            prop,
1684                        );
1685                        let new = css_property_cache.get_property_slow(
1686                            node_data,
1687                            node_id,
1688                            new_node_state,
1689                            prop,
1690                        );
1691                        if old == new {
1692                            None
1693                        } else {
1694                            Some(ChangedCssProperty {
1695                                previous_state: *old_node_state,
1696                                previous_prop: old.map_or_else(|| CssProperty::auto(*prop), Clone::clone),
1697                                current_state: *new_node_state,
1698                                current_prop: new.map_or_else(|| CssProperty::auto(*prop), Clone::clone),
1699                            })
1700                        }
1701                    })
1702                    .collect::<Vec<_>>();
1703
1704                if changes.is_empty() {
1705                    None
1706                } else {
1707                    Some((*node_id, changes))
1708                }
1709            })
1710            .collect::<Vec<_>>();
1711
1712        v.into_iter().collect()
1713    }
1714
1715    /// Unified entry point for all CSS restyle operations.
1716    ///
1717    /// This function synchronizes the `StyledNodeState` with runtime state
1718    /// and computes which CSS properties have changed. It determines whether
1719    /// layout, display list, or GPU-only updates are needed.
1720    ///
1721    /// # Arguments
1722    /// * `focus_changes` - Nodes gaining/losing focus
1723    /// * `hover_changes` - Nodes gaining/losing hover
1724    /// * `active_changes` - Nodes gaining/losing active (mouse down)
1725    ///
1726    /// # Returns
1727    /// * `RestyleResult` containing changed nodes and what needs updating
1728    #[must_use]
1729    pub fn restyle_on_state_change(
1730        &mut self,
1731        focus_changes: Option<FocusChange>,
1732        hover_changes: Option<HoverChange>,
1733        active_changes: Option<ActiveChange>,
1734    ) -> RestyleResult {
1735        
1736        // Start with GPU-only assumption; refined below as changes are analyzed.
1737        let mut result = RestyleResult {
1738            gpu_only_changes: true,
1739            ..RestyleResult::default()
1740        };
1741
1742        // Helper closure to merge changes and analyze property categories
1743        let mut process_changes = |changes: RestyleNodes| {
1744            for (node_id, props) in changes {
1745                for change in &props {
1746                    let prop_type = change.current_prop.get_type();
1747
1748                    // Use the granular RelayoutScope instead of the binary
1749                    // can_trigger_relayout(). We pass node_is_ifc_member = true
1750                    // conservatively: this means font/text property changes will
1751                    // produce IfcOnly (rather than None). Phase 2c can refine
1752                    // this by checking whether the node actually participates
1753                    // in an IFC.
1754                    let scope = prop_type.relayout_scope(/* node_is_ifc_member */ true);
1755
1756                    // Track the highest scope seen
1757                    if scope > result.max_relayout_scope {
1758                        result.max_relayout_scope = scope;
1759                    }
1760
1761                    // Any scope above None triggers layout
1762                    if scope != RelayoutScope::None {
1763                        result.needs_layout = true;
1764                        result.gpu_only_changes = false;
1765                    }
1766                    
1767                    // Check if this is a GPU-only property
1768                    if !prop_type.is_gpu_only_property() {
1769                        result.gpu_only_changes = false;
1770                    }
1771                    
1772                    // Any visual change needs display list update (unless GPU-only)
1773                    result.needs_display_list = true;
1774                }
1775                
1776                result.changed_nodes.entry(node_id).or_default().extend(props);
1777            }
1778        };
1779
1780        // 1. Process focus changes
1781        if let Some(focus) = focus_changes {
1782            if let Some(old) = focus.lost_focus {
1783                let changes = self.restyle_nodes_focus(&[old], false);
1784                process_changes(changes);
1785            }
1786            if let Some(new) = focus.gained_focus {
1787                let changes = self.restyle_nodes_focus(&[new], true);
1788                process_changes(changes);
1789            }
1790        }
1791
1792        // 2. Process hover changes
1793        if let Some(hover) = hover_changes {
1794            if !hover.left_nodes.is_empty() {
1795                let changes = self.restyle_nodes_hover(&hover.left_nodes, false);
1796                process_changes(changes);
1797            }
1798            if !hover.entered_nodes.is_empty() {
1799                let changes = self.restyle_nodes_hover(&hover.entered_nodes, true);
1800                process_changes(changes);
1801            }
1802        }
1803
1804        // 3. Process active changes
1805        if let Some(active) = active_changes {
1806            if !active.deactivated.is_empty() {
1807                let changes = self.restyle_nodes_active(&active.deactivated, false);
1808                process_changes(changes);
1809            }
1810            if !active.activated.is_empty() {
1811                let changes = self.restyle_nodes_active(&active.activated, true);
1812                process_changes(changes);
1813            }
1814        }
1815
1816        // If no changes, reset display_list flag
1817        if result.changed_nodes.is_empty() {
1818            result.needs_display_list = false;
1819            result.gpu_only_changes = false;
1820        }
1821        
1822        // If layout is needed, display list is also needed
1823        if result.needs_layout {
1824            result.needs_display_list = true;
1825            result.gpu_only_changes = false;
1826        }
1827
1828        result
1829    }
1830
1831    /// Overrides CSS properties for a single node from user code (typically a
1832    /// callback). Writes into `CssPropertyCache::user_overridden_properties`,
1833    /// which `get_property_slow` / `get_property_fast` / `get_computed_value`
1834    /// consult at higher priority than the static CSS cascade — making this
1835    /// the fast path for animating a handful of properties per frame.
1836    ///
1837    /// Passing `CssProperty::Initial` for a property removes any override for
1838    /// that type, restoring the cascaded value. Returns the set of
1839    /// `ChangedCssProperty` entries the caller can feed into the incremental
1840    /// restyle pipeline.
1841    #[must_use]
1842    pub fn restyle_user_property(
1843        &mut self,
1844        node_id: &NodeId,
1845        new_properties: &[CssProperty],
1846    ) -> RestyleNodes {
1847        let mut map = BTreeMap::default();
1848
1849        if new_properties.is_empty() {
1850            return map;
1851        }
1852
1853        let node_count = self.node_data.as_ref().len();
1854        if node_id.index() >= node_count {
1855            return map;
1856        }
1857
1858        let node_data = self.node_data.as_container();
1859        let node_data = &node_data[*node_id];
1860
1861        let node_states = &self.styled_nodes.as_container();
1862        let old_node_state = &node_states[*node_id].styled_node_state;
1863
1864        let changes: Vec<ChangedCssProperty> = {
1865            let css_property_cache = self.get_css_property_cache();
1866
1867            new_properties
1868                .iter()
1869                .filter_map(|new_prop| {
1870                    let old_prop = css_property_cache.get_property_slow(
1871                        node_data,
1872                        node_id,
1873                        old_node_state,
1874                        &new_prop.get_type(),
1875                    );
1876
1877                    let old_prop = old_prop.map_or_else(|| CssProperty::auto(new_prop.get_type()), Clone::clone);
1878
1879                    if old_prop == *new_prop {
1880                        None
1881                    } else {
1882                        Some(ChangedCssProperty {
1883                            previous_state: *old_node_state,
1884                            previous_prop: old_prop,
1885                            // overriding a user property does not change the state
1886                            current_state: *old_node_state,
1887                            current_prop: new_prop.clone(),
1888                        })
1889                    }
1890                })
1891                .collect()
1892        };
1893
1894        let css_property_cache_mut = self.get_css_property_cache_mut();
1895
1896        // user_overridden_properties is built lazily (empty after StyledDom
1897        // construction). Grow to cover this node_id before indexing so the
1898        // override path works on any DOM, not just ones that already have
1899        // overrides from a prior mutation.
1900        if css_property_cache_mut.user_overridden_properties.len() < node_count {
1901            css_property_cache_mut
1902                .user_overridden_properties
1903                .resize(node_count, Vec::new());
1904        }
1905
1906        for new_prop in new_properties {
1907            let prop_type = new_prop.get_type();
1908            let vec = &mut css_property_cache_mut
1909                .user_overridden_properties[node_id.index()];
1910            if new_prop.is_initial() {
1911                // CssProperty::Initial = remove overridden property
1912                if let Ok(idx) = vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
1913                    vec.remove(idx);
1914                }
1915            } else {
1916                match vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
1917                    Ok(idx) => vec[idx].1 = new_prop.clone(),
1918                    Err(idx) => vec.insert(idx, (prop_type, new_prop.clone())),
1919                }
1920            }
1921        }
1922
1923        if !changes.is_empty() {
1924            map.insert(*node_id, changes);
1925        }
1926
1927        map
1928    }
1929
1930    /// Returns a HTML-formatted version of the DOM for easier debugging.
1931    ///
1932    /// For example, a DOM with a parent div containing a child div would return:
1933    ///
1934    /// ```xml,no_run,ignore
1935    /// <div id="hello">
1936    ///      <div id="test" />
1937    /// </div>
1938    /// ```
1939    #[must_use] pub fn get_html_string(&self, custom_head: &str, custom_body: &str, test_mode: bool) -> String {
1940        let css_property_cache = self.get_css_property_cache();
1941
1942        let mut output = String::new();
1943
1944        // After which nodes should a close tag be printed?
1945        let mut should_print_close_tag_after_node: BTreeMap<NodeId, Vec<(NodeId, usize)>> = BTreeMap::new();
1946
1947        let should_print_close_tag_debug = self
1948            .non_leaf_nodes
1949            .iter()
1950            .filter_map(|p| {
1951                let parent_node_id = p.node_id.into_crate_internal()?;
1952                let mut total_last_child = None;
1953                recursive_get_last_child(
1954                    parent_node_id,
1955                    self.node_hierarchy.as_ref(),
1956                    &mut total_last_child,
1957                );
1958                let total_last_child = total_last_child?;
1959                Some((parent_node_id, (total_last_child, p.depth)))
1960            })
1961            .collect::<BTreeMap<_, _>>();
1962
1963        for (parent_id, (last_child, parent_depth)) in should_print_close_tag_debug {
1964            should_print_close_tag_after_node
1965                .entry(last_child)
1966                .or_default()
1967                .push((parent_id, parent_depth));
1968        }
1969
1970        let mut all_node_depths = self
1971            .non_leaf_nodes
1972            .iter()
1973            .filter_map(|p| {
1974                let parent_node_id = p.node_id.into_crate_internal()?;
1975                Some((parent_node_id, p.depth))
1976            })
1977            .collect::<BTreeMap<_, _>>();
1978
1979        for (parent_node_id, parent_depth) in self
1980            .non_leaf_nodes
1981            .iter()
1982            .filter_map(|p| Some((p.node_id.into_crate_internal()?, p.depth)))
1983        {
1984            for child_id in parent_node_id.az_children(&self.node_hierarchy.as_container()) {
1985                all_node_depths.insert(child_id, parent_depth + 1);
1986            }
1987        }
1988
1989        for node_id in self.node_hierarchy.as_container().linear_iter() {
1990            // A single-node DOM (or any node not reached as a non-leaf parent or
1991            // one of their children, e.g. a lone root) has no entry here; treat
1992            // its depth as 0 instead of panic-indexing the map.
1993            let depth = all_node_depths.get(&node_id).copied().unwrap_or(0);
1994
1995            let node_data = &self.node_data.as_container()[node_id];
1996            let node_state = &self.styled_nodes.as_container()[node_id].styled_node_state;
1997            let tabs = String::from("    ").repeat(depth);
1998
1999            output.push_str("\r\n");
2000            output.push_str(&tabs);
2001            output.push_str(&node_data.debug_print_start(css_property_cache, &node_id, node_state));
2002
2003            if let Some(content) = node_data.get_node_type().format().as_ref() {
2004                output.push_str(content);
2005            }
2006
2007            let node_has_children = self.node_hierarchy.as_container()[node_id]
2008                .first_child_id(node_id)
2009                .is_some();
2010            if !node_has_children {
2011                let node_data = &self.node_data.as_container()[node_id];
2012                output.push_str(&node_data.debug_print_end());
2013            }
2014
2015            if let Some(close_tag_vec) = should_print_close_tag_after_node.get(&node_id) {
2016                let mut close_tag_vec = close_tag_vec.clone();
2017                close_tag_vec.sort_by(|a, b| b.1.cmp(&a.1)); // sort by depth descending
2018                for (close_tag_parent_id, close_tag_depth) in close_tag_vec {
2019                    let node_data = &self.node_data.as_container()[close_tag_parent_id];
2020                    let tabs = String::from("    ").repeat(close_tag_depth);
2021                    output.push_str("\r\n");
2022                    output.push_str(&tabs);
2023                    output.push_str(&node_data.debug_print_end());
2024                }
2025            }
2026        }
2027
2028        if test_mode {
2029            output
2030        } else {
2031            format!(
2032                "
2033                <html>
2034                    <head>
2035                    <style>* {{ margin:0px; padding:0px; }}</style>
2036                    {custom_head}
2037                    </head>
2038                {output}
2039                {custom_body}
2040                </html>
2041            "
2042            )
2043        }
2044    }
2045
2046    /// Returns nodes grouped by their rendering order (respects z-index and position).
2047    #[must_use] pub fn get_rects_in_rendering_order(&self) -> ContentGroup {
2048        Self::determine_rendering_order(
2049            self.non_leaf_nodes.as_ref(),
2050            &self.node_hierarchy.as_container(),
2051            &self.styled_nodes.as_container(),
2052            &self.node_data.as_container(),
2053            self.get_css_property_cache(),
2054        )
2055    }
2056
2057    /// Returns the rendering order of the items (the rendering
2058    /// order doesn't have to be the original order)
2059    fn determine_rendering_order(
2060        non_leaf_nodes: &[ParentWithNodeDepth],
2061        node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
2062        styled_nodes: &NodeDataContainerRef<'_, StyledNode>,
2063        node_data_container: &NodeDataContainerRef<'_, NodeData>,
2064        css_property_cache: &CssPropertyCache,
2065    ) -> ContentGroup {
2066        let children_sorted = non_leaf_nodes
2067            .iter()
2068            .filter_map(|parent| {
2069                Some((
2070                    parent.node_id,
2071                    sort_children_by_position(
2072                        parent.node_id.into_crate_internal()?,
2073                        node_hierarchy,
2074                        styled_nodes,
2075                        node_data_container,
2076                        css_property_cache,
2077                    ),
2078                ))
2079            })
2080            .collect::<Vec<_>>();
2081
2082        let children_sorted: BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>> =
2083            children_sorted.into_iter().collect();
2084
2085        let mut root_content_group = ContentGroup {
2086            root: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
2087            children: Vec::new().into(),
2088        };
2089
2090        fill_content_group_children(&mut root_content_group, &children_sorted);
2091
2092        root_content_group
2093    }
2094
2095    /// Replaces this `StyledDom` with default and returns the old value.
2096    #[must_use] pub fn swap_with_default(&mut self) -> Self {
2097        let mut new = Self::default();
2098        core::mem::swap(self, &mut new);
2099        new
2100    }
2101
2102}
2103
2104/// Same as `Dom`, but arena-based for more efficient memory layout and faster traversal.
2105#[derive(Debug, PartialEq, PartialOrd, Eq)]
2106pub struct CompactDom {
2107    /// The arena containing the hierarchical relationships (parent, child, sibling) of all nodes.
2108    pub node_hierarchy: NodeHierarchy,
2109    /// The arena containing the actual data (`NodeData`) for each node.
2110    pub node_data: NodeDataContainer<NodeData>,
2111    /// The ID of the root node of the DOM tree.
2112    pub root: NodeId,
2113}
2114
2115impl CompactDom {
2116    /// Returns the number of nodes in this DOM.
2117    #[inline]
2118    #[must_use] pub fn len(&self) -> usize {
2119        self.node_hierarchy.as_ref().len()
2120    }
2121
2122    /// Returns `true` if this DOM has no nodes.
2123    #[inline]
2124    #[must_use] pub fn is_empty(&self) -> bool {
2125        self.node_hierarchy.as_ref().is_empty()
2126    }
2127}
2128
2129impl From<Dom> for CompactDom {
2130    fn from(dom: Dom) -> Self {
2131        convert_dom_into_compact_dom(dom)
2132    }
2133}
2134
2135/// Converts a tree-based Dom into an arena-based `CompactDom` for efficient traversal.
2136#[must_use] pub fn convert_dom_into_compact_dom(mut dom: Dom) -> CompactDom {
2137    // note: somehow convert this into a non-recursive form later on!
2138    fn convert_dom_into_compact_dom_internal(
2139        dom: &mut Dom,
2140        node_hierarchy: &mut [Node],
2141        node_data: &mut Vec<NodeData>,
2142        parent_node_id: NodeId,
2143        node: Node,
2144        cur_node_id: &mut usize,
2145    ) {
2146        // - parent [0]
2147        //    - child [1]
2148        //    - child [2]
2149        //        - child of child 2 [2]
2150        //        - child of child 2 [4]
2151        //    - child [5]
2152        //    - child [6]
2153        //        - child of child 4 [7]
2154
2155        // Write node into the arena here!
2156        node_hierarchy[parent_node_id.index()] = node;
2157
2158        // MOVE the node's inline `style` AND its `extra` (NodeDataExt) box instead of relying on
2159        // copy_special's `self.style.clone()` / `self.extra.clone()`. Both derived Clones lower to
2160        // indirect-jump jump tables that remill mis-lifts on the web backend: CssProperty's clone
2161        // comes back with discriminant 0 (drops simple inline CSS) and for COMPLEX values (AzButton's
2162        // gradient; the NodeDataExt attributes Vec) the mis-lifted clone reads/writes wrong-sized data,
2163        // which clobbers the adjacent `style` temporary → "memory access out of bounds" later in the
2164        // cascade (StyledDom::create → restyle's inheritance loop reads the corrupted style). 2026-06-02:
2165        // copy_special_moving_complex mem::takes BOTH style+extra before copy_special, so copy_special
2166        // clones an EMPTY style + None extra (no broken clone runs) and restores them after. (Extra was
2167        // added after the AzButton ids/classes node — which lazily allocates NodeDataExt — OOB'd even
2168        // with the style-only take.) The Dom is consumed here, so the move is correct.
2169        let copy = dom.root.copy_special_moving_complex();
2170
2171        node_data[parent_node_id.index()] = copy;
2172
2173        *cur_node_id += 1;
2174
2175        let mut previous_sibling_id = None;
2176        let children_len = dom.children.len();
2177        for (child_index, child_dom) in dom.children.as_mut().iter_mut().enumerate() {
2178            let child_node_id = NodeId::new(*cur_node_id);
2179            let is_last_child = (child_index + 1) == children_len;
2180            let child_dom_is_empty = child_dom.children.is_empty();
2181            let child_node = Node {
2182                parent: Some(parent_node_id),
2183                previous_sibling: previous_sibling_id,
2184                next_sibling: if is_last_child {
2185                    None
2186                } else {
2187                    Some(child_node_id + child_dom.estimated_total_children + 1)
2188                },
2189                last_child: if child_dom_is_empty {
2190                    None
2191                } else {
2192                    Some(child_node_id + child_dom.estimated_total_children)
2193                },
2194            };
2195            previous_sibling_id = Some(child_node_id);
2196            // recurse BEFORE adding the next child
2197            convert_dom_into_compact_dom_internal(
2198                child_dom,
2199                node_hierarchy,
2200                node_data,
2201                child_node_id,
2202                child_node,
2203                cur_node_id,
2204            );
2205        }
2206
2207        // AUTHORITATIVE last_child. The per-child `last_child` set at construction used
2208        // `child_node_id + estimated_total_children`, which is the last node of the
2209        // whole SUBTREE (its deepest descendant), NOT the last DIRECT child — wrong
2210        // whenever that last child has children of its own. It corrupted `last_child_id()`
2211        // and, through it, append_child (which spliced onto the wrong node). The loop
2212        // above already tracked `previous_sibling_id`, which now holds the real last
2213        // direct child (None if there were none), so overwrite with it. This runs for
2214        // every node including the root, so it also corrects the root's own computation.
2215        node_hierarchy[parent_node_id.index()].last_child = previous_sibling_id;
2216    }
2217
2218    // Pre-allocate all nodes (+ 1 root node)
2219    let sum_nodes = dom.fixup_children_estimated();
2220
2221    let mut node_hierarchy = vec![Node::ROOT; sum_nodes + 1];
2222    let mut node_data = vec![NodeData::create_div(); sum_nodes + 1];
2223    let mut cur_node_id = 0;
2224
2225    let root_node_id = NodeId::ZERO;
2226    let root_node = Node {
2227        parent: None,
2228        previous_sibling: None,
2229        next_sibling: None,
2230        last_child: if dom.children.is_empty() {
2231            None
2232        } else {
2233            Some(root_node_id + dom.estimated_total_children)
2234        },
2235    };
2236
2237    convert_dom_into_compact_dom_internal(
2238        &mut dom,
2239        &mut node_hierarchy,
2240        &mut node_data,
2241        root_node_id,
2242        root_node,
2243        &mut cur_node_id,
2244    );
2245
2246    CompactDom {
2247        node_hierarchy: NodeHierarchy {
2248            internal: node_hierarchy,
2249        },
2250        node_data: NodeDataContainer {
2251            internal: node_data,
2252        },
2253        root: root_node_id,
2254    }
2255}
2256
2257/// #47: scope every node's inline css to its own subtree. Walks the tree in the
2258/// SAME pre-order `convert_dom_into_compact_dom` uses to assign flat `NodeIds`, so the
2259/// `[flat_id, flat_id + estimated_total_children]` range pushed onto each rule (via
2260/// `CssPath::push_front_scope`) matches the ids the cascade will later see. After
2261/// this, a node's `with_css`/`set_css` rules can only match nodes inside its subtree
2262/// — they can no longer leak to the whole tree. `fixup_children_estimated()` must
2263/// have run first so `estimated_total_children` is populated/exact.
2264fn scope_inline_css(dom: &mut Dom, next_id: &mut usize) {
2265    let start = *next_id;
2266    let end = start + dom.estimated_total_children;
2267    for css in dom.css.as_mut().iter_mut() {
2268        for rule in css.rules.as_mut().iter_mut() {
2269            // push_front_scope picks node-only ([start,start]) for bare `*` rules
2270            // (with_css inline decls) and subtree ([start,end]) for rules with a real
2271            // selector (add_component_css), so descendant selectors match the subtree.
2272            rule.path.push_front_scope(start, end);
2273        }
2274    }
2275    *next_id += 1;
2276    for child in dom.children.as_mut().iter_mut() {
2277        scope_inline_css(child, next_id);
2278    }
2279}
2280
2281/// Recursively collect all CSS objects from a Dom tree (depth-first).
2282/// Inner (deeper) CSS objects come first, outer (shallower) CSS objects come last.
2283/// This means outer CSS has higher cascade priority when applied in order.
2284fn collect_css_from_dom(dom: &Dom, out: &mut Vec<Css>) {
2285    // First, recurse into children (inner CSS = lower priority)
2286    for child in &dom.children {
2287        collect_css_from_dom(child, out);
2288    }
2289    // Then, add this node's CSS objects (outer CSS = higher priority)
2290    for css in &dom.css {
2291        out.push(css.clone());
2292    }
2293}
2294
2295/// Recursively strip CSS from all Dom nodes (sets css to empty vec).
2296/// Called after collecting CSS so the `CompactDom` doesn't carry CSS data.
2297fn strip_css_from_dom(dom: &mut Dom) {
2298    dom.css = Vec::new().into();
2299    for child in dom.children.as_mut().iter_mut() {
2300        strip_css_from_dom(child);
2301    }
2302}
2303
2304fn fill_content_group_children(
2305    group: &mut ContentGroup,
2306    children_sorted: &BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>>,
2307) {
2308    if let Some(c) = children_sorted.get(&group.root) {
2309        // returns None for leaf nodes
2310        group.children = c
2311            .iter()
2312            .map(|child| ContentGroup {
2313                root: *child,
2314                children: Vec::new().into(),
2315            })
2316            .collect::<Vec<ContentGroup>>()
2317            .into();
2318
2319        for c in group.children.as_mut() {
2320            fill_content_group_children(c, children_sorted);
2321        }
2322    }
2323}
2324
2325fn sort_children_by_position(
2326    parent: NodeId,
2327    node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
2328    rectangles: &NodeDataContainerRef<'_, StyledNode>,
2329    node_data_container: &NodeDataContainerRef<'_, NodeData>,
2330    css_property_cache: &CssPropertyCache,
2331) -> Vec<NodeHierarchyItemId> {
2332    use azul_css::props::layout::LayoutPosition::Absolute;
2333
2334    let children_positions = parent
2335        .az_children(node_hierarchy)
2336        .map(|nid| {
2337            let position = css_property_cache
2338                .get_position(
2339                    &node_data_container[nid],
2340                    &nid,
2341                    &rectangles[nid].styled_node_state,
2342                )
2343                .and_then(|p| (*p).get_property_or_default())
2344                .unwrap_or_default();
2345            let id = NodeHierarchyItemId::from_crate_internal(Some(nid));
2346            (id, position)
2347        })
2348        .collect::<Vec<_>>();
2349
2350    let mut not_absolute_children = children_positions
2351        .iter()
2352        .filter_map(|(node_id, position)| {
2353            if *position == Absolute {
2354                None
2355            } else {
2356                Some(*node_id)
2357            }
2358        })
2359        .collect::<Vec<_>>();
2360
2361    let mut absolute_children = children_positions
2362        .iter()
2363        .filter_map(|(node_id, position)| {
2364            if *position == Absolute {
2365                Some(*node_id)
2366            } else {
2367                None
2368            }
2369        })
2370        .collect::<Vec<_>>();
2371
2372    // Append the position:absolute children after the regular children
2373    not_absolute_children.append(&mut absolute_children);
2374    not_absolute_children
2375}
2376
2377// calls get_last_child() recursively until the last child of the last child of the ... has been
2378// found
2379fn recursive_get_last_child(
2380    node_id: NodeId,
2381    node_hierarchy: &[NodeHierarchyItem],
2382    target: &mut Option<NodeId>,
2383) {
2384    match node_hierarchy[node_id.index()].last_child_id() {
2385        None => (),
2386        Some(s) => {
2387            *target = Some(s);
2388            recursive_get_last_child(s, node_hierarchy, target);
2389        }
2390    }
2391}
2392
2393// ============================================================================
2394// DOM TRAVERSAL FOR MULTI-NODE SELECTION
2395// ============================================================================
2396
2397/// Determine if `node_a` comes before `node_b` in document order.
2398///
2399/// Document order is defined as pre-order depth-first traversal order.
2400/// This is equivalent to the order nodes appear in HTML source.
2401///
2402/// ## Algorithm
2403/// 1. Find the path from root to each node
2404/// 2. Find the Lowest Common Ancestor (LCA)
2405/// 3. At the divergence point, the child that appears first in sibling order comes first
2406#[must_use] pub fn is_before_in_document_order(
2407    hierarchy: &NodeHierarchyItemVec,
2408    node_a: NodeId,
2409    node_b: NodeId,
2410) -> bool {
2411    if node_a == node_b {
2412        return false;
2413    }
2414    
2415    let hierarchy = hierarchy.as_container();
2416    
2417    // Get paths from root to each node (stored as root-first order)
2418    let path_a = get_path_to_root(&hierarchy, node_a);
2419    let path_b = get_path_to_root(&hierarchy, node_b);
2420    
2421    // Find divergence point (last common ancestor)
2422    let min_len = path_a.len().min(path_b.len());
2423    
2424    for i in 0..min_len {
2425        if path_a[i] != path_b[i] {
2426            // Found divergence - check which sibling comes first
2427            let child_towards_a = path_a[i];
2428            let child_towards_b = path_b[i];
2429            
2430            // A smaller NodeId index means it was created earlier in DOM construction,
2431            // which means it comes first in document order for siblings
2432            return child_towards_a.index() < child_towards_b.index();
2433        }
2434    }
2435    
2436    // One path is a prefix of the other - the shorter path (ancestor) comes first
2437    path_a.len() < path_b.len()
2438}
2439
2440/// Get the path from root to a node, returned in root-first order.
2441fn get_path_to_root(
2442    hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
2443    node: NodeId,
2444) -> Vec<NodeId> {
2445    let mut path = Vec::new();
2446    let mut current = Some(node);
2447    
2448    while let Some(node_id) = current {
2449        path.push(node_id);
2450        current = hierarchy.get(node_id).and_then(NodeHierarchyItem::parent_id);
2451    }
2452    
2453    // Reverse to get root-first order
2454    path.reverse();
2455    path
2456}
2457
2458/// Collect all nodes between start and end (inclusive) in document order.
2459///
2460/// This performs a pre-order depth-first traversal starting from the root,
2461/// collecting nodes once we've seen `start` and stopping at `end`.
2462///
2463/// ## Parameters
2464/// * `hierarchy` - The node hierarchy
2465/// * `start_node` - First node in document order
2466/// * `end_node` - Last node in document order
2467///
2468/// ## Returns
2469/// Vector of `NodeIds` in document order, from start to end (inclusive)
2470#[must_use] pub fn collect_nodes_in_document_order(
2471    hierarchy: &NodeHierarchyItemVec,
2472    start_node: NodeId,
2473    end_node: NodeId,
2474) -> Vec<NodeId> {
2475    if start_node == end_node {
2476        return vec![start_node];
2477    }
2478    
2479    let hierarchy_container = hierarchy.as_container();
2480    let hierarchy_slice = hierarchy.as_ref();
2481    
2482    let mut result = Vec::new();
2483    let mut in_range = false;
2484    
2485    // Pre-order DFS using a stack
2486    // We need to traverse in document order, which is pre-order DFS
2487    let mut stack: Vec<NodeId> = vec![NodeId::ZERO]; // Start from root
2488    
2489    while let Some(current) = stack.pop() {
2490        // Check if we've entered the range
2491        if current == start_node {
2492            in_range = true;
2493        }
2494        
2495        // Collect if in range
2496        if in_range {
2497            result.push(current);
2498        }
2499        
2500        // Check if we've exited the range
2501        if current == end_node {
2502            break;
2503        }
2504        
2505        // Push children in reverse order so they pop in correct order
2506        // (first child should be processed first)
2507        if let Some(item) = hierarchy_container.get(current) {
2508            // Get first child
2509            if let Some(first_child) = item.first_child_id(current) {
2510                // Collect all children by following next_sibling
2511                let mut children = Vec::new();
2512                let mut child = Some(first_child);
2513                while let Some(child_id) = child {
2514                    children.push(child_id);
2515                    child = hierarchy_container.get(child_id).and_then(NodeHierarchyItem::next_sibling_id);
2516                }
2517                // Push in reverse order for correct DFS order
2518                for child_id in children.into_iter().rev() {
2519                    stack.push(child_id);
2520                }
2521            }
2522        }
2523    }
2524    
2525    result
2526}
2527
2528/// Check if two `StyledDom`s are structurally equivalent for layout purposes.
2529///
2530/// Returns `true` if the DOMs have the same structure, node types, classes,
2531/// IDs, inline styles, and callback event registrations — meaning the
2532/// layout output would be identical.
2533///
2534/// Image callback nodes are compared by function pointer and `RefAny` type ID
2535/// rather than heap pointer, since each `layout()` call creates new `ImageRef`
2536/// allocations even when the callback is the same.
2537///
2538/// This is used to short-circuit the expensive layout pipeline when the DOM
2539/// hasn't actually changed (e.g., an animation timer fires but only the GL
2540/// texture content changed, not the DOM structure).
2541#[must_use] pub fn is_layout_equivalent(old: &StyledDom, new: &StyledDom) -> bool {
2542    use crate::dom::NodeType;
2543    use crate::resources::DecodedImage;
2544
2545    // Quick check: node count must match
2546    let old_nodes = old.node_data.as_ref();
2547    let new_nodes = new.node_data.as_ref();
2548    if old_nodes.len() != new_nodes.len() {
2549        return false;
2550    }
2551
2552    // Check hierarchy (parent/child/sibling structure)
2553    let old_hier = old.node_hierarchy.as_ref();
2554    let new_hier = new.node_hierarchy.as_ref();
2555    if old_hier.len() != new_hier.len() {
2556        return false;
2557    }
2558    if old_hier != new_hier {
2559        return false;
2560    }
2561
2562    // Per-node comparison
2563    for (old_node, new_node) in old_nodes.iter().zip(new_nodes.iter()) {
2564
2565        // Compare node type discriminant
2566        if core::mem::discriminant(&old_node.node_type)
2567            != core::mem::discriminant(&new_node.node_type)
2568        {
2569            return false;
2570        }
2571
2572        // Compare node type content (with special handling for image callbacks)
2573        match (&old_node.node_type, &new_node.node_type) {
2574            (NodeType::Image(old_img), NodeType::Image(new_img)) => {
2575                match (old_img.get_data(), new_img.get_data()) {
2576                    (DecodedImage::Callback(old_cb), DecodedImage::Callback(new_cb)) => {
2577                        // Compare callback function pointer (stable across frames)
2578                        if old_cb.callback.cb != new_cb.callback.cb {
2579                            return false;
2580                        }
2581                        // Compare RefAny type ID (not instance pointer)
2582                        if old_cb.refany.get_type_id() != new_cb.refany.get_type_id() {
2583                            return false;
2584                        }
2585                    }
2586                    _ => {
2587                        // Raw images / GL textures: compare by pointer identity
2588                        if old_img != new_img {
2589                            return false;
2590                        }
2591                    }
2592                }
2593            }
2594            _ => {
2595                if old_node.node_type != new_node.node_type {
2596                    return false;
2597                }
2598            }
2599        }
2600
2601        // Compare IDs and classes (now stored in attributes as AttributeType::Id/Class)
2602        {
2603            use crate::dom::AttributeType;
2604            let old_ids_classes: Vec<_> = old_node.attributes().as_ref().iter()
2605                .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
2606                .collect();
2607            let new_ids_classes: Vec<_> = new_node.attributes().as_ref().iter()
2608                .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
2609                .collect();
2610            if old_ids_classes != new_ids_classes {
2611                return false;
2612            }
2613        }
2614
2615        // Compare inline CSS (direct layout input)
2616        if old_node.style != new_node.style {
2617            return false;
2618        }
2619
2620        // Compare callback event types (affects hit-test tags)
2621        // We compare only event types, not function pointers or data
2622        let old_cbs = old_node.callbacks.as_ref();
2623        let new_cbs = new_node.callbacks.as_ref();
2624        if old_cbs.len() != new_cbs.len() {
2625            return false;
2626        }
2627        for (old_cb, new_cb) in old_cbs.iter().zip(new_cbs.iter()) {
2628            if old_cb.event != new_cb.event {
2629                return false;
2630            }
2631        }
2632
2633        // Compare attributes (some affect layout, e.g. colspan)
2634        if old_node.attributes().as_ref() != new_node.attributes().as_ref() {
2635            return false;
2636        }
2637    }
2638
2639    // Compare styled node states (hover/focus/active flags affect CSS resolution)
2640    let old_styled = old.styled_nodes.as_ref();
2641    let new_styled = new.styled_nodes.as_ref();
2642    if old_styled.len() != new_styled.len() {
2643        return false;
2644    }
2645    if old_styled != new_styled {
2646        return false;
2647    }
2648
2649    true
2650}
2651
2652#[cfg(test)]
2653mod audit_tests {
2654    use super::*;
2655    use azul_css::props::basic::StyleFontFamily;
2656
2657    fn fam(name: &str) -> StyleFontFamily {
2658        StyleFontFamily::System(name.to_string().into())
2659    }
2660
2661    #[test]
2662    fn style_font_families_hash_is_length_sensitive() {
2663        // The length prefix guarantees that lists of different lengths cannot
2664        // collide, and that hashing is deterministic.
2665        let a = StyleFontFamiliesHash::new(&[fam("Arial")]);
2666        let a2 = StyleFontFamiliesHash::new(&[fam("Arial")]);
2667        assert_eq!(a, a2, "hash must be deterministic");
2668
2669        let two = StyleFontFamiliesHash::new(&[fam("Arial"), fam("Helvetica")]);
2670        assert_ne!(a, two, "different-length family lists must not collide");
2671
2672        let empty = StyleFontFamiliesHash::new(&[]);
2673        assert_ne!(empty, a);
2674        assert_ne!(empty, two);
2675
2676        // Order still matters.
2677        let rev = StyleFontFamiliesHash::new(&[fam("Helvetica"), fam("Arial")]);
2678        assert_ne!(two, rev);
2679    }
2680}
2681
2682#[cfg(test)]
2683#[allow(clippy::too_many_lines)]
2684mod autotest_generated {
2685    use azul_css::{
2686        dynamic_selector::PseudoStateFlags,
2687        props::basic::StyleFontFamily,
2688    };
2689
2690    use super::*;
2691
2692    // ---------------------------------------------------------------------
2693    // helpers
2694    // ---------------------------------------------------------------------
2695
2696    /// Builds a `NodeHierarchyItem` directly from the RAW (1-based) encoding:
2697    /// `0` = none, `n` = `NodeId(n - 1)`.
2698    const fn raw_item(parent: usize, prev: usize, next: usize, last: usize) -> NodeHierarchyItem {
2699        NodeHierarchyItem {
2700            parent,
2701            previous_sibling: prev,
2702            next_sibling: next,
2703            last_child: last,
2704        }
2705    }
2706
2707    /// `<body>` with `n` leaf `<div>` children, cascaded against an empty stylesheet.
2708    /// Node ids are `0 = body`, `1..=n` = the children.
2709    fn flat_body(n: usize) -> StyledDom {
2710        let children: Vec<Dom> = (0..n).map(|_| Dom::create_div()).collect();
2711        let mut dom = Dom::create_body().with_children(children.into());
2712        StyledDom::create(&mut dom, Css::empty())
2713    }
2714
2715    /// `<body> > <div> > <div>` — the last direct child of the root is itself a parent.
2716    fn nested_body() -> StyledDom {
2717        let mut dom = Dom::create_body().with_children(
2718            vec![Dom::create_div().with_children(vec![Dom::create_div()].into())].into(),
2719        );
2720        StyledDom::create(&mut dom, Css::empty())
2721    }
2722
2723    fn parse_css(s: &str) -> Css {
2724        azul_css::parser2::new_from_str(s).0
2725    }
2726
2727    fn family(name: &str) -> StyleFontFamily {
2728        StyleFontFamily::System(name.to_string().into())
2729    }
2730
2731    const fn pseudo_flags(all: bool) -> PseudoStateFlags {
2732        PseudoStateFlags {
2733            hover: all,
2734            active: all,
2735            focused: all,
2736            disabled: all,
2737            checked: all,
2738            focus_within: all,
2739            visited: all,
2740            backdrop: all,
2741            dragging: all,
2742            drag_over: all,
2743        }
2744    }
2745
2746    fn empty_menu() -> Menu {
2747        let items: Vec<crate::menu::MenuItem> = Vec::new();
2748        Menu::create(items.into())
2749    }
2750
2751    // ---------------------------------------------------------------------
2752    // RestyleResult (predicate + merge)
2753    // ---------------------------------------------------------------------
2754
2755    #[test]
2756    fn restyle_result_default_reports_no_changes() {
2757        let r = RestyleResult::default();
2758        assert!(!r.has_changes());
2759        assert!(!r.needs_layout);
2760        assert!(!r.needs_display_list);
2761        assert!(!r.gpu_only_changes);
2762        assert_eq!(r.max_relayout_scope, RelayoutScope::None);
2763    }
2764
2765    #[test]
2766    fn restyle_result_has_changes_keys_off_node_map_not_property_count() {
2767        // A node entry with an EMPTY change list still counts as "changed":
2768        // has_changes() only looks at the node map, never at the inner Vec.
2769        let mut r = RestyleResult::default();
2770        r.changed_nodes.insert(NodeId::ZERO, Vec::new());
2771        assert!(r.has_changes());
2772
2773        r.changed_nodes.clear();
2774        assert!(!r.has_changes());
2775    }
2776
2777    #[test]
2778    fn restyle_result_merge_ors_layout_flags_and_ands_gpu_only() {
2779        let mut a = RestyleResult {
2780            needs_layout: false,
2781            needs_display_list: false,
2782            gpu_only_changes: true,
2783            ..RestyleResult::default()
2784        };
2785        let b = RestyleResult {
2786            needs_layout: true,
2787            needs_display_list: true,
2788            gpu_only_changes: true,
2789            ..RestyleResult::default()
2790        };
2791        a.merge(b);
2792        assert!(a.needs_layout, "needs_layout is OR-ed");
2793        assert!(a.needs_display_list, "needs_display_list is OR-ed");
2794        assert!(a.gpu_only_changes, "true && true stays true");
2795
2796        // ...and a single non-GPU-only participant clears the flag.
2797        let mut c = RestyleResult {
2798            gpu_only_changes: true,
2799            ..RestyleResult::default()
2800        };
2801        c.merge(RestyleResult {
2802            gpu_only_changes: false,
2803            ..RestyleResult::default()
2804        });
2805        assert!(!c.gpu_only_changes, "gpu_only_changes is AND-ed");
2806    }
2807
2808    #[test]
2809    fn restyle_result_merge_keeps_the_most_expensive_scope() {
2810        let mut low = RestyleResult {
2811            max_relayout_scope: RelayoutScope::None,
2812            ..RestyleResult::default()
2813        };
2814        low.merge(RestyleResult {
2815            max_relayout_scope: RelayoutScope::Full,
2816            ..RestyleResult::default()
2817        });
2818        assert_eq!(low.max_relayout_scope, RelayoutScope::Full);
2819
2820        // ...and merging a cheaper scope must NOT downgrade it.
2821        let mut high = RestyleResult {
2822            max_relayout_scope: RelayoutScope::Full,
2823            ..RestyleResult::default()
2824        };
2825        high.merge(RestyleResult {
2826            max_relayout_scope: RelayoutScope::IfcOnly,
2827            ..RestyleResult::default()
2828        });
2829        assert_eq!(high.max_relayout_scope, RelayoutScope::Full);
2830    }
2831
2832    #[test]
2833    fn restyle_result_merge_of_default_is_not_the_identity_for_gpu_only() {
2834        // `RestyleResult::default()` has gpu_only_changes == false, and merge()
2835        // AND-s that flag — so merging an EMPTY result still clears it. Pinned
2836        // here because it is a genuine footgun for callers that merge in a loop.
2837        let mut a = RestyleResult {
2838            gpu_only_changes: true,
2839            ..RestyleResult::default()
2840        };
2841        a.merge(RestyleResult::default());
2842        assert!(!a.gpu_only_changes);
2843        assert!(!a.has_changes());
2844    }
2845
2846    #[test]
2847    fn restyle_result_merge_concatenates_changes_for_the_same_node() {
2848        let prop = |t| ChangedCssProperty {
2849            previous_state: StyledNodeState::new(),
2850            previous_prop: CssProperty::auto(t),
2851            current_state: StyledNodeState::new(),
2852            current_prop: CssProperty::initial(t),
2853        };
2854
2855        let mut a = RestyleResult::default();
2856        a.changed_nodes
2857            .insert(NodeId::ZERO, vec![prop(CssPropertyType::Width)]);
2858
2859        let mut b = RestyleResult::default();
2860        b.changed_nodes
2861            .insert(NodeId::ZERO, vec![prop(CssPropertyType::Height)]);
2862        b.changed_nodes
2863            .insert(NodeId::new(1), vec![prop(CssPropertyType::Opacity)]);
2864
2865        a.merge(b);
2866
2867        assert_eq!(a.changed_nodes.len(), 2);
2868        assert_eq!(
2869            a.changed_nodes[&NodeId::ZERO].len(),
2870            2,
2871            "changes for the same node are appended, not replaced"
2872        );
2873        assert_eq!(a.changed_nodes[&NodeId::new(1)].len(), 1);
2874        assert!(a.has_changes());
2875    }
2876
2877    // ---------------------------------------------------------------------
2878    // StyledNodeState (constructor + predicates)
2879    // ---------------------------------------------------------------------
2880
2881    #[test]
2882    fn styled_node_state_new_is_all_false_and_normal() {
2883        let s = StyledNodeState::new();
2884        assert!(s.is_normal());
2885        assert!(!s.hover);
2886        assert!(!s.active);
2887        assert!(!s.focused);
2888        assert!(!s.disabled);
2889        assert!(!s.checked);
2890        assert!(!s.focus_within);
2891        assert!(!s.visited);
2892        assert!(!s.backdrop);
2893        assert!(!s.dragging);
2894        assert!(!s.drag_over);
2895        assert_eq!(s, StyledNodeState::default());
2896    }
2897
2898    #[test]
2899    fn styled_node_state_has_state_zero_is_always_true() {
2900        // 0 == "Normal", which is active regardless of the other flags.
2901        assert!(StyledNodeState::new().has_state(0));
2902        assert!(StyledNodeState::from_pseudo_state_flags(&pseudo_flags(true)).has_state(0));
2903    }
2904
2905    #[test]
2906    fn styled_node_state_has_state_maps_every_index_exactly_once() {
2907        // Each setter must light up exactly one state index in 1..=10.
2908        let setters: [(u8, fn(&mut StyledNodeState)); 10] = [
2909            (1, |s| s.hover = true),
2910            (2, |s| s.active = true),
2911            (3, |s| s.focused = true),
2912            (4, |s| s.disabled = true),
2913            (5, |s| s.checked = true),
2914            (6, |s| s.focus_within = true),
2915            (7, |s| s.visited = true),
2916            (8, |s| s.backdrop = true),
2917            (9, |s| s.dragging = true),
2918            (10, |s| s.drag_over = true),
2919        ];
2920
2921        for (expected_idx, set) in setters {
2922            let mut s = StyledNodeState::new();
2923            set(&mut s);
2924            assert!(!s.is_normal(), "state {expected_idx} must not be 'normal'");
2925            for idx in 1..=10u8 {
2926                assert_eq!(
2927                    s.has_state(idx),
2928                    idx == expected_idx,
2929                    "state index {idx} misreported for setter {expected_idx}"
2930                );
2931            }
2932        }
2933    }
2934
2935    #[test]
2936    fn styled_node_state_has_state_is_false_for_every_out_of_range_u8() {
2937        let all_on = StyledNodeState::from_pseudo_state_flags(&pseudo_flags(true));
2938        for idx in 11..=u8::MAX {
2939            assert!(!StyledNodeState::new().has_state(idx));
2940            assert!(
2941                !all_on.has_state(idx),
2942                "unknown state index {idx} must be inactive even when every flag is set"
2943            );
2944        }
2945    }
2946
2947    #[test]
2948    fn styled_node_state_from_pseudo_state_flags_roundtrips_every_field() {
2949        let all_on = StyledNodeState::from_pseudo_state_flags(&pseudo_flags(true));
2950        assert!(!all_on.is_normal());
2951        for idx in 0..=10u8 {
2952            assert!(all_on.has_state(idx), "state {idx} should be active");
2953        }
2954
2955        let all_off = StyledNodeState::from_pseudo_state_flags(&pseudo_flags(false));
2956        assert!(all_off.is_normal());
2957        assert_eq!(all_off, StyledNodeState::new());
2958    }
2959
2960    #[test]
2961    fn styled_node_state_debug_lists_active_states_and_normal_when_empty() {
2962        assert_eq!(format!("{:?}", StyledNodeState::new()), "[\"normal\"]");
2963
2964        let mut s = StyledNodeState::new();
2965        s.hover = true;
2966        s.drag_over = true;
2967        let dbg = format!("{s:?}");
2968        assert!(dbg.contains("hover"), "{dbg}");
2969        assert!(dbg.contains("drag_over"), "{dbg}");
2970        assert!(!dbg.contains("normal"), "{dbg}");
2971    }
2972
2973    // ---------------------------------------------------------------------
2974    // StyledNodeVec containers
2975    // ---------------------------------------------------------------------
2976
2977    #[test]
2978    fn styled_node_vec_empty_container_is_empty_and_get_returns_none() {
2979        let v: StyledNodeVec = Vec::new().into();
2980        let c = v.as_container();
2981        assert_eq!(c.len(), 0);
2982        assert!(c.is_empty());
2983        assert!(c.get(NodeId::ZERO).is_none());
2984        assert!(c.get(NodeId::new(usize::MAX)).is_none());
2985    }
2986
2987    #[test]
2988    fn styled_node_vec_container_mut_writes_are_visible_through_container() {
2989        let mut v: StyledNodeVec = vec![StyledNode::default(), StyledNode::default()].into();
2990        {
2991            let mut c = v.as_container_mut();
2992            c[NodeId::new(1)].styled_node_state.hover = true;
2993        }
2994        let c = v.as_container();
2995        assert_eq!(c.len(), 2);
2996        assert!(!c[NodeId::ZERO].styled_node_state.hover);
2997        assert!(c[NodeId::new(1)].styled_node_state.hover);
2998        assert!(c.get(NodeId::new(2)).is_none());
2999    }
3000
3001    // ---------------------------------------------------------------------
3002    // Font family hashes
3003    // ---------------------------------------------------------------------
3004
3005    #[test]
3006    fn style_font_family_hash_is_deterministic_and_input_sensitive() {
3007        assert_eq!(
3008            StyleFontFamilyHash::new(&family("Arial")),
3009            StyleFontFamilyHash::new(&family("Arial"))
3010        );
3011        assert_ne!(
3012            StyleFontFamilyHash::new(&family("Arial")),
3013            StyleFontFamilyHash::new(&family("Ariaĺ"))
3014        );
3015        // Same string, different variant → different cache key.
3016        assert_ne!(
3017            StyleFontFamilyHash::new(&StyleFontFamily::System("x".to_string().into())),
3018            StyleFontFamilyHash::new(&StyleFontFamily::File("x".to_string().into()))
3019        );
3020    }
3021
3022    #[test]
3023    fn style_font_family_hash_handles_empty_unicode_and_huge_names() {
3024        let empty = family("");
3025        let unicode = family("🦀 ノート ﷽ عربى");
3026        let huge = family(&"A".repeat(100_000));
3027
3028        // No panic, and each distinct input is stable across calls.
3029        assert_eq!(StyleFontFamilyHash::new(&empty), StyleFontFamilyHash::new(&empty));
3030        assert_eq!(
3031            StyleFontFamilyHash::new(&unicode),
3032            StyleFontFamilyHash::new(&unicode)
3033        );
3034        assert_eq!(StyleFontFamilyHash::new(&huge), StyleFontFamilyHash::new(&huge));
3035        assert_ne!(StyleFontFamilyHash::new(&empty), StyleFontFamilyHash::new(&unicode));
3036        assert_ne!(StyleFontFamilyHash::new(&empty), StyleFontFamilyHash::new(&huge));
3037    }
3038
3039    #[test]
3040    fn style_font_families_hash_empty_slice_is_stable_and_distinct() {
3041        let empty = StyleFontFamiliesHash::new(&[]);
3042        assert_eq!(empty, StyleFontFamiliesHash::new(&[]));
3043        assert_ne!(empty, StyleFontFamiliesHash::new(&[family("")]));
3044    }
3045
3046    #[test]
3047    fn style_font_families_hash_scales_to_large_lists_and_is_length_sensitive() {
3048        let big: Vec<StyleFontFamily> = (0..1000).map(|i| family(&format!("font-{i}"))).collect();
3049        let one_shorter = &big[..999];
3050
3051        assert_eq!(
3052            StyleFontFamiliesHash::new(&big),
3053            StyleFontFamiliesHash::new(&big),
3054            "hashing 1000 families must be deterministic"
3055        );
3056        assert_ne!(
3057            StyleFontFamiliesHash::new(&big),
3058            StyleFontFamiliesHash::new(one_shorter),
3059            "the length prefix must separate [0..1000) from [0..999)"
3060        );
3061    }
3062
3063    // ---------------------------------------------------------------------
3064    // NodeHierarchyItemId: 1-based encode/decode round-trip
3065    // ---------------------------------------------------------------------
3066
3067    #[test]
3068    fn node_hierarchy_item_id_none_is_zero() {
3069        assert_eq!(NodeHierarchyItemId::NONE.into_raw(), 0);
3070        assert_eq!(NodeHierarchyItemId::NONE.into_crate_internal(), None);
3071        assert_eq!(NodeHierarchyItemId::from_crate_internal(None).into_raw(), 0);
3072        assert_eq!(NodeHierarchyItemId::from_raw(0).into_crate_internal(), None);
3073        assert_eq!(NodeHierarchyItemId::from_crate_internal(None), NodeHierarchyItemId::NONE);
3074    }
3075
3076    #[test]
3077    fn node_hierarchy_item_id_encode_decode_roundtrip_at_boundaries() {
3078        // usize::MAX - 1 is the largest index that survives the +1 encoding.
3079        for idx in [0usize, 1, 2, 1023, usize::MAX / 2, usize::MAX - 1] {
3080            let id = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(idx)));
3081            assert_eq!(id.into_raw(), idx + 1, "1-based encoding for {idx}");
3082            assert_eq!(
3083                id.into_crate_internal(),
3084                Some(NodeId::new(idx)),
3085                "decode(encode(x)) == x for {idx}"
3086            );
3087        }
3088    }
3089
3090    #[test]
3091    fn node_hierarchy_item_id_raw_roundtrip_is_identity_even_at_usize_max() {
3092        for raw in [0usize, 1, 2, 7, u32::MAX as usize, usize::MAX] {
3093            let decoded = NodeHierarchyItemId::from_raw(raw).into_crate_internal();
3094            let reencoded = NodeHierarchyItemId::from_crate_internal(decoded).into_raw();
3095            assert_eq!(reencoded, raw, "encode(decode(raw)) must be identity for {raw}");
3096        }
3097    }
3098
3099    #[test]
3100    fn node_hierarchy_item_id_from_raw_decodes_one_based() {
3101        assert_eq!(
3102            NodeHierarchyItemId::from_raw(1).into_crate_internal(),
3103            Some(NodeId::ZERO),
3104            "raw 1 is NodeId(0), NOT NodeId(1)"
3105        );
3106        assert_eq!(
3107            NodeHierarchyItemId::from_raw(usize::MAX).into_crate_internal(),
3108            Some(NodeId::new(usize::MAX - 1))
3109        );
3110    }
3111
3112    #[test]
3113    fn node_hierarchy_item_id_debug_and_display_agree() {
3114        let none = NodeHierarchyItemId::NONE;
3115        assert_eq!(format!("{none:?}"), "None");
3116        assert_eq!(format!("{none}"), format!("{none:?}"));
3117
3118        let some = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(5)));
3119        assert_eq!(format!("{some:?}"), "Some(NodeId(5))");
3120        assert_eq!(format!("{some}"), format!("{some:?}"));
3121
3122        // Extreme value: must not panic and must stay non-empty.
3123        let max = NodeHierarchyItemId::from_raw(usize::MAX);
3124        assert!(!format!("{max:?}").is_empty());
3125    }
3126
3127    #[test]
3128    fn node_hierarchy_item_id_ordering_follows_raw_value() {
3129        let a = NodeHierarchyItemId::from_raw(0);
3130        let b = NodeHierarchyItemId::from_raw(1);
3131        let c = NodeHierarchyItemId::from_raw(usize::MAX);
3132        assert!(a < b);
3133        assert!(b < c);
3134        assert_eq!(a, NodeHierarchyItemId::NONE);
3135    }
3136
3137    #[test]
3138    fn node_hierarchy_item_id_from_impls_match_the_explicit_ones() {
3139        let opt = Some(NodeId::new(41));
3140        let via_from: NodeHierarchyItemId = opt.into();
3141        assert_eq!(via_from, NodeHierarchyItemId::from_crate_internal(opt));
3142
3143        let back: Option<NodeId> = via_from.into();
3144        assert_eq!(back, opt);
3145
3146        let none: NodeHierarchyItemId = None.into();
3147        assert_eq!(none.into_raw(), 0);
3148    }
3149
3150    // ---------------------------------------------------------------------
3151    // NodeHierarchyItem getters
3152    // ---------------------------------------------------------------------
3153
3154    #[test]
3155    fn node_hierarchy_item_zeroed_has_no_links() {
3156        let z = NodeHierarchyItem::zeroed();
3157        assert_eq!(z.parent_id(), None);
3158        assert_eq!(z.previous_sibling_id(), None);
3159        assert_eq!(z.next_sibling_id(), None);
3160        assert_eq!(z.last_child_id(), None);
3161        assert_eq!(z.first_child_id(NodeId::ZERO), None);
3162        assert_eq!(z.first_child_id(NodeId::new(usize::MAX)), None);
3163        assert_eq!(z, NodeHierarchyItem::from(Node::ROOT));
3164    }
3165
3166    #[test]
3167    fn node_hierarchy_item_getters_decode_the_one_based_fields() {
3168        let item = raw_item(1, 2, 3, 4);
3169        assert_eq!(item.parent_id(), Some(NodeId::new(0)));
3170        assert_eq!(item.previous_sibling_id(), Some(NodeId::new(1)));
3171        assert_eq!(item.next_sibling_id(), Some(NodeId::new(2)));
3172        assert_eq!(item.last_child_id(), Some(NodeId::new(3)));
3173
3174        // first_child is derived: parent + 1, but only if the node has children.
3175        assert_eq!(item.first_child_id(NodeId::new(7)), Some(NodeId::new(8)));
3176    }
3177
3178    #[test]
3179    fn node_hierarchy_item_getters_at_usize_max_do_not_overflow() {
3180        let item = raw_item(usize::MAX, usize::MAX, usize::MAX, usize::MAX);
3181        assert_eq!(item.parent_id(), Some(NodeId::new(usize::MAX - 1)));
3182        assert_eq!(item.previous_sibling_id(), Some(NodeId::new(usize::MAX - 1)));
3183        assert_eq!(item.next_sibling_id(), Some(NodeId::new(usize::MAX - 1)));
3184        assert_eq!(item.last_child_id(), Some(NodeId::new(usize::MAX - 1)));
3185
3186        // NodeId's Add is saturating, so `current + 1` clamps instead of wrapping
3187        // to 0 (which would alias the root node).
3188        assert_eq!(
3189            item.first_child_id(NodeId::new(usize::MAX)),
3190            Some(NodeId::new(usize::MAX)),
3191            "first_child_id must saturate, never wrap to NodeId(0)"
3192        );
3193    }
3194
3195    #[test]
3196    fn node_hierarchy_item_from_node_preserves_every_link() {
3197        let node = Node {
3198            parent: Some(NodeId::new(3)),
3199            previous_sibling: None,
3200            next_sibling: Some(NodeId::new(9)),
3201            last_child: Some(NodeId::new(12)),
3202        };
3203        let item: NodeHierarchyItem = node.into();
3204        assert_eq!(item.parent_id(), node.parent);
3205        assert_eq!(item.previous_sibling_id(), node.previous_sibling);
3206        assert_eq!(item.next_sibling_id(), node.next_sibling);
3207        assert_eq!(item.last_child_id(), node.last_child);
3208    }
3209
3210    // ---------------------------------------------------------------------
3211    // NodeHierarchyItemVec container + subtree_len
3212    // ---------------------------------------------------------------------
3213
3214    #[test]
3215    fn node_hierarchy_item_vec_containers_read_and_write() {
3216        let mut v: NodeHierarchyItemVec = vec![NodeHierarchyItem::zeroed(); 2].into();
3217        {
3218            let mut c = v.as_container_mut();
3219            c[NodeId::new(1)].parent = 1; // raw 1 == NodeId(0)
3220        }
3221        let c = v.as_container();
3222        assert_eq!(c.len(), 2);
3223        assert_eq!(c[NodeId::new(1)].parent_id(), Some(NodeId::ZERO));
3224        assert!(c.get(NodeId::new(2)).is_none());
3225
3226        let empty: NodeHierarchyItemVec = Vec::new().into();
3227        assert!(empty.as_container().is_empty());
3228    }
3229
3230    #[test]
3231    fn subtree_len_counts_descendants_of_a_real_tree() {
3232        // body(0) > div(1) > div(2)
3233        let sd = nested_body();
3234        let h = sd.node_hierarchy.as_container();
3235        assert_eq!(h.len(), 3);
3236        assert_eq!(h.subtree_len(NodeId::ZERO), 2, "root has 2 descendants");
3237        assert_eq!(h.subtree_len(NodeId::new(1)), 1);
3238        assert_eq!(h.subtree_len(NodeId::new(2)), 0, "a leaf has no descendants");
3239    }
3240
3241    #[test]
3242    fn subtree_len_saturates_on_a_malformed_backwards_next_sibling() {
3243        // Node 2 claims its next sibling is node 0 — a backwards link a malformed
3244        // FastDom can produce. The subtraction must saturate, not underflow-panic.
3245        let v: NodeHierarchyItemVec = vec![
3246            raw_item(0, 0, 0, 0),
3247            raw_item(0, 0, 0, 0),
3248            raw_item(0, 0, /* next = NodeId(0) */ 1, 0),
3249        ]
3250        .into();
3251        let c = v.as_container();
3252        assert_eq!(c.subtree_len(NodeId::new(2)), 0);
3253
3254        // Self-referential next_sibling (node 1 -> node 1) must also saturate.
3255        let v2: NodeHierarchyItemVec = vec![raw_item(0, 0, 0, 0), raw_item(0, 0, 2, 0)].into();
3256        assert_eq!(v2.as_container().subtree_len(NodeId::new(1)), 0);
3257    }
3258
3259    // ---------------------------------------------------------------------
3260    // StyledDomMemoryReport
3261    // ---------------------------------------------------------------------
3262
3263    #[test]
3264    fn memory_report_default_total_is_zero() {
3265        assert_eq!(StyledDomMemoryReport::default().total_bytes(), 0);
3266    }
3267
3268    #[test]
3269    fn memory_report_total_bytes_sums_every_field() {
3270        let r = StyledDomMemoryReport {
3271            node_count: 3,
3272            node_hierarchy_bytes: 1,
3273            node_data_bytes: 2,
3274            styled_nodes_bytes: 4,
3275            cascade_info_bytes: 8,
3276            tag_ids_bytes: 16,
3277            non_leaf_nodes_bytes: 32,
3278            callback_vecs_bytes: 64,
3279            ..StyledDomMemoryReport::default()
3280        };
3281        assert_eq!(r.total_bytes(), 127, "node_count must NOT be part of the sum");
3282
3283        // A single saturated field must not overflow the running sum.
3284        let extreme = StyledDomMemoryReport {
3285            node_data_bytes: usize::MAX,
3286            ..StyledDomMemoryReport::default()
3287        };
3288        assert_eq!(extreme.total_bytes(), usize::MAX);
3289    }
3290
3291    #[test]
3292    fn memory_report_tracks_node_count_and_is_monotonic_in_dom_size() {
3293        let small = flat_body(1).memory_report();
3294        let large = flat_body(50).memory_report();
3295        assert_eq!(small.node_count, 2);
3296        assert_eq!(large.node_count, 51);
3297        assert!(large.total_bytes() > small.total_bytes());
3298        assert!(small.total_bytes() >= small.node_hierarchy_bytes + small.node_data_bytes);
3299
3300        // Also fine on the smallest possible DOM.
3301        let d = StyledDom::default().memory_report();
3302        assert_eq!(d.node_count, 1);
3303        assert!(d.total_bytes() > 0);
3304    }
3305
3306    // ---------------------------------------------------------------------
3307    // StyledDom construction
3308    // ---------------------------------------------------------------------
3309
3310    #[test]
3311    fn default_styled_dom_is_a_single_rooted_body() {
3312        let sd = StyledDom::default();
3313        assert_eq!(sd.node_count(), 1);
3314        assert_eq!(sd.root.into_crate_internal(), Some(NodeId::ZERO));
3315        assert_eq!(sd.node_hierarchy.as_ref().len(), 1);
3316        assert_eq!(sd.styled_nodes.as_ref().len(), 1);
3317        assert_eq!(sd.cascade_info.as_ref().len(), 1);
3318        assert_eq!(sd.non_leaf_nodes.as_ref().len(), 1);
3319        assert_eq!(sd.non_leaf_nodes.as_ref()[0].depth, 0);
3320        assert!(sd.tag_ids_to_node_ids.as_ref().is_empty());
3321        assert!(sd.get_styled_node_state(&NodeId::ZERO).is_normal());
3322    }
3323
3324    #[test]
3325    fn create_empties_the_source_dom() {
3326        // Documented: "After calling this function, the DOM will be reset to an empty DOM."
3327        let mut dom = Dom::create_body().with_children(vec![Dom::create_div(); 3].into());
3328        let sd = StyledDom::create(&mut dom, Css::empty());
3329        assert_eq!(sd.node_count(), 4);
3330        assert!(
3331            dom.children.as_ref().is_empty(),
3332            "the source Dom must be left empty (it is swapped out, not cloned)"
3333        );
3334    }
3335
3336    #[test]
3337    fn create_keeps_every_parallel_array_the_same_length() {
3338        for n in [0usize, 1, 3, 64] {
3339            let sd = flat_body(n);
3340            let count = sd.node_count();
3341            assert_eq!(count, n + 1);
3342            assert_eq!(sd.node_hierarchy.as_ref().len(), count);
3343            assert_eq!(sd.styled_nodes.as_ref().len(), count);
3344            assert_eq!(sd.cascade_info.as_ref().len(), count);
3345        }
3346    }
3347
3348    #[test]
3349    fn create_survives_malformed_truncated_and_unicode_css() {
3350        let cases: Vec<String> = vec![
3351            String::new(),
3352            "}}}{{{".to_string(),
3353            "div {".to_string(),
3354            "div { color: }".to_string(),
3355            "div { : red; }".to_string(),
3356            "@media".to_string(),
3357            "/* unterminated comment".to_string(),
3358            "div { width: 99999999999999999999999px; }".to_string(),
3359            "div { width: -0px; opacity: 1e400; }".to_string(),
3360            "div { width: NaNpx; height: infpx; }".to_string(),
3361            "* { color: #ZZZZZZ; }".to_string(),
3362            "日本語 { content: \"🦀\"; }".to_string(),
3363            ".\u{202e}rtl { color: red; }".to_string(),
3364            "a".repeat(10_000),
3365            "div { color: red; }".repeat(500),
3366        ];
3367
3368        for case in &cases {
3369            let css = parse_css(case);
3370            let mut dom = Dom::create_body().with_children(vec![Dom::create_div()].into());
3371            let sd = StyledDom::create(&mut dom, css);
3372            assert_eq!(
3373                sd.node_count(),
3374                2,
3375                "CSS must never change the node count; failing input: {case:?}"
3376            );
3377        }
3378    }
3379
3380    #[test]
3381    fn create_handles_deep_and_wide_doms() {
3382        // deep: 64 nested divs under a body
3383        let mut deep = Dom::create_div();
3384        for _ in 0..63 {
3385            deep = Dom::create_div().with_children(vec![deep].into());
3386        }
3387        let mut deep_body = Dom::create_body().with_children(vec![deep].into());
3388        let sd = StyledDom::create(&mut deep_body, Css::empty());
3389        assert_eq!(sd.node_count(), 65);
3390        assert_eq!(
3391            sd.non_leaf_nodes.as_ref().len(),
3392            64,
3393            "every node except the innermost leaf is a parent"
3394        );
3395
3396        // wide: 1000 siblings
3397        let wide = flat_body(1000);
3398        assert_eq!(wide.node_count(), 1001);
3399        assert_eq!(wide.node_hierarchy.as_container().subtree_len(NodeId::ZERO), 1000);
3400        assert_eq!(wide.non_leaf_nodes.as_ref().len(), 1);
3401    }
3402
3403    #[test]
3404    fn create_from_dom_collects_scoped_css_without_changing_the_tree() {
3405        let dom = Dom::create_body().with_children(
3406            vec![
3407                Dom::create_div().with_css("color: red"),
3408                Dom::create_div().with_children(vec![Dom::create_div().with_css("width: 5px")].into()),
3409            ]
3410            .into(),
3411        );
3412        let sd = StyledDom::create_from_dom(dom);
3413        assert_eq!(sd.node_count(), 4);
3414        assert_eq!(sd.node_hierarchy.as_ref().len(), 4);
3415        assert!(sd.get_css_property_cache().compact_cache.is_some());
3416    }
3417
3418    #[test]
3419    fn create_from_dom_on_a_bare_leaf_produces_one_node() {
3420        let sd = StyledDom::create_from_dom(Dom::create_div());
3421        assert_eq!(sd.node_count(), 1);
3422        assert_eq!(sd.root.into_crate_internal(), Some(NodeId::ZERO));
3423    }
3424
3425    // ---------------------------------------------------------------------
3426    // append_child / append_child_with_index / finalize / with_child
3427    // ---------------------------------------------------------------------
3428
3429    #[test]
3430    fn append_child_grows_the_node_count_by_the_child_dom_size() {
3431        let mut base = flat_body(2);
3432        base.append_child(flat_body(3));
3433        assert_eq!(base.node_count(), 3 + 4);
3434        assert_eq!(base.node_hierarchy.as_ref().len(), 7);
3435        assert_eq!(base.styled_nodes.as_ref().len(), 7);
3436        assert_eq!(base.cascade_info.as_ref().len(), 7);
3437    }
3438
3439    #[test]
3440    fn append_child_links_the_new_root_as_the_last_sibling() {
3441        // Flat parent: body(0) > [div(1), div(2)], then append a 1-node StyledDom.
3442        let mut base = flat_body(2);
3443        base.append_child(StyledDom::default());
3444
3445        let h = base.node_hierarchy.as_container();
3446        let children: Vec<NodeId> = NodeId::ZERO.az_children(&h).collect();
3447        assert_eq!(
3448            children,
3449            vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)],
3450            "the appended root must become the last direct child"
3451        );
3452        assert_eq!(h[NodeId::new(3)].parent_id(), Some(NodeId::ZERO));
3453        assert_eq!(h[NodeId::new(3)].previous_sibling_id(), Some(NodeId::new(2)));
3454        assert_eq!(h[NodeId::new(3)].next_sibling_id(), None);
3455    }
3456
3457    /// ADVERSARIAL: `append_child` reads `last_child_id()` to find the current
3458    /// last sibling. If `last_child` names a *descendant* rather than the last
3459    /// *direct child*, the appended root is spliced into the wrong sibling chain
3460    /// and disappears from the root's children.
3461    #[test]
3462    fn append_child_keeps_the_root_children_reachable_for_a_nested_dom() {
3463        let mut base = nested_body(); // body(0) > div(1) > div(2)
3464        base.append_child(StyledDom::default());
3465        assert_eq!(base.node_count(), 4);
3466
3467        let h = base.node_hierarchy.as_container();
3468        let children: Vec<NodeId> = NodeId::ZERO.az_children(&h).collect();
3469        assert_eq!(
3470            children,
3471            vec![NodeId::new(1), NodeId::new(3)],
3472            "after append_child the root must have exactly its old child plus the appended root"
3473        );
3474    }
3475
3476    #[test]
3477    fn append_child_with_index_saturates_the_u32_cascade_index() {
3478        for (child_index, expected) in [
3479            (0usize, 0u32),
3480            (7, 7),
3481            (u32::MAX as usize, u32::MAX),
3482            (u32::MAX as usize + 1, u32::MAX),
3483            (usize::MAX, u32::MAX),
3484        ] {
3485            let mut base = flat_body(0); // single body node
3486            base.append_child_with_index(StyledDom::default(), child_index);
3487
3488            // The appended root lands at index self_len == 1 in the merged arrays.
3489            assert_eq!(
3490                base.cascade_info.as_ref()[1].index_in_parent,
3491                expected,
3492                "child_index {child_index} must saturate to {expected}, never wrap"
3493            );
3494            assert!(base.cascade_info.as_ref()[1].is_last_child);
3495            assert_eq!(base.node_count(), 2);
3496        }
3497    }
3498
3499    #[test]
3500    fn finalize_non_leaf_nodes_sorts_by_depth_and_is_idempotent() {
3501        let mut base = flat_body(1);
3502        base.append_child_with_index(flat_body(2), 1);
3503        base.append_child_with_index(flat_body(2), 2);
3504        base.finalize_non_leaf_nodes();
3505
3506        let depths: Vec<usize> = base.non_leaf_nodes.as_ref().iter().map(|p| p.depth).collect();
3507        let mut sorted = depths.clone();
3508        sorted.sort_unstable();
3509        assert_eq!(depths, sorted, "non_leaf_nodes must be depth-ordered");
3510
3511        base.finalize_non_leaf_nodes();
3512        let again: Vec<usize> = base.non_leaf_nodes.as_ref().iter().map(|p| p.depth).collect();
3513        assert_eq!(depths, again, "finalize must be idempotent");
3514    }
3515
3516    #[test]
3517    fn with_child_matches_append_child() {
3518        let mut appended = flat_body(2);
3519        appended.append_child(flat_body(1));
3520
3521        let built = flat_body(2).with_child(flat_body(1));
3522
3523        assert_eq!(built.node_count(), appended.node_count());
3524        assert_eq!(
3525            built.node_hierarchy.as_ref(),
3526            appended.node_hierarchy.as_ref()
3527        );
3528    }
3529
3530    #[test]
3531    fn swap_with_default_returns_the_old_dom_and_resets_self() {
3532        let mut sd = flat_body(3);
3533        let old = sd.swap_with_default();
3534        assert_eq!(old.node_count(), 4);
3535        assert_eq!(sd.node_count(), 1, "self must be left as the default StyledDom");
3536        assert_eq!(sd.root.into_crate_internal(), Some(NodeId::ZERO));
3537    }
3538
3539    // ---------------------------------------------------------------------
3540    // Menus
3541    // ---------------------------------------------------------------------
3542
3543    #[test]
3544    fn context_menu_and_menu_bar_are_stored_on_the_root_node() {
3545        let mut sd = flat_body(1);
3546        assert!(sd.node_data.as_container()[NodeId::ZERO].get_context_menu().is_none());
3547
3548        sd.set_context_menu(empty_menu());
3549        sd.set_menu_bar(empty_menu());
3550
3551        let data = sd.node_data.as_container();
3552        assert!(data[NodeId::ZERO].get_context_menu().is_some());
3553        assert!(data[NodeId::ZERO].get_menu_bar().is_some());
3554
3555        // ...and the child must not have inherited either of them.
3556        assert!(data[NodeId::new(1)].get_context_menu().is_none());
3557        assert!(data[NodeId::new(1)].get_menu_bar().is_none());
3558    }
3559
3560    #[test]
3561    fn menu_builders_are_equivalent_to_the_setters_and_dont_touch_the_tree() {
3562        let sd = StyledDom::default()
3563            .with_context_menu(empty_menu())
3564            .with_menu_bar(empty_menu());
3565        assert_eq!(sd.node_count(), 1);
3566        let data = sd.node_data.as_container();
3567        assert!(data[NodeId::ZERO].get_context_menu().is_some());
3568        assert!(data[NodeId::ZERO].get_menu_bar().is_some());
3569    }
3570
3571    // ---------------------------------------------------------------------
3572    // restyle_nodes_* / restyle_on_state_change / restyle_user_property
3573    // ---------------------------------------------------------------------
3574
3575    #[test]
3576    fn restyle_nodes_hover_sets_and_clears_the_state_flag() {
3577        let mut sd = flat_body(2);
3578        let _ = sd.restyle_nodes_hover(&[NodeId::new(1)], true);
3579        assert!(sd.get_styled_node_state(&NodeId::new(1)).hover);
3580        assert!(!sd.get_styled_node_state(&NodeId::new(2)).hover);
3581
3582        let _ = sd.restyle_nodes_hover(&[NodeId::new(1)], false);
3583        assert!(!sd.get_styled_node_state(&NodeId::new(1)).hover);
3584        assert!(sd.get_styled_node_state(&NodeId::new(1)).is_normal());
3585    }
3586
3587    #[test]
3588    fn restyle_nodes_active_and_focus_set_independent_flags() {
3589        let mut sd = flat_body(1);
3590        let _ = sd.restyle_nodes_active(&[NodeId::ZERO], true);
3591        let _ = sd.restyle_nodes_focus(&[NodeId::ZERO], true);
3592
3593        let state = sd.get_styled_node_state(&NodeId::ZERO);
3594        assert!(state.active);
3595        assert!(state.focused);
3596        assert!(!state.hover, "hover must be untouched");
3597        assert!(!state.is_normal());
3598    }
3599
3600    #[test]
3601    fn restyle_nodes_ignores_out_of_range_node_ids_instead_of_panicking() {
3602        let mut sd = flat_body(1); // valid ids: 0, 1
3603        let changed = sd.restyle_nodes_hover(&[NodeId::new(2), NodeId::new(usize::MAX)], true);
3604        assert!(changed.is_empty());
3605        assert!(!sd.get_styled_node_state(&NodeId::ZERO).hover);
3606        assert!(!sd.get_styled_node_state(&NodeId::new(1)).hover);
3607
3608        // A mix of valid and stale ids must still apply the valid ones.
3609        let _ = sd.restyle_nodes_hover(&[NodeId::new(1), NodeId::new(999)], true);
3610        assert!(sd.get_styled_node_state(&NodeId::new(1)).hover);
3611    }
3612
3613    #[test]
3614    fn restyle_nodes_handles_empty_and_duplicated_input() {
3615        let mut sd = flat_body(1);
3616        assert!(sd.restyle_nodes_focus(&[], true).is_empty());
3617
3618        // Duplicates must be idempotent, not double-applied or panicking.
3619        let _ = sd.restyle_nodes_focus(&[NodeId::ZERO, NodeId::ZERO, NodeId::ZERO], true);
3620        assert!(sd.get_styled_node_state(&NodeId::ZERO).focused);
3621    }
3622
3623    #[test]
3624    #[should_panic(expected = "index out of bounds")]
3625    fn get_styled_node_state_panics_on_an_out_of_range_node_id() {
3626        // Documents the contract: unlike restyle_nodes_*, this getter does NOT
3627        // bounds-check — callers must pass an id that indexes into this DOM.
3628        let sd = flat_body(1);
3629        let _ = sd.get_styled_node_state(&NodeId::new(99));
3630    }
3631
3632    #[test]
3633    fn restyle_on_state_change_with_no_changes_reports_nothing_to_do() {
3634        let mut sd = flat_body(2);
3635        let r = sd.restyle_on_state_change(None, None, None);
3636        assert!(!r.has_changes());
3637        assert!(!r.needs_layout);
3638        assert!(!r.needs_display_list);
3639        assert!(!r.gpu_only_changes);
3640        assert_eq!(r.max_relayout_scope, RelayoutScope::None);
3641    }
3642
3643    #[test]
3644    fn restyle_on_state_change_tolerates_stale_node_ids() {
3645        let mut sd = flat_body(1);
3646        let r = sd.restyle_on_state_change(
3647            Some(FocusChange {
3648                lost_focus: Some(NodeId::new(500)),
3649                gained_focus: Some(NodeId::new(usize::MAX)),
3650            }),
3651            Some(HoverChange {
3652                left_nodes: vec![NodeId::new(700)],
3653                entered_nodes: vec![NodeId::new(800)],
3654            }),
3655            Some(ActiveChange {
3656                deactivated: vec![NodeId::new(900)],
3657                activated: vec![NodeId::new(1000)],
3658            }),
3659        );
3660        assert!(!r.has_changes(), "stale ids must be filtered, not applied");
3661        assert_eq!(sd.node_count(), 2);
3662    }
3663
3664    #[test]
3665    fn restyle_on_state_change_applies_state_to_valid_nodes() {
3666        let mut sd = flat_body(1);
3667        let r = sd.restyle_on_state_change(
3668            None,
3669            Some(HoverChange {
3670                left_nodes: Vec::new(),
3671                entered_nodes: vec![NodeId::new(1)],
3672            }),
3673            None,
3674        );
3675        assert!(sd.get_styled_node_state(&NodeId::new(1)).hover);
3676        assert!(
3677            r.changed_nodes.keys().all(|n| *n == NodeId::new(1)),
3678            "only the node whose state actually changed may be reported"
3679        );
3680    }
3681
3682    #[test]
3683    fn restyle_user_property_rejects_empty_lists_and_stale_nodes() {
3684        let mut sd = flat_body(1);
3685        assert!(sd.restyle_user_property(&NodeId::ZERO, &[]).is_empty());
3686        assert!(
3687            sd.restyle_user_property(
3688                &NodeId::new(50),
3689                &[CssProperty::auto(CssPropertyType::Width)]
3690            )
3691            .is_empty(),
3692            "an out-of-range node id must be a no-op, not a panic"
3693        );
3694        assert!(
3695            sd.get_css_property_cache()
3696                .user_overridden_properties
3697                .iter()
3698                .all(Vec::is_empty),
3699            "a rejected call must not record an override"
3700        );
3701    }
3702
3703    #[test]
3704    fn restyle_user_property_stores_the_override_and_initial_removes_it() {
3705        let mut sd = flat_body(1);
3706        let node = NodeId::ZERO;
3707
3708        let _ = sd.restyle_user_property(&node, &[CssProperty::auto(CssPropertyType::Width)]);
3709        {
3710            let overrides = &sd.get_css_property_cache().user_overridden_properties;
3711            assert_eq!(overrides.len(), sd.node_count(), "table grows to cover the DOM");
3712            assert_eq!(overrides[0].len(), 1);
3713            assert_eq!(overrides[0][0].0, CssPropertyType::Width);
3714        }
3715
3716        // Re-setting the same type replaces rather than duplicating.
3717        let _ = sd.restyle_user_property(&node, &[CssProperty::none(CssPropertyType::Width)]);
3718        assert_eq!(sd.get_css_property_cache().user_overridden_properties[0].len(), 1);
3719
3720        // CssProperty::Initial removes the override again.
3721        let _ = sd.restyle_user_property(&node, &[CssProperty::initial(CssPropertyType::Width)]);
3722        assert!(sd.get_css_property_cache().user_overridden_properties[0].is_empty());
3723
3724        // Removing a property that was never set must not panic.
3725        let _ = sd.restyle_user_property(&node, &[CssProperty::initial(CssPropertyType::Height)]);
3726        assert!(sd.get_css_property_cache().user_overridden_properties[0].is_empty());
3727    }
3728
3729    #[test]
3730    fn restyle_and_recompute_preserve_the_tree_and_rebuild_the_compact_cache() {
3731        let mut sd = flat_body(3);
3732        let before = sd.node_count();
3733
3734        sd.restyle(parse_css("div { color: red; } body > div:hover { color: blue; }"));
3735        assert_eq!(sd.node_count(), before);
3736        assert!(sd.get_css_property_cache().compact_cache.is_some());
3737
3738        // A second restyle with garbage CSS must not corrupt the structure.
3739        sd.restyle(parse_css("}}} div { : ; }"));
3740        assert_eq!(sd.node_count(), before);
3741
3742        sd.recompute_inheritance_and_compact_cache();
3743        assert_eq!(sd.node_count(), before);
3744        assert!(sd.get_css_property_cache().compact_cache.is_some());
3745    }
3746
3747    #[test]
3748    fn get_css_property_cache_mut_sees_the_same_cache_as_the_shared_getter() {
3749        let mut sd = flat_body(1);
3750        let node_count = sd.node_count();
3751        sd.get_css_property_cache_mut()
3752            .user_overridden_properties
3753            .resize(node_count, Vec::new());
3754        assert_eq!(
3755            sd.get_css_property_cache().user_overridden_properties.len(),
3756            node_count
3757        );
3758    }
3759
3760    // ---------------------------------------------------------------------
3761    // get_html_string
3762    // ---------------------------------------------------------------------
3763
3764    #[test]
3765    fn get_html_string_test_mode_omits_the_html_wrapper() {
3766        let sd = flat_body(2);
3767        let out = sd.get_html_string("HEAD_MARK", "BODY_MARK", true);
3768        assert!(!out.is_empty());
3769        assert!(!out.contains("HEAD_MARK"), "test_mode must not emit the custom head");
3770        assert!(!out.contains("BODY_MARK"), "test_mode must not emit the custom body");
3771        assert!(!out.contains("<html>"));
3772    }
3773
3774    #[test]
3775    fn get_html_string_embeds_custom_head_and_body_verbatim() {
3776        let sd = flat_body(1);
3777        let head = "🦀 <meta charset=\"utf-8\"> & ünïcödé";
3778        let body = "x".repeat(10_000);
3779        let out = sd.get_html_string(head, &body, false);
3780        assert!(out.contains("<html>"));
3781        assert!(out.contains(head));
3782        assert!(out.contains(&body));
3783    }
3784
3785    #[test]
3786    fn get_html_string_does_not_panic_on_extreme_doms() {
3787        // A single-node DOM has no non_leaf parent entry for its root — the depth
3788        // lookup must fall back to 0 rather than panic-indexing the map.
3789        assert!(!StyledDom::default().get_html_string("", "", true).is_empty());
3790        assert!(!flat_body(0).get_html_string("", "", true).is_empty());
3791        assert!(!nested_body().get_html_string("", "", true).is_empty());
3792        assert!(!flat_body(200).get_html_string("", "", true).is_empty());
3793    }
3794
3795    // ---------------------------------------------------------------------
3796    // rendering order
3797    // ---------------------------------------------------------------------
3798
3799    #[test]
3800    fn get_rects_in_rendering_order_is_a_permutation_of_the_children() {
3801        let sd = flat_body(3);
3802        let group = sd.get_rects_in_rendering_order();
3803        assert_eq!(group.root.into_crate_internal(), Some(NodeId::ZERO));
3804
3805        let mut ids: Vec<usize> = group
3806            .children
3807            .as_ref()
3808            .iter()
3809            .filter_map(|c| c.root.into_crate_internal())
3810            .map(|n| n.index())
3811            .collect();
3812        ids.sort_unstable();
3813        assert_eq!(ids, vec![1, 2, 3], "every child appears exactly once");
3814    }
3815
3816    #[test]
3817    fn get_rects_in_rendering_order_nests_grandchildren() {
3818        let sd = nested_body(); // body(0) > div(1) > div(2)
3819        let group = sd.get_rects_in_rendering_order();
3820        assert_eq!(group.children.as_ref().len(), 1);
3821
3822        let child = &group.children.as_ref()[0];
3823        assert_eq!(child.root.into_crate_internal(), Some(NodeId::new(1)));
3824        assert_eq!(child.children.as_ref().len(), 1);
3825        assert_eq!(
3826            child.children.as_ref()[0].root.into_crate_internal(),
3827            Some(NodeId::new(2))
3828        );
3829    }
3830
3831    #[test]
3832    fn determine_rendering_order_with_no_parents_yields_a_childless_root() {
3833        let sd = StyledDom::default();
3834        let hierarchy = sd.node_hierarchy.as_container();
3835        let styled = sd.styled_nodes.as_container();
3836        let data = sd.node_data.as_container();
3837
3838        let group = StyledDom::determine_rendering_order(
3839            &[],
3840            &hierarchy,
3841            &styled,
3842            &data,
3843            sd.get_css_property_cache(),
3844        );
3845        assert_eq!(group.root.into_crate_internal(), Some(NodeId::ZERO));
3846        assert!(group.children.as_ref().is_empty());
3847    }
3848
3849    #[test]
3850    fn sort_children_by_position_returns_every_child_of_a_leaf_free_parent() {
3851        let sd = flat_body(3);
3852        let hierarchy = sd.node_hierarchy.as_container();
3853        let styled = sd.styled_nodes.as_container();
3854        let data = sd.node_data.as_container();
3855
3856        let sorted = sort_children_by_position(
3857            NodeId::ZERO,
3858            &hierarchy,
3859            &styled,
3860            &data,
3861            sd.get_css_property_cache(),
3862        );
3863        assert_eq!(sorted.len(), 3);
3864
3865        // A leaf parent has no children at all.
3866        let leaf = sort_children_by_position(
3867            NodeId::new(3),
3868            &hierarchy,
3869            &styled,
3870            &data,
3871            sd.get_css_property_cache(),
3872        );
3873        assert!(leaf.is_empty());
3874    }
3875
3876    #[test]
3877    fn fill_content_group_children_builds_the_nested_group_tree() {
3878        let id = |i: usize| NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(i)));
3879
3880        let mut sorted: BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>> = BTreeMap::new();
3881        sorted.insert(id(0), vec![id(1), id(2)]);
3882        sorted.insert(id(1), vec![id(3)]);
3883
3884        let mut group = ContentGroup {
3885            root: id(0),
3886            children: Vec::new().into(),
3887        };
3888        fill_content_group_children(&mut group, &sorted);
3889
3890        assert_eq!(group.children.as_ref().len(), 2);
3891        assert_eq!(group.children.as_ref()[0].root, id(1));
3892        assert_eq!(group.children.as_ref()[0].children.as_ref().len(), 1);
3893        assert_eq!(group.children.as_ref()[0].children.as_ref()[0].root, id(3));
3894        assert!(
3895            group.children.as_ref()[1].children.as_ref().is_empty(),
3896            "a node with no entry in the map is a leaf"
3897        );
3898    }
3899
3900    #[test]
3901    fn fill_content_group_children_leaves_an_unknown_root_untouched() {
3902        let sorted: BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>> = BTreeMap::new();
3903        let mut group = ContentGroup {
3904            root: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(9))),
3905            children: Vec::new().into(),
3906        };
3907        fill_content_group_children(&mut group, &sorted);
3908        assert!(group.children.as_ref().is_empty());
3909    }
3910
3911    // ---------------------------------------------------------------------
3912    // recursive_get_last_child / get_path_to_root
3913    // ---------------------------------------------------------------------
3914
3915    #[test]
3916    fn recursive_get_last_child_descends_to_the_deepest_last_child() {
3917        // 0 -> 1 -> 2 (2 is a leaf)
3918        let items = vec![
3919            raw_item(0, 0, 0, 2), // node 0, last_child = NodeId(1)
3920            raw_item(1, 0, 0, 3), // node 1, last_child = NodeId(2)
3921            raw_item(2, 0, 0, 0), // node 2, leaf
3922        ];
3923
3924        let mut target = None;
3925        recursive_get_last_child(NodeId::ZERO, &items, &mut target);
3926        assert_eq!(target, Some(NodeId::new(2)));
3927    }
3928
3929    #[test]
3930    fn recursive_get_last_child_leaves_the_target_untouched_for_a_leaf() {
3931        let items = vec![raw_item(0, 0, 0, 0)];
3932        let mut target = None;
3933        recursive_get_last_child(NodeId::ZERO, &items, &mut target);
3934        assert_eq!(target, None);
3935
3936        // A pre-set target is also left alone.
3937        let mut preset = Some(NodeId::new(7));
3938        recursive_get_last_child(NodeId::ZERO, &items, &mut preset);
3939        assert_eq!(preset, Some(NodeId::new(7)));
3940    }
3941
3942    #[test]
3943    fn get_path_to_root_is_root_first_and_tolerates_unknown_nodes() {
3944        let sd = nested_body(); // body(0) > div(1) > div(2)
3945        let h = sd.node_hierarchy.as_container();
3946
3947        assert_eq!(get_path_to_root(&h, NodeId::ZERO), vec![NodeId::ZERO]);
3948        assert_eq!(
3949            get_path_to_root(&h, NodeId::new(2)),
3950            vec![NodeId::ZERO, NodeId::new(1), NodeId::new(2)]
3951        );
3952
3953        // An id outside the arena yields a one-element path instead of panicking.
3954        assert_eq!(
3955            get_path_to_root(&h, NodeId::new(9999)),
3956            vec![NodeId::new(9999)]
3957        );
3958    }
3959
3960    // ---------------------------------------------------------------------
3961    // document order
3962    // ---------------------------------------------------------------------
3963
3964    #[test]
3965    fn is_before_in_document_order_is_false_for_identical_nodes() {
3966        let sd = flat_body(2);
3967        assert!(!is_before_in_document_order(
3968            &sd.node_hierarchy,
3969            NodeId::new(1),
3970            NodeId::new(1)
3971        ));
3972    }
3973
3974    #[test]
3975    fn is_before_in_document_order_orders_ancestors_and_siblings() {
3976        let sd = flat_body(3); // body(0) > [1, 2, 3]
3977        let h = &sd.node_hierarchy;
3978
3979        assert!(is_before_in_document_order(h, NodeId::ZERO, NodeId::new(1)));
3980        assert!(!is_before_in_document_order(h, NodeId::new(1), NodeId::ZERO));
3981        assert!(is_before_in_document_order(h, NodeId::new(1), NodeId::new(3)));
3982        assert!(!is_before_in_document_order(h, NodeId::new(3), NodeId::new(1)));
3983    }
3984
3985    #[test]
3986    fn is_before_in_document_order_is_antisymmetric_across_a_nested_tree() {
3987        let sd = nested_body();
3988        let h = &sd.node_hierarchy;
3989        for a in 0..3 {
3990            for b in 0..3 {
3991                let ab = is_before_in_document_order(h, NodeId::new(a), NodeId::new(b));
3992                let ba = is_before_in_document_order(h, NodeId::new(b), NodeId::new(a));
3993                if a == b {
3994                    assert!(!ab && !ba, "a node is never before itself");
3995                } else {
3996                    assert_ne!(ab, ba, "exactly one of ({a},{b}) / ({b},{a}) must hold");
3997                }
3998            }
3999        }
4000    }
4001
4002    #[test]
4003    fn is_before_in_document_order_is_deterministic_for_unknown_nodes() {
4004        let sd = flat_body(1);
4005        let h = &sd.node_hierarchy;
4006        // Out-of-range ids fall back to a single-element path; the comparison must
4007        // still terminate and return a stable answer instead of panicking.
4008        assert!(is_before_in_document_order(h, NodeId::ZERO, NodeId::new(usize::MAX)));
4009        assert!(!is_before_in_document_order(h, NodeId::new(usize::MAX), NodeId::ZERO));
4010    }
4011
4012    #[test]
4013    fn collect_nodes_in_document_order_start_equals_end() {
4014        let sd = flat_body(2);
4015        assert_eq!(
4016            collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::new(2), NodeId::new(2)),
4017            vec![NodeId::new(2)]
4018        );
4019        // Even a bogus id short-circuits to itself (documented start == end path).
4020        assert_eq!(
4021            collect_nodes_in_document_order(
4022                &sd.node_hierarchy,
4023                NodeId::new(usize::MAX),
4024                NodeId::new(usize::MAX)
4025            ),
4026            vec![NodeId::new(usize::MAX)]
4027        );
4028    }
4029
4030    #[test]
4031    fn collect_nodes_in_document_order_walks_the_tree_in_pre_order() {
4032        let sd = flat_body(3); // body(0) > [1, 2, 3]
4033        assert_eq!(
4034            collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::ZERO, NodeId::new(3)),
4035            vec![NodeId::ZERO, NodeId::new(1), NodeId::new(2), NodeId::new(3)]
4036        );
4037        assert_eq!(
4038            collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::new(1), NodeId::new(2)),
4039            vec![NodeId::new(1), NodeId::new(2)]
4040        );
4041
4042        // Nested: body(0) > div(1) > div(2) — pre-order is 0, 1, 2.
4043        let nested = nested_body();
4044        assert_eq!(
4045            collect_nodes_in_document_order(&nested.node_hierarchy, NodeId::ZERO, NodeId::new(2)),
4046            vec![NodeId::ZERO, NodeId::new(1), NodeId::new(2)]
4047        );
4048    }
4049
4050    #[test]
4051    fn collect_nodes_in_document_order_terminates_when_end_precedes_start() {
4052        // The traversal hits `end` before it ever enters the range, so it bails
4053        // out with an empty result rather than looping forever.
4054        let sd = flat_body(3);
4055        let out = collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::new(2), NodeId::new(1));
4056        assert!(out.is_empty());
4057    }
4058
4059    #[test]
4060    fn collect_nodes_in_document_order_with_an_unreachable_end_stops_at_the_tree_end() {
4061        let sd = flat_body(3);
4062        let out = collect_nodes_in_document_order(
4063            &sd.node_hierarchy,
4064            NodeId::new(1),
4065            NodeId::new(usize::MAX),
4066        );
4067        assert_eq!(
4068            out,
4069            vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)],
4070            "an end node that is never reached must terminate at the end of the traversal"
4071        );
4072    }
4073
4074    // ---------------------------------------------------------------------
4075    // is_layout_equivalent
4076    // ---------------------------------------------------------------------
4077
4078    #[test]
4079    fn is_layout_equivalent_holds_for_independently_built_identical_doms() {
4080        assert!(is_layout_equivalent(&flat_body(3), &flat_body(3)));
4081        assert!(is_layout_equivalent(
4082            &StyledDom::default(),
4083            &StyledDom::default()
4084        ));
4085        assert!(is_layout_equivalent(&nested_body(), &nested_body()));
4086    }
4087
4088    #[test]
4089    fn is_layout_equivalent_rejects_a_different_node_count() {
4090        assert!(!is_layout_equivalent(&flat_body(3), &flat_body(4)));
4091        assert!(!is_layout_equivalent(&flat_body(0), &flat_body(1)));
4092    }
4093
4094    #[test]
4095    fn is_layout_equivalent_rejects_a_different_structure() {
4096        // Same node count (3), different shape: [body > div > div] vs [body > div, div]
4097        assert!(!is_layout_equivalent(&nested_body(), &flat_body(2)));
4098    }
4099
4100    #[test]
4101    fn is_layout_equivalent_rejects_a_changed_class() {
4102        let build = |class: &str| {
4103            let mut dom = Dom::create_body().with_children(
4104                vec![Dom::create_div().with_class(class.to_string().into())].into(),
4105            );
4106            StyledDom::create(&mut dom, Css::empty())
4107        };
4108        assert!(is_layout_equivalent(&build("a"), &build("a")));
4109        assert!(!is_layout_equivalent(&build("a"), &build("b")));
4110    }
4111
4112    #[test]
4113    fn is_layout_equivalent_rejects_a_changed_pseudo_state() {
4114        let base = flat_body(2);
4115        let mut hovered = flat_body(2);
4116        let _ = hovered.restyle_nodes_hover(&[NodeId::new(1)], true);
4117        assert!(
4118            !is_layout_equivalent(&base, &hovered),
4119            ":hover changes CSS resolution, so the DOMs are not layout-equivalent"
4120        );
4121    }
4122
4123    // ---------------------------------------------------------------------
4124    // CompactDom + convert_dom_into_compact_dom
4125    // ---------------------------------------------------------------------
4126
4127    #[test]
4128    fn compact_dom_len_and_is_empty() {
4129        let single = convert_dom_into_compact_dom(Dom::create_div());
4130        assert_eq!(single.len(), 1);
4131        assert!(!single.is_empty());
4132
4133        let tree = convert_dom_into_compact_dom(
4134            Dom::create_body().with_children(vec![Dom::create_div(); 4].into()),
4135        );
4136        assert_eq!(tree.len(), 5);
4137        assert!(!tree.is_empty());
4138
4139        // A hand-built zero-node arena is the only way to observe is_empty() == true.
4140        let empty = CompactDom {
4141            node_hierarchy: NodeHierarchy {
4142                internal: Vec::new(),
4143            },
4144            node_data: NodeDataContainer {
4145                internal: Vec::new(),
4146            },
4147            root: NodeId::ZERO,
4148        };
4149        assert_eq!(empty.len(), 0);
4150        assert!(empty.is_empty());
4151    }
4152
4153    #[test]
4154    fn convert_dom_into_compact_dom_links_flat_siblings() {
4155        let compact = convert_dom_into_compact_dom(
4156            Dom::create_body().with_children(vec![Dom::create_div(); 3].into()),
4157        );
4158        assert_eq!(compact.len(), 4);
4159        assert_eq!(compact.root, NodeId::ZERO);
4160
4161        let h = compact.node_hierarchy.as_ref();
4162        assert_eq!(h[NodeId::ZERO].parent, None);
4163        assert_eq!(h[NodeId::ZERO].last_child, Some(NodeId::new(3)));
4164
4165        for i in 1..=3usize {
4166            assert_eq!(h[NodeId::new(i)].parent, Some(NodeId::ZERO));
4167            let expected_next = if i == 3 { None } else { Some(NodeId::new(i + 1)) };
4168            assert_eq!(h[NodeId::new(i)].next_sibling, expected_next);
4169            let expected_prev = if i == 1 { None } else { Some(NodeId::new(i - 1)) };
4170            assert_eq!(h[NodeId::new(i)].previous_sibling, expected_prev);
4171            assert_eq!(h[NodeId::new(i)].last_child, None, "the children are leaves");
4172        }
4173    }
4174
4175    /// ADVERSARIAL: `last_child` must name the last DIRECT child — that is the
4176    /// contract `NodeHierarchyItem::last_child_id()` documents, the one
4177    /// `az_reverse_children` walks backwards from, and the one `append_child`
4178    /// splices new siblings onto. The flat encoding computes it as
4179    /// `node_id + estimated_total_children`, which is the last node of the whole
4180    /// SUBTREE — those coincide only when the last direct child is a leaf.
4181    #[test]
4182    fn convert_dom_into_compact_dom_last_child_is_the_last_direct_child() {
4183        // body(0) > div(1) > div(2): the body's only direct child is node 1.
4184        let sd = nested_body();
4185        let h = sd.node_hierarchy.as_container();
4186
4187        let last_direct_child = NodeId::ZERO.az_children(&h).last();
4188        assert_eq!(last_direct_child, Some(NodeId::new(1)));
4189        assert_eq!(
4190            h[NodeId::ZERO].last_child_id(),
4191            last_direct_child,
4192            "last_child_id() must agree with the forward child iteration"
4193        );
4194    }
4195
4196    #[test]
4197    fn convert_dom_into_compact_dom_handles_an_empty_and_a_deep_tree() {
4198        assert_eq!(convert_dom_into_compact_dom(Dom::create_body()).len(), 1);
4199
4200        let mut deep = Dom::create_div();
4201        for _ in 0..64 {
4202            deep = Dom::create_div().with_children(vec![deep].into());
4203        }
4204        let compact = convert_dom_into_compact_dom(deep);
4205        assert_eq!(compact.len(), 65);
4206        // Pre-order ids: every node's parent is the node right before it.
4207        let h = compact.node_hierarchy.as_ref();
4208        for i in 1..65usize {
4209            assert_eq!(h[NodeId::new(i)].parent, Some(NodeId::new(i - 1)));
4210        }
4211    }
4212
4213    // ---------------------------------------------------------------------
4214    // scope_inline_css / collect_css_from_dom / strip_css_from_dom
4215    // ---------------------------------------------------------------------
4216
4217    #[test]
4218    fn scope_inline_css_advances_next_id_once_per_node() {
4219        let mut dom = Dom::create_body().with_children(
4220            vec![
4221                Dom::create_div().with_children(vec![Dom::create_div()].into()),
4222                Dom::create_div(),
4223            ]
4224            .into(),
4225        );
4226        let _ = dom.fixup_children_estimated();
4227
4228        let mut next = 0usize;
4229        scope_inline_css(&mut dom, &mut next);
4230        assert_eq!(next, 4, "4 nodes → the counter must land on 4 (pre-order ids 0..3)");
4231    }
4232
4233    #[test]
4234    fn scope_inline_css_from_zero_and_from_a_large_offset() {
4235        let mut leaf = Dom::create_div();
4236        let _ = leaf.fixup_children_estimated();
4237        let mut next = 0usize;
4238        scope_inline_css(&mut leaf, &mut next);
4239        assert_eq!(next, 1, "a single leaf consumes exactly one id");
4240
4241        // A large (but non-saturating) starting id must not panic or wrap.
4242        let mut dom = Dom::create_body().with_children(vec![Dom::create_div(); 2].into());
4243        let _ = dom.fixup_children_estimated();
4244        let mut big = 1_000_000usize;
4245        scope_inline_css(&mut dom, &mut big);
4246        assert_eq!(big, 1_000_003);
4247    }
4248
4249    #[test]
4250    fn scope_inline_css_preserves_the_rule_count_of_every_node() {
4251        let mut dom = Dom::create_body()
4252            .with_css("color: red")
4253            .with_children(vec![Dom::create_div().with_css("width: 5px")].into());
4254        let _ = dom.fixup_children_estimated();
4255
4256        let rules_before: usize = dom
4257            .css
4258            .as_ref()
4259            .iter()
4260            .map(|c| c.rules.as_ref().len())
4261            .sum::<usize>()
4262            + dom.children.as_ref()[0]
4263                .css
4264                .as_ref()
4265                .iter()
4266                .map(|c| c.rules.as_ref().len())
4267                .sum::<usize>();
4268        assert!(rules_before > 0, "with_css must produce at least one rule");
4269
4270        let mut next = 0usize;
4271        scope_inline_css(&mut dom, &mut next);
4272
4273        let rules_after: usize = dom
4274            .css
4275            .as_ref()
4276            .iter()
4277            .map(|c| c.rules.as_ref().len())
4278            .sum::<usize>()
4279            + dom.children.as_ref()[0]
4280                .css
4281                .as_ref()
4282                .iter()
4283                .map(|c| c.rules.as_ref().len())
4284                .sum::<usize>();
4285        assert_eq!(
4286            rules_before, rules_after,
4287            "scoping rewrites paths in place; it must not add or drop rules"
4288        );
4289        assert_eq!(next, 2);
4290    }
4291
4292    #[test]
4293    fn collect_css_from_dom_yields_inner_css_before_outer_css() {
4294        let outer = parse_css("div { color: red; } span { color: blue; }");
4295        let inner = parse_css("p { color: green; }");
4296        let outer_rules = outer.rules.as_ref().len();
4297        let inner_rules = inner.rules.as_ref().len();
4298        assert_ne!(
4299            outer_rules, inner_rules,
4300            "the two stylesheets must be distinguishable by rule count"
4301        );
4302
4303        let mut child = Dom::create_div();
4304        child.add_component_css(inner);
4305        let mut dom = Dom::create_body().with_children(vec![child].into());
4306        dom.add_component_css(outer);
4307
4308        let mut out = Vec::new();
4309        collect_css_from_dom(&dom, &mut out);
4310
4311        assert_eq!(out.len(), 2);
4312        assert_eq!(
4313            out[0].rules.as_ref().len(),
4314            inner_rules,
4315            "deeper CSS is collected first (lower cascade priority)"
4316        );
4317        assert_eq!(out[1].rules.as_ref().len(), outer_rules);
4318    }
4319
4320    #[test]
4321    fn collect_css_from_dom_on_a_css_free_tree_appends_nothing() {
4322        let dom = Dom::create_body().with_children(vec![Dom::create_div(); 3].into());
4323        let mut out = Vec::new();
4324        collect_css_from_dom(&dom, &mut out);
4325        assert!(out.is_empty());
4326
4327        // ...and an already-populated `out` is appended to, not replaced.
4328        let mut prefilled = vec![Css::empty()];
4329        collect_css_from_dom(&dom, &mut prefilled);
4330        assert_eq!(prefilled.len(), 1);
4331    }
4332
4333    #[test]
4334    fn strip_css_from_dom_clears_every_node_recursively() {
4335        let mut dom = Dom::create_body()
4336            .with_css("color: red")
4337            .with_children(
4338                vec![Dom::create_div()
4339                    .with_css("width: 5px")
4340                    .with_children(vec![Dom::create_div().with_css("height: 5px")].into())]
4341                .into(),
4342            );
4343        assert!(!dom.css.as_ref().is_empty());
4344
4345        strip_css_from_dom(&mut dom);
4346
4347        assert!(dom.css.as_ref().is_empty());
4348        let child = &dom.children.as_ref()[0];
4349        assert!(child.css.as_ref().is_empty());
4350        assert!(child.children.as_ref()[0].css.as_ref().is_empty());
4351
4352        // Idempotent.
4353        strip_css_from_dom(&mut dom);
4354        assert!(dom.css.as_ref().is_empty());
4355    }
4356}