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