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

            
11
use alloc::{boxed::Box, collections::btree_map::BTreeMap, string::String, vec::Vec};
12
use core::{
13
    fmt,
14
    hash::{Hash, Hasher},
15
};
16

            
17
use 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

            
66
use 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)]
87
pub struct ChangedCssProperty {
88
    pub previous_state: StyledNodeState,
89
    pub previous_prop: CssProperty,
90
    pub current_state: StyledNodeState,
91
    pub current_prop: CssProperty,
92
}
93

            
94
impl_option!(
95
    ChangedCssProperty,
96
    OptionChangedCssProperty,
97
    copy = false,
98
    [Debug, Clone, PartialEq, Hash, PartialOrd, Eq, Ord]
99
);
100

            
101
impl_vec!(ChangedCssProperty, ChangedCssPropertyVec, ChangedCssPropertyVecDestructor, ChangedCssPropertyVecDestructorType, ChangedCssPropertyVecSlice, OptionChangedCssProperty);
102
impl_vec_debug!(ChangedCssProperty, ChangedCssPropertyVec);
103
impl_vec_partialord!(ChangedCssProperty, ChangedCssPropertyVec);
104
impl_vec_clone!(
105
    ChangedCssProperty,
106
    ChangedCssPropertyVec,
107
    ChangedCssPropertyVecDestructor
108
);
109
impl_vec_partialeq!(ChangedCssProperty, ChangedCssPropertyVec);
110

            
111
/// Focus state change for restyle operations
112
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113
pub 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)]
122
pub 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)]
131
pub 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)]
140
pub 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

            
163
impl RestyleResult {
164
    /// Returns true if any changes occurred
165
19
    #[must_use] pub fn has_changes(&self) -> bool {
166
19
        !self.changed_nodes.is_empty()
167
19
    }
168

            
169
    /// Merge another `RestyleResult` into this one
170
25
    pub fn merge(&mut self, other: Self) {
171
46
        for (node_id, changes) in other.changed_nodes {
172
21
            self.changed_nodes.entry(node_id).or_default().extend(changes);
173
21
        }
174
25
        self.needs_layout = self.needs_layout || other.needs_layout;
175
25
        self.needs_display_list = self.needs_display_list || other.needs_display_list;
176
25
        self.gpu_only_changes = self.gpu_only_changes && other.gpu_only_changes;
177
        // Keep the highest (most expensive) scope
178
25
        if other.max_relayout_scope > self.max_relayout_scope {
179
1
            self.max_relayout_scope = other.max_relayout_scope;
180
24
        }
181
25
    }
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)]
190
pub 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

            
213
impl fmt::Debug for StyledNodeState {
214
2
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
215
2
        let mut v = Vec::new();
216
2
        if self.hover {
217
1
            v.push("hover");
218
1
        }
219
2
        if self.active {
220
            v.push("active");
221
2
        }
222
2
        if self.focused {
223
            v.push("focused");
224
2
        }
225
2
        if self.disabled {
226
            v.push("disabled");
227
2
        }
228
2
        if self.checked {
229
            v.push("checked");
230
2
        }
231
2
        if self.focus_within {
232
            v.push("focus_within");
233
2
        }
234
2
        if self.visited {
235
            v.push("visited");
236
2
        }
237
2
        if self.backdrop {
238
            v.push("backdrop");
239
2
        }
240
2
        if self.dragging {
241
            v.push("dragging");
242
2
        }
243
2
        if self.drag_over {
244
1
            v.push("drag_over");
245
1
        }
246
2
        if v.is_empty() {
247
1
            v.push("normal");
248
1
        }
249
2
        write!(f, "{v:?}")
250
2
    }
251
}
252

            
253
impl StyledNodeState {
254
    /// Creates a new state with all states set to false (normal state).
255
35691
    #[must_use] pub const fn new() -> Self {
256
35691
        Self {
257
35691
            hover: false,
258
35691
            active: false,
259
35691
            focused: false,
260
35691
            disabled: false,
261
35691
            checked: false,
262
35691
            focus_within: false,
263
35691
            visited: false,
264
35691
            backdrop: false,
265
35691
            dragging: false,
266
35691
            drag_over: false,
267
35691
        }
268
35691
    }
269

            
270
    /// Check if a specific pseudo-state is active
271
603
    #[must_use] pub const fn has_state(&self, state_type: u8) -> bool {
272
603
        match state_type {
273
3
            0 => true, // Normal is always active
274
11
            1 => self.hover,
275
11
            2 => self.active,
276
11
            3 => self.focused,
277
11
            4 => self.disabled,
278
11
            5 => self.checked,
279
11
            6 => self.focus_within,
280
11
            7 => self.visited,
281
11
            8 => self.backdrop,
282
11
            9 => self.dragging,
283
11
            10 => self.drag_over,
284
490
            _ => false,
285
        }
286
603
    }
287

            
288
    /// Returns true if no special state is active (just normal)
289
52052239
    #[must_use] pub const fn is_normal(&self) -> bool {
290
52052239
        !self.hover
291
52049934
            && !self.active
292
52049932
            && !self.focused
293
52049429
            && !self.disabled
294
52049428
            && !self.checked
295
52049427
            && !self.focus_within
296
52049426
            && !self.visited
297
52049425
            && !self.backdrop
298
52049424
            && !self.dragging
299
52049423
            && !self.drag_over
300
52052239
    }
301

            
302
    /// Create from `PseudoStateFlags`
303
5
    #[must_use] pub const fn from_pseudo_state_flags(flags: &azul_css::dynamic_selector::PseudoStateFlags) -> Self {
304
5
        Self {
305
5
            hover: flags.hover,
306
5
            active: flags.active,
307
5
            focused: flags.focused,
308
5
            disabled: flags.disabled,
309
5
            checked: flags.checked,
310
5
            focus_within: flags.focus_within,
311
5
            visited: flags.visited,
312
5
            backdrop: flags.backdrop,
313
5
            dragging: flags.dragging,
314
5
            drag_over: flags.drag_over,
315
5
        }
316
5
    }
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)]
326
pub struct StyledNode {
327
    /// Current state of this styled node (used later for caching the style / layout)
328
    pub styled_node_state: StyledNodeState,
329
}
330

            
331
impl_option!(
332
    StyledNode,
333
    OptionStyledNode,
334
    copy = false,
335
    [Debug, Clone, PartialEq, Eq, PartialOrd]
336
);
337

            
338
impl_vec!(StyledNode, StyledNodeVec, StyledNodeVecDestructor, StyledNodeVecDestructorType, StyledNodeVecSlice, OptionStyledNode);
339
impl_vec_mut!(StyledNode, StyledNodeVec);
340
impl_vec_debug!(StyledNode, StyledNodeVec);
341
impl_vec_partialord!(StyledNode, StyledNodeVec);
342
impl_vec_clone!(StyledNode, StyledNodeVec, StyledNodeVecDestructor);
343
impl_vec_partialeq!(StyledNode, StyledNodeVec);
344

            
345
impl StyledNodeVec {
346
    /// Returns an immutable container reference for indexed access.
347
38455446
    #[must_use] pub fn as_container(&self) -> NodeDataContainerRef<'_, StyledNode> {
348
38455446
        NodeDataContainerRef {
349
38455446
            internal: self.as_ref(),
350
38455446
        }
351
38455446
    }
352
    /// Returns a mutable container reference for indexed access.
353
526
    pub fn as_container_mut(&mut self) -> NodeDataContainerRefMut<'_, StyledNode> {
354
526
        NodeDataContainerRefMut {
355
526
            internal: self.as_mut(),
356
526
        }
357
526
    }
358
}
359

            
360
#[test]
361
#[allow(clippy::used_underscore_binding)] // intentional `_`-prefix (FFI/api.json pub field, or cfg-gated binding); access is deliberate
362
1
fn test_css_styling_with_nested_divs() {
363
1
    let s = "
364
1
        html, body, p {
365
1
            margin: 0;
366
1
            padding: 0;
367
1
        }
368
1
        #div1 {
369
1
            border: solid black;
370
1
            height: 2in;
371
1
            position: absolute;
372
1
            top: 1in;
373
1
            width: 3in;
374
1
        }
375
1
        div div {
376
1
            background: blue;
377
1
            height: 1in;
378
1
            position: fixed;
379
1
            width: 1in;
380
1
        }
381
1
    ";
382

            
383
1
    let css = azul_css::parser2::new_from_str(s);
384
1
    let mut _styled_dom = Dom::create_body()
385
1
        .with_children(
386
1
            vec![Dom::create_div()
387
1
                .with_ids_and_classes(
388
1
                    vec![crate::dom::IdOrClass::Id("div1".to_string().into())].into(),
389
                )
390
1
                .with_children(vec![Dom::create_div()].into())]
391
1
            .into(),
392
        );
393
1
    _styled_dom.add_component_css(css.0);
394
1
}
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]
404
1
fn test_recompute_preserves_hot_flag_has_background() {
405
    use azul_css::compact_cache::HOT_FLAG_HAS_BACKGROUND;
406

            
407
1
    let css_str = "
408
1
        body { margin: 0; padding: 0; }
409
1
        .painted { background: red; width: 100px; height: 100px; }
410
1
    ";
411
1
    let css = azul_css::parser2::new_from_str(css_str).0;
412

            
413
1
    let mut dom = Dom::create_body().with_children(
414
1
        vec![Dom::create_div().with_class("painted".to_string().into())].into(),
415
    );
416
1
    let mut styled = StyledDom::create(&mut dom, css);
417

            
418
    // Frame 1: find the painted node by walking its hot_flags.
419
1
    let any_bg_frame1 = {
420
1
        let cache = styled
421
1
            .css_property_cache
422
1
            .ptr
423
1
            .compact_cache
424
1
            .as_ref()
425
1
            .expect("compact_cache populated by create_from_compact_dom");
426
1
        (0..styled.node_hierarchy.as_ref().len())
427
2
            .any(|i| cache.tier2_cold[i].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0)
428
    };
429
1
    assert!(
430
1
        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
1
    styled.recompute_inheritance_and_compact_cache();
438

            
439
1
    let any_bg_frame2 = {
440
1
        let cache = styled
441
1
            .css_property_cache
442
1
            .ptr
443
1
            .compact_cache
444
1
            .as_ref()
445
1
            .expect("compact_cache rebuilt by recompute_inheritance_and_compact_cache");
446
1
        (0..styled.node_hierarchy.as_ref().len())
447
2
            .any(|i| cache.tier2_cold[i].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0)
448
    };
449
1
    assert!(
450
1
        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
1
}
458

            
459
/// Calculated hash of a font-family
460
#[derive(Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
461
pub struct StyleFontFamilyHash(pub u64);
462

            
463
impl ::core::fmt::Debug for StyleFontFamilyHash {
464
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
465
        write!(f, "StyleFontFamilyHash({})", self.0)
466
    }
467
}
468

            
469
impl StyleFontFamilyHash {
470
    /// Computes a 64-bit hash of a font family for cache lookups.
471
22
    #[must_use] pub fn new(family: &StyleFontFamily) -> Self {
472
        use core::hash::Hasher;
473
22
        let mut hasher = crate::hash::DefaultHasher::new();
474
22
        family.hash(&mut hasher);
475
22
        Self(hasher.finish())
476
22
    }
477
}
478

            
479
/// Calculated hash of a font-family
480
#[derive(Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
481
pub struct StyleFontFamiliesHash(pub u64);
482

            
483
impl ::core::fmt::Debug for StyleFontFamiliesHash {
484
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
485
        write!(f, "StyleFontFamiliesHash({})", self.0)
486
    }
487
}
488

            
489
impl StyleFontFamiliesHash {
490
    /// Computes a 64-bit hash of multiple font families for cache lookups.
491
54
    #[must_use] pub fn new(families: &[StyleFontFamily]) -> Self {
492
        use core::hash::Hasher;
493
54
        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
54
        families.len().hash(&mut hasher);
498
4101
        for f in families {
499
4047
            f.hash(&mut hasher);
500
4047
        }
501
54
        Self(hasher.finish())
502
54
    }
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)]
548
pub 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

            
554
impl fmt::Debug for NodeHierarchyItemId {
555
52
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
556
52
        match self.into_crate_internal() {
557
37
            Some(n) => write!(f, "Some(NodeId({n}))"),
558
15
            None => write!(f, "None"),
559
        }
560
52
    }
561
}
562

            
563
impl fmt::Display for NodeHierarchyItemId {
564
4
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
565
4
        write!(f, "{self:?}")
566
4
    }
567
}
568

            
569
impl 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
2379
    #[must_use] pub const fn from_raw(value: usize) -> Self {
581
2379
        Self { inner: value }
582
2379
    }
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
19
    #[must_use] pub const fn into_raw(&self) -> usize {
591
19
        self.inner
592
19
    }
593
}
594

            
595
impl_option!(
596
    NodeHierarchyItemId,
597
    OptionNodeHierarchyItemId,
598
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
599
);
600

            
601
impl_vec!(NodeHierarchyItemId, NodeHierarchyItemIdVec, NodeHierarchyItemIdVecDestructor, NodeHierarchyItemIdVecDestructorType, NodeHierarchyItemIdVecSlice, OptionNodeHierarchyItemId);
602
impl_vec_mut!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
603
impl_vec_debug!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
604
impl_vec_ord!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
605
impl_vec_eq!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
606
impl_vec_hash!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
607
impl_vec_partialord!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
608
impl_vec_clone!(NodeHierarchyItemId, NodeHierarchyItemIdVec, NodeHierarchyItemIdVecDestructor);
609
impl_vec_partialeq!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
610

            
611
impl NodeHierarchyItemId {
612
    /// Decodes to `Option<NodeId>` (0 = None, n > 0 = Some(NodeId(n-1))).
613
    #[inline]
614
91630152
    #[must_use] pub const fn into_crate_internal(&self) -> Option<NodeId> {
615
91630152
        NodeId::from_usize(self.inner)
616
91630152
    }
617

            
618
    /// Encodes from `Option<NodeId>` (None → 0, Some(NodeId(n)) → n+1).
619
    #[inline]
620
1446858
    #[must_use] pub const fn from_crate_internal(t: Option<NodeId>) -> Self {
621
1446858
        Self {
622
1446858
            inner: NodeId::into_raw(&t),
623
1446858
        }
624
1446858
    }
625
}
626

            
627
impl From<Option<NodeId>> for NodeHierarchyItemId {
628
    #[inline]
629
20275
    fn from(opt: Option<NodeId>) -> Self {
630
20275
        Self::from_crate_internal(opt)
631
20275
    }
632
}
633

            
634
impl From<NodeHierarchyItemId> for Option<NodeId> {
635
    #[inline]
636
1
    fn from(id: NodeHierarchyItemId) -> Self {
637
1
        id.into_crate_internal()
638
1
    }
639
}
640

            
641
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
642
#[repr(C)]
643
pub struct NodeHierarchyItem {
644
    pub parent: usize,
645
    pub previous_sibling: usize,
646
    pub next_sibling: usize,
647
    pub last_child: usize,
648
}
649

            
650
impl_option!(
651
    NodeHierarchyItem,
652
    OptionNodeHierarchyItem,
653
    [Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash]
654
);
655

            
656
impl NodeHierarchyItem {
657
    /// Creates a zeroed hierarchy item (no parent, siblings, or children).
658
6
    #[must_use] pub const fn zeroed() -> Self {
659
6
        Self {
660
6
            parent: 0,
661
6
            previous_sibling: 0,
662
6
            next_sibling: 0,
663
6
            last_child: 0,
664
6
        }
665
6
    }
666
}
667

            
668
impl From<Node> for NodeHierarchyItem {
669
603393
    fn from(node: Node) -> Self {
670
603393
        Self {
671
603393
            parent: NodeId::into_raw(&node.parent),
672
603393
            previous_sibling: NodeId::into_raw(&node.previous_sibling),
673
603393
            next_sibling: NodeId::into_raw(&node.next_sibling),
674
603393
            last_child: NodeId::into_raw(&node.last_child),
675
603393
        }
676
603393
    }
677
}
678

            
679
impl NodeHierarchyItem {
680
    /// Returns the parent node ID, if any.
681
24894888
    #[must_use] pub const fn parent_id(&self) -> Option<NodeId> {
682
24894888
        NodeId::from_usize(self.parent)
683
24894888
    }
684
    /// Returns the previous sibling node ID, if any.
685
3433
    #[must_use] pub const fn previous_sibling_id(&self) -> Option<NodeId> {
686
3433
        NodeId::from_usize(self.previous_sibling)
687
3433
    }
688
    /// Returns the next sibling node ID, if any.
689
2474542
    #[must_use] pub const fn next_sibling_id(&self) -> Option<NodeId> {
690
2474542
        NodeId::from_usize(self.next_sibling)
691
2474542
    }
692
    /// Returns the first child node ID (`current_node_id` + 1 if has children).
693
5318214
    #[must_use] pub fn first_child_id(&self, current_node_id: NodeId) -> Option<NodeId> {
694
5318214
        self.last_child_id().map(|_| current_node_id + 1)
695
5318214
    }
696
    /// Returns the last child node ID, if any.
697
5376385
    #[must_use] pub const fn last_child_id(&self) -> Option<NodeId> {
698
5376385
        NodeId::from_usize(self.last_child)
699
5376385
    }
700
}
701

            
702
impl_vec!(NodeHierarchyItem, NodeHierarchyItemVec, NodeHierarchyItemVecDestructor, NodeHierarchyItemVecDestructorType, NodeHierarchyItemVecSlice, OptionNodeHierarchyItem);
703
impl_vec_mut!(NodeHierarchyItem, NodeHierarchyItemVec);
704
impl_vec_debug!(AzNode, NodeHierarchyItemVec);
705
impl_vec_partialord!(AzNode, NodeHierarchyItemVec);
706
impl_vec_clone!(
707
    NodeHierarchyItem,
708
    NodeHierarchyItemVec,
709
    NodeHierarchyItemVecDestructor
710
);
711
impl_vec_partialeq!(AzNode, NodeHierarchyItemVec);
712

            
713
impl NodeHierarchyItemVec {
714
    /// Returns an immutable container reference for indexed access.
715
9983423
    #[must_use] pub fn as_container(&self) -> NodeDataContainerRef<'_, NodeHierarchyItem> {
716
9983423
        NodeDataContainerRef {
717
9983423
            internal: self.as_ref(),
718
9983423
        }
719
9983423
    }
720
    /// Returns a mutable container reference for indexed access.
721
1069
    pub fn as_container_mut(&mut self) -> NodeDataContainerRefMut<'_, NodeHierarchyItem> {
722
1069
        NodeDataContainerRefMut {
723
1069
            internal: self.as_mut(),
724
1069
        }
725
1069
    }
726
}
727

            
728
impl NodeDataContainerRef<'_, NodeHierarchyItem> {
729
    /// Returns the number of descendant nodes under the given parent.
730
    #[inline]
731
2833
    #[must_use] pub fn subtree_len(&self, parent_id: NodeId) -> usize {
732
2833
        let self_item_index = parent_id.index();
733
2833
        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
2833
        next_item_index.saturating_sub(self_item_index).saturating_sub(1)
737
2833
    }
738
}
739

            
740
#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
741
#[repr(C)]
742
pub struct ParentWithNodeDepth {
743
    pub depth: usize,
744
    pub node_id: NodeHierarchyItemId,
745
}
746

            
747
impl_option!(
748
    ParentWithNodeDepth,
749
    OptionParentWithNodeDepth,
750
    [Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash]
751
);
752

            
753
impl 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

            
764
impl_vec!(ParentWithNodeDepth, ParentWithNodeDepthVec, ParentWithNodeDepthVecDestructor, ParentWithNodeDepthVecDestructorType, ParentWithNodeDepthVecSlice, OptionParentWithNodeDepth);
765
impl_vec_mut!(ParentWithNodeDepth, ParentWithNodeDepthVec);
766
impl_vec_debug!(ParentWithNodeDepth, ParentWithNodeDepthVec);
767
impl_vec_partialord!(ParentWithNodeDepth, ParentWithNodeDepthVec);
768
impl_vec_clone!(
769
    ParentWithNodeDepth,
770
    ParentWithNodeDepthVec,
771
    ParentWithNodeDepthVecDestructor
772
);
773
impl_vec_partialeq!(ParentWithNodeDepth, ParentWithNodeDepthVec);
774

            
775
#[derive(Debug, Clone, Copy, PartialEq, Eq, Ord, PartialOrd)]
776
#[repr(C)]
777
pub 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

            
786
impl_option!(
787
    TagIdToNodeIdMapping,
788
    OptionTagIdToNodeIdMapping,
789
    copy = false,
790
    [Debug, Clone, PartialEq, Eq, Ord, PartialOrd]
791
);
792

            
793
impl_vec!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec, TagIdToNodeIdMappingVecDestructor, TagIdToNodeIdMappingVecDestructorType, TagIdToNodeIdMappingVecSlice, OptionTagIdToNodeIdMapping);
794
impl_vec_mut!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
795
impl_vec_debug!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
796
impl_vec_partialord!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
797
impl_vec_clone!(
798
    TagIdToNodeIdMapping,
799
    TagIdToNodeIdMappingVec,
800
    TagIdToNodeIdMappingVecDestructor
801
);
802
impl_vec_partialeq!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
803

            
804
#[derive(Debug, Clone, PartialEq, PartialOrd)]
805
#[repr(C)]
806
pub 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

            
814
impl_option!(
815
    ContentGroup,
816
    OptionContentGroup,
817
    copy = false,
818
    [Debug, Clone, PartialEq, PartialOrd]
819
);
820

            
821
impl_vec!(ContentGroup, ContentGroupVec, ContentGroupVecDestructor, ContentGroupVecDestructorType, ContentGroupVecSlice, OptionContentGroup);
822
impl_vec_mut!(ContentGroup, ContentGroupVec);
823
impl_vec_debug!(ContentGroup, ContentGroupVec);
824
impl_vec_partialord!(ContentGroup, ContentGroupVec);
825
impl_vec_clone!(ContentGroup, ContentGroupVec, ContentGroupVecDestructor);
826
impl_vec_partialeq!(ContentGroup, ContentGroupVec);
827

            
828
#[derive(Debug, PartialEq, Clone)]
829
#[repr(C)]
830
pub 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
}
844
impl_option!(
845
    StyledDom,
846
    OptionStyledDom,
847
    copy = false,
848
    [Debug, Clone, PartialEq]
849
);
850

            
851
impl Default for StyledDom {
852
3117
    fn default() -> Self {
853
3117
        let root_node: NodeHierarchyItem = Node::ROOT.into();
854
3117
        let root_node_id: NodeHierarchyItemId =
855
3117
            NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO));
856
3117
        Self {
857
3117
            root: root_node_id,
858
3117
            node_hierarchy: vec![root_node].into(),
859
3117
            node_data: vec![NodeData::create_body()].into(),
860
3117
            styled_nodes: vec![StyledNode::default()].into(),
861
3117
            cascade_info: vec![CascadeInfo {
862
3117
                index_in_parent: 0,
863
3117
                is_last_child: true,
864
3117
            }]
865
3117
            .into(),
866
3117
            tag_ids_to_node_ids: Vec::new().into(),
867
3117
            non_leaf_nodes: vec![ParentWithNodeDepth {
868
3117
                depth: 0,
869
3117
                node_id: root_node_id,
870
3117
            }]
871
3117
            .into(),
872
3117
            nodes_with_window_callbacks: Vec::new().into(),
873
3117
            nodes_with_datasets: Vec::new().into(),
874
3117
            css_property_cache: CssPropertyCachePtr::new(CssPropertyCache::empty(1)),
875
3117
            dom_id: DomId::ROOT_ID,
876
3117
        }
877
3117
    }
878
}
879

            
880
/// Per-field heap-byte breakdown of a `StyledDom`.
881
#[derive(Debug, Clone, Copy, Default)]
882
pub 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

            
894
impl StyledDomMemoryReport {
895
7
    #[must_use] pub const fn total_bytes(&self) -> usize {
896
7
        self.node_hierarchy_bytes
897
7
            + self.node_data_bytes
898
7
            + self.styled_nodes_bytes
899
7
            + self.cascade_info_bytes
900
7
            + self.tag_ids_bytes
901
7
            + self.non_leaf_nodes_bytes
902
7
            + self.callback_vecs_bytes
903
7
            + self.css_property_cache.total_bytes()
904
7
    }
905
}
906

            
907
impl StyledDom {
908
    /// Approximate heap bytes retained by this `StyledDom`, broken out by field.
909
3
    #[must_use] pub fn memory_report(&self) -> StyledDomMemoryReport {
910
3
        let n = self.node_data.len();
911
        StyledDomMemoryReport {
912
3
            node_count: n,
913
3
            node_hierarchy_bytes: size_of_val(self.node_hierarchy.as_ref()),
914
            node_data_bytes: {
915
3
                let base = n * size_of::<NodeData>();
916
                // NodeData contains inline Vecs (callbacks, css_props, datasets)
917
                // that have their own heap allocations. Approximate:
918
3
                let mut inner = 0usize;
919
54
                for nd in self.node_data.as_ref() {
920
54
                    inner += nd.get_callbacks().len() * 64; // rough per-callback
921
54
                    // Each rule = path + decls Vec + conditions Vec + priority byte.
922
54
                    // Approximate at 64 bytes per rule + the heap for declarations.
923
54
                    inner += nd.style.rules.as_ref().len() * 64;
924
54
                }
925
3
                base + inner
926
            },
927
3
            styled_nodes_bytes: n * size_of::<StyledNode>(),
928
3
            cascade_info_bytes: n * size_of::<CascadeInfo>(),
929
3
            tag_ids_bytes: size_of_val(self.tag_ids_to_node_ids.as_ref()),
930
3
            non_leaf_nodes_bytes: size_of_val(self.non_leaf_nodes.as_ref()),
931
            callback_vecs_bytes:
932
3
                self.nodes_with_window_callbacks.as_ref().len() * 8
933
3
                + self.nodes_with_datasets.as_ref().len() * 8,
934
3
            css_property_cache: self.css_property_cache.ptr.memory_breakdown(),
935
        }
936
3
    }
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
32491
    pub fn create(dom: &mut Dom, css: Css) -> Self {
945
        use core::mem;
946

            
947
32491
        let mut swap_dom = Dom::create_body();
948
32491
        mem::swap(dom, &mut swap_dom);
949

            
950
32491
        let compact_dom: CompactDom = swap_dom.into();
951
32491
        let node_hierarchy: NodeHierarchyItemVec = compact_dom
952
32491
            .node_hierarchy
953
32491
            .as_ref()
954
32491
            .internal
955
32491
            .iter()
956
599040
            .map(|i| (*i).into())
957
32491
            .collect::<Vec<NodeHierarchyItem>>()
958
32491
            .into();
959

            
960
32491
        Self::create_from_compact_dom(compact_dom, css, node_hierarchy)
961
32491
    }
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
2934
    #[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
2934
        let mut combined_rules: Vec<azul_css::css::CssRuleBlock> = Vec::new();
977
2934
        let mut combined_keyframes: Vec<azul_css::css::Keyframes> = Vec::new();
978
2934
        let css_entries = fast_dom.css.into_library_owned_vec();
979
        {
980
2934
            let hierarchy = fast_dom.node_hierarchy.as_container();
981
5703
            for mut css_with_id in css_entries {
982
                // Keyframes are name-global (no scoping): collect before the
983
                // rules are consumed. Later definitions win at resolve time.
984
2769
                combined_keyframes
985
2769
                    .extend(core::mem::take(&mut css_with_id.css.keyframes).into_library_owned_vec());
986
2769
                let owner = css_with_id.node_id;
987
2769
                let end = if owner < hierarchy.len() {
988
2769
                    owner + hierarchy.subtree_len(NodeId::new(owner))
989
                } else {
990
                    owner
991
                };
992
7298
                for mut rule in css_with_id.css.rules.into_library_owned_vec() {
993
7296
                    // Bare-declaration wrappers (INLINE priority) stay
994
7296
                    // node-only; a stylesheet's `* { ... }` (AUTHOR/UA
995
7296
                    // priority) scopes to the whole subtree. See
996
7296
                    // push_front_scope_for.
997
7296
                    let node_only =
998
7296
                        rule.priority >= azul_css::css::rule_priority::INLINE;
999
7296
                    rule.path.push_front_scope_for(owner, end, node_only);
7296
                    combined_rules.push(rule);
7296
                }
            }
        }
2934
        let combined_css = if combined_rules.is_empty() && combined_keyframes.is_empty() {
475
            Css::empty()
        } else {
2459
            let mut css = Css::new(combined_rules);
2459
            css.keyframes = combined_keyframes.into();
2459
            css
        };
        // 2. Convert NodeHierarchyItemVec → NodeHierarchy (Vec<Node>)
        //    for cascade tree computation
2934
        let node_hierarchy_items = fast_dom.node_hierarchy;
2934
        let nodes: Vec<Node> = node_hierarchy_items.as_ref()
2934
            .iter()
2934
            .map(|item| Node {
87551
                parent: NodeId::from_usize(item.parent),
87551
                previous_sibling: NodeId::from_usize(item.previous_sibling),
87551
                next_sibling: NodeId::from_usize(item.next_sibling),
87551
                last_child: NodeId::from_usize(item.last_child),
87551
            })
2934
            .collect();
2934
        let node_hierarchy_internal = NodeHierarchy { internal: nodes };
        // 3. Build CompactDom from the flat arenas (no conversion needed)
2934
        let node_data_vec = fast_dom.node_data.into_library_owned_vec();
2934
        let compact_dom = CompactDom {
2934
            node_hierarchy: node_hierarchy_internal,
2934
            node_data: NodeDataContainer { internal: node_data_vec },
2934
            root: NodeId::ZERO,
2934
        };
        // 4. Delegate to create() which handles cascade, UA CSS, etc.
        //    We need a mutable Dom to pass to create(), but we already have CompactDom.
        //    Instead, inline the cascade logic from create() with our CompactDom.
2934
        Self::create_from_compact_dom(compact_dom, combined_css, node_hierarchy_items)
2934
    }
    /// Internal: creates `StyledDom` from a `CompactDom` + CSS + pre-built hierarchy items.
    /// Shared by both the Slow path (create → `convert_dom_into_compact_dom` → this)
    /// and the Fast path (`create_from_fast_dom` → this).
    #[allow(clippy::similar_names)] // domain-standard coordinate/control-point names
    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
35425
    fn create_from_compact_dom(
35425
        compact_dom: CompactDom,
35425
        mut css: Css,
35425
        node_hierarchy: NodeHierarchyItemVec,
35425
    ) -> Self {
        use crate::dom::EventFilter;
        static CASCADE_BREAKDOWN: crate::sync::OnceLock<bool> = crate::sync::OnceLock::new();
35425
        let cascade_dbg = *CASCADE_BREAKDOWN.get_or_init(crate::profile::memory_enabled);
35425
        let node_count = compact_dom.len();
35425
        let non_leaf_nodes = compact_dom
35425
            .node_hierarchy
35425
            .as_ref()
35425
            .get_parents_sorted_by_depth();
35425
        let mut styled_nodes = vec![
35425
            StyledNode {
35425
                styled_node_state: StyledNodeState::new()
35425
            };
35425
            node_count
        ];
35425
        let mut css_property_cache = CssPropertyCache::empty(compact_dom.node_data.len());
35425
        let html_tree = construct_html_cascade_tree(
35425
            &compact_dom.node_hierarchy.as_ref(),
35425
            &non_leaf_nodes[..],
35425
            &compact_dom.node_data.as_ref(),
        );
35425
        let non_leaf_nodes = non_leaf_nodes
35425
            .iter()
35425
            .map(|(depth, node_id)| ParentWithNodeDepth {
357341
                depth: *depth,
357341
                node_id: NodeHierarchyItemId::from_crate_internal(Some(*node_id)),
357341
            })
35425
            .collect::<Vec<_>>();
35425
        let non_leaf_nodes: ParentWithNodeDepthVec = non_leaf_nodes.into();
35425
        let _restyle_tag_ids = css_property_cache.restyle(
35425
            &mut css,
35425
            &compact_dom.node_data.as_ref(),
35425
            &node_hierarchy,
35425
            &non_leaf_nodes,
35425
            &html_tree.as_ref(),
        );
        // Retain the author stylesheet on the cache (this used to `drop(css)` to
        // save ~500 KiB, but that made runtime-inserted nodes unstyleable: the
        // rules were gone, so nothing could ever re-run the cascade for them —
        // see e2e/bug-inserted-node-no-author-css.json).
35425
        css_property_cache.retained_author_css = css;
        // Apply UA defaults + compute inherited values so consumers that
        // read `css_property_cache.computed_values` (the web/HTML
        // renderer in `dll/src/web/html_render.rs`) see resolved
        // properties. The compact cache below stores the same info in
        // a different layout for the desktop renderer; computed_values
        // is the "tall" form that the web renderer's CSS emitter
        // (`emit_css_from_cache`) walks per node.
35425
        css_property_cache.apply_ua_css(compact_dom.node_data.as_ref().internal);
35425
        css_property_cache.compute_inherited_values(
35425
            node_hierarchy.as_container().internal,
35425
            compact_dom.node_data.as_ref().internal,
        );
35425
        let prev_font_hashes: Vec<u64> = css_property_cache.compact_cache
35425
            .as_ref()
35425
            .map(|c| c.prev_font_hashes.clone())
35425
            .unwrap_or_default();
35425
        let compact = css_property_cache.build_compact_cache_with_inheritance(
35425
            compact_dom.node_data.as_ref().internal,
35425
            node_hierarchy.as_container().internal,
35425
            &prev_font_hashes,
        );
35425
        css_property_cache.compact_cache = Some(compact);
35425
        let pre_prune = if cascade_dbg {
            Some(css_property_cache.memory_breakdown())
35425
        } else { None };
35425
        css_property_cache.prune_compact_normal_props();
35425
        if let Some(pre) = pre_prune {
            let post = css_property_cache.memory_breakdown();
            #[cfg(feature = "std")]
            eprintln!("[PRUNE] css_props {} → {} KiB  cascaded {} → {} KiB  (saved {} KiB)",
                pre.css_props_bytes / 1024, post.css_props_bytes / 1024,
                pre.cascaded_props_bytes / 1024, post.cascaded_props_bytes / 1024,
                (pre.total_bytes().saturating_sub(post.total_bytes())) / 1024);
            #[cfg(not(feature = "std"))]
            let _ = post;
35425
        }
35425
        let tag_ids = css_property_cache.generate_tag_ids(
35425
            &compact_dom.node_data.as_ref(),
35425
            &node_hierarchy,
        );
35425
        if cascade_dbg {
            let bd = css_property_cache.memory_breakdown();
            #[cfg(feature = "std")]
            eprintln!("[CASCADE] {} nodes  cascaded_props={} KiB  css_props={} KiB  compact={} KiB  computed={} KiB  total={} KiB",
                node_count,
                bd.cascaded_props_bytes / 1024, bd.css_props_bytes / 1024,
                bd.compact_cache_bytes / 1024, bd.computed_values_bytes / 1024,
                bd.total_bytes() / 1024);
            #[cfg(not(feature = "std"))]
            let _ = bd;
35425
        }
        // Collect callback/dataset nodes in a single pass (avoids 3 separate 50K scans).
        // For XHTML-parsed DOMs with no callbacks, this early-exits immediately.
35425
        let has_any_callbacks = compact_dom.node_data.as_ref().internal.iter()
221665
            .any(|c| !c.get_callbacks().is_empty() || c.get_dataset().is_some());
35425
        let (nodes_with_window_callbacks, nodes_with_datasets) = if has_any_callbacks {
14795
            let mut win_cbs = Vec::new();
14795
            let mut datasets = Vec::new();
492371
            for (node_id, c) in compact_dom.node_data.as_ref().internal.iter().enumerate() {
492371
                let cbs = c.get_callbacks();
492371
                let has_dataset = c.get_dataset().is_some();
492371
                if !cbs.is_empty() || has_dataset {
93973
                    datasets.push(NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_id))));
398398
                }
630223
                for cb in cbs {
137852
                    if let EventFilter::Window(_) = cb.event {
                        win_cbs.push(NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_id))));
                        break;
137852
                    }
                }
            }
14795
            (win_cbs, datasets)
        } else {
20630
            (Vec::new(), Vec::new())
        };
35425
        let mut styled_dom = Self {
35425
            root: NodeHierarchyItemId::from_crate_internal(Some(compact_dom.root)),
35425
            node_hierarchy,
35425
            node_data: compact_dom.node_data.internal.into(),
35425
            cascade_info: html_tree.internal.into(),
35425
            styled_nodes: styled_nodes.into(),
35425
            tag_ids_to_node_ids: tag_ids.into(),
35425
            nodes_with_window_callbacks: nodes_with_window_callbacks.into(),
35425
            nodes_with_datasets: nodes_with_datasets.into(),
35425
            non_leaf_nodes,
35425
            css_property_cache: CssPropertyCachePtr::new(css_property_cache),
35425
            dom_id: DomId::ROOT_ID,
35425
        };
        #[cfg(feature = "table_layout")]
        if let Err(_e) = crate::dom_table::generate_anonymous_table_elements(&mut styled_dom) {
        }
35425
        styled_dom
35425
    }
    /// Creates a `StyledDom` from a recursive Dom tree with deferred CSS.
    ///
    /// This is the Phase 7.2 entry point: the layout callback returns a recursive
    /// `Dom` with `css: Vec<Css>` on each node. This function:
    ///
    /// 1. Collects all CSS objects from the recursive tree
    /// 2. Flattens the Dom into contiguous arrays (`CompactDom`)
    /// 3. Merges all CSS objects and runs a single cascade pass
    /// 4. Runs `apply_ua_css` → `compute_inherited_values` → `build_compact_cache`
    /// 5. Generates anonymous table elements
16727
    #[must_use] pub fn create_from_dom(mut dom: Dom) -> Self {
        use azul_css::css::Css;
        // #47: scope each node's inline css to its subtree BEFORE collecting, so a
        // non-root node's with_css cannot leak to the whole tree. Uses the same
        // pre-order ids the flatten (convert_dom_into_compact_dom) will assign;
        // needs estimated_total_children populated first.
16727
        dom.fixup_children_estimated();
16727
        let mut next_scope_id = 0usize;
16727
        scope_inline_css(&mut dom, &mut next_scope_id);
        // 1. Collect all CSS objects from the recursive Dom tree (now scoped)
16727
        let mut all_css = Vec::new();
16727
        collect_css_from_dom(&dom, &mut all_css);
        // 2. Merge all CSS objects into one combined Css
16727
        let mut combined_css = if all_css.is_empty() {
15732
            Css::empty()
        } else {
995
            let mut combined_rules: Vec<azul_css::css::CssRuleBlock> = Vec::new();
995
            let mut combined_keyframes: Vec<azul_css::css::Keyframes> = Vec::new();
7745
            for css in all_css {
6750
                combined_rules.extend(css.rules.into_library_owned_vec());
6750
                combined_keyframes.extend(css.keyframes.into_library_owned_vec());
6750
            }
995
            let mut css = Css::new(combined_rules);
995
            css.keyframes = combined_keyframes.into();
995
            css
        };
        // 3. Strip CSS from all Dom nodes before flattening
        //    (CSS is already collected, don't need it in the flat tree)
16727
        strip_css_from_dom(&mut dom);
        // 4. Use existing StyledDom::create to flatten + cascade
16727
        Self::create(&mut dom, combined_css)
16727
    }
    /// Appends another `StyledDom` as a child to the `self.root`
    /// without re-styling the DOM itself
235
    pub fn append_child(&mut self, other: Self) {
235
        let self_root_id = self.root.into_crate_internal().unwrap_or(NodeId::ZERO);
235
        let current_root_children_count = self_root_id
235
            .az_children(&self.node_hierarchy.as_container())
235
            .count();
235
        self.append_child_with_index(other, current_root_children_count);
235
        self.finalize_non_leaf_nodes();
235
    }
    /// Optimized version of `append_child` that takes the child index directly
    /// instead of counting existing children (O(1) instead of O(n))
242
    pub fn append_child_with_index(&mut self, mut other: Self, child_index: usize) {
        // shift all the node ids in other by self.len()
242
        let self_len = self.node_hierarchy.as_ref().len();
242
        let other_len = other.node_hierarchy.as_ref().len();
242
        let self_root_id = self.root.into_crate_internal().unwrap_or(NodeId::ZERO);
242
        let other_root_id = other.root.into_crate_internal().unwrap_or(NodeId::ZERO);
        // Use provided index instead of counting children
242
        other.cascade_info.as_mut()[other_root_id.index()].index_in_parent =
242
            u32::try_from(child_index).unwrap_or(u32::MAX);
242
        other.cascade_info.as_mut()[other_root_id.index()].is_last_child = true;
242
        self.cascade_info.append(&mut other.cascade_info);
        // adjust node hierarchy
384
        for other in other.node_hierarchy.as_mut().iter_mut() {
384
            if other.parent != 0 {
142
                other.parent += self_len;
242
            }
384
            if other.previous_sibling != 0 {
4
                other.previous_sibling += self_len;
380
            }
384
            if other.next_sibling != 0 {
4
                other.next_sibling += self_len;
380
            }
384
            if other.last_child != 0 {
138
                other.last_child += self_len;
246
            }
        }
242
        other.node_hierarchy.as_container_mut()[other_root_id].parent =
242
            NodeId::into_raw(&Some(self_root_id));
242
        let current_last_child = self.node_hierarchy.as_container()[self_root_id].last_child_id();
242
        other.node_hierarchy.as_container_mut()[other_root_id].previous_sibling =
242
            NodeId::into_raw(&current_last_child);
242
        if let Some(current_last) = current_last_child {
161
            if self.node_hierarchy.as_container_mut()[current_last]
161
                .next_sibling_id()
161
                .is_some()
            {
                self.node_hierarchy.as_container_mut()[current_last].next_sibling +=
                    other_root_id.index() + other_len;
161
            } else {
161
                self.node_hierarchy.as_container_mut()[current_last].next_sibling =
161
                    NodeId::into_raw(&Some(NodeId::new(self_len + other_root_id.index())));
161
            }
81
        }
242
        self.node_hierarchy.as_container_mut()[self_root_id].last_child =
242
            NodeId::into_raw(&Some(NodeId::new(self_len + other_root_id.index())));
242
        self.node_hierarchy.append(&mut other.node_hierarchy);
242
        self.node_data.append(&mut other.node_data);
242
        self.styled_nodes.append(&mut other.styled_nodes);
242
        self.get_css_property_cache_mut()
242
            .append(other.get_css_property_cache_mut());
        // Tag IDs are globally unique (AtomicUsize counter) and never collide,
        // so we only shift node_id (which changes when DOMs are merged).
356
        for tag_id_node_id in &mut other.tag_ids_to_node_ids {
114
            tag_id_node_id.node_id.inner += self_len;
114
        }
242
        self.tag_ids_to_node_ids
242
            .append(&mut other.tag_ids_to_node_ids);
242
        for nid in &mut other.nodes_with_window_callbacks {
            nid.inner += self_len;
        }
242
        self.nodes_with_window_callbacks
242
            .append(&mut other.nodes_with_window_callbacks);
242
        for nid in &mut other.nodes_with_datasets {
            nid.inner += self_len;
        }
242
        self.nodes_with_datasets
242
            .append(&mut other.nodes_with_datasets);
        // edge case: if the other StyledDom consists of only one node
        // then it is not a parent itself
242
        if other_len != 1 {
257
            for other_non_leaf_node in &mut other.non_leaf_nodes {
138
                other_non_leaf_node.node_id.inner += self_len;
138
                other_non_leaf_node.depth += 1;
138
            }
119
            self.non_leaf_nodes.append(&mut other.non_leaf_nodes);
            // NOTE: Sorting deferred - call finalize_non_leaf_nodes() after all appends
123
        }
242
    }
    /// Call this after all `append_child_with_index` operations are complete
    /// to sort `non_leaf_nodes` by depth (required for correct rendering)
239
    pub fn finalize_non_leaf_nodes(&mut self) {
246
        self.non_leaf_nodes.sort_by(|a, b| a.depth.cmp(&b.depth));
239
    }
    /// Same as `append_child()`, but as a builder method
1
    #[must_use] pub fn with_child(mut self, other: Self) -> Self {
1
        self.append_child(other);
1
        self
1
    }
    /// Sets the context menu for the root node
2
    pub fn set_context_menu(&mut self, context_menu: Menu) {
2
        if let Some(root_id) = self.root.into_crate_internal() {
2
            self.node_data.as_container_mut()[root_id].set_context_menu(context_menu);
2
        }
2
    }
    /// Builder method for setting the context menu
1
    #[must_use] pub fn with_context_menu(mut self, context_menu: Menu) -> Self {
1
        self.set_context_menu(context_menu);
1
        self
1
    }
    /// Sets the menu bar for the root node
2
    pub fn set_menu_bar(&mut self, menu_bar: Menu) {
2
        if let Some(root_id) = self.root.into_crate_internal() {
2
            self.node_data.as_container_mut()[root_id].set_menu_bar(menu_bar);
2
        }
2
    }
    /// Builder method for setting the menu bar
1
    #[must_use] pub fn with_menu_bar(mut self, menu_bar: Menu) -> Self {
1
        self.set_menu_bar(menu_bar);
1
        self
1
    }
    /// Re-compute inherited CSS values and rebuild the compact layout cache.
    ///
    /// This MUST be called after `append_child()` merges multiple `StyledDom`s.
    /// `append_child()` concatenates the CSS property caches but does NOT
    /// re-run inheritance or rebuild the compact cache. This means:
    ///
    /// 1. **Broken inheritance**: Inherited properties (`color`, `font-size`,
    ///    `direction`) from the parent DOM do not flow into appended subtrees.
    /// 2. **Stale compact cache**: The child's tier 1/2/2b entries still reflect
    ///    the child's isolated cascade, not the composed tree.
    ///
    /// Calling this method after all `append_child()` calls fixes both issues
    /// by re-running a full depth-first inheritance pass and rebuilding the
    /// compact cache from scratch on the composed tree.
1723
    pub fn recompute_inheritance_and_compact_cache(&mut self) {
        // Use the _with_inheritance variant: it does inheritance inline (via
        // parent-compact-field copy) AND populates hot_flags via
        // apply_css_property_to_compact.  The plain build_compact_cache would
        // leave HOT_FLAG_HAS_BACKGROUND / HAS_CLIP_PATH / extra_flags at 0,
        // causing renderer negative fast-paths to skip paint (regression
        // introduced by ff059052b).  No SIGABRT risk — _with_inheritance
        // never pushes to the flat cascaded_props storage.
1723
        let prev_font_hashes: Vec<u64> = self.css_property_cache
1723
            .downcast_mut()
1723
            .compact_cache
1723
            .as_ref()
1723
            .map(|c| c.prev_font_hashes.clone())
1723
            .unwrap_or_default();
1723
        let compact = self.css_property_cache
1723
            .downcast_mut()
1723
            .build_compact_cache_with_inheritance(
1723
                self.node_data.as_container().internal,
1723
                self.node_hierarchy.as_container().internal,
1723
                &prev_font_hashes,
            );
1723
        self.css_property_cache.downcast_mut().compact_cache = Some(compact);
1723
    }
    /// Re-applies CSS styles to the existing DOM structure.
    /// Grow retained author-CSS subtree scopes to cover a node just appended under
    /// `parent`. Mount/`with_css` rules carry a `Root([start, end])` scope
    /// (`push_front_scope`) that only matches nodes within a node's ORIGINAL subtree
    /// range, so a node appended afterwards falls outside every scope and
    /// `restyle_retained` cannot match it. Appending under `parent` (rightmost-spine
    /// only, so subtrees stay contiguous in the flat arena) grows `parent`'s and its
    /// ancestors' subtrees; bump the inclusive `end` of every scope that already
    /// covers `parent` out to the new node.
    #[allow(clippy::similar_names)] // new_node/parent and the p/n index locals read clearly in context
2
    pub fn extend_author_scopes_for_appended(&mut self, new_node: NodeId, parent: NodeId) {
        use azul_css::css::CssPathSelector;
2
        let p = parent.index();
2
        let n = new_node.index();
2
        let cache = self.css_property_cache.downcast_mut();
6
        for rule in cache.retained_author_css.rules.as_mut() {
6
            let mut sels = rule.path.selectors.as_ref().to_vec();
6
            let mut changed = false;
18
            for sel in &mut sels {
12
                if let CssPathSelector::Root(range) = sel {
6
                    if range.contains(p) && range.end < n {
6
                        range.end = n;
6
                        changed = true;
6
                    }
6
                }
            }
6
            if changed {
6
                rule.path.selectors = sels.into();
6
            }
        }
2
    }
    /// Re-run the author cascade from the stylesheet retained at creation /
    /// last `restyle` (`CssPropertyCache::retained_author_css`). Call after a
    /// structural DOM mutation (e.g. inserting a node) so new nodes receive
    /// author CSS; a no-op when no author stylesheet was ever attached.
    /// The PER-TICK override channel: write `user_overridden_properties`
    /// WITHOUT recomputing inheritance or the compact cache. Sound only when
    /// the caller supplies the pixels itself (the transition driver patches
    /// the display list with the interpolated value directly) — every other
    /// caller wants [`Self::restyle_user_property`]. At t=1 the override is
    /// removed and the (correctly cascaded) target shows through.
61
    pub fn set_user_property_override_fast(&mut self, node_id: &NodeId, new_properties: &[CssProperty]) {
61
        let node_count = self.node_data.as_ref().len();
61
        if node_id.index() >= node_count {
            return;
61
        }
61
        let cache = self.get_css_property_cache_mut();
61
        if cache.user_overridden_properties.len() < node_count {
            cache.user_overridden_properties.resize(node_count, Vec::new());
61
        }
122
        for new_prop in new_properties {
61
            let prop_type = new_prop.get_type();
61
            let vec = &mut cache.user_overridden_properties[node_id.index()];
61
            if new_prop.is_initial() {
1
                if let Ok(idx) = vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
1
                    vec.remove(idx);
1
                }
            } else {
60
                match vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
60
                    Ok(idx) => vec[idx].1 = new_prop.clone(),
                    Err(idx) => vec.insert(idx, (prop_type, new_prop.clone())),
                }
            }
        }
61
    }
75
    pub fn restyle_retained(&mut self) {
75
        let css = self
75
            .css_property_cache
75
            .downcast_mut()
75
            .retained_author_css
75
            .clone();
75
        if css.is_empty() {
            return;
75
        }
75
        self.restyle(css);
75
    }
77
    pub fn restyle(&mut self, mut css: Css) {
        // NOTE: the tag_ids returned by `cache.restyle` here are generated from
        // the STALE `compact_cache` (display/overflow reads) and are intentionally
        // discarded — we regenerate them below AFTER the compact cache and
        // inheritance have been recomputed (audit styled_dom.rs:1404/1426).
77
        let _stale_tag_ids = self.css_property_cache.downcast_mut().restyle(
77
            &mut css,
77
            &self.node_data.as_container(),
77
            &self.node_hierarchy,
77
            &self.non_leaf_nodes,
77
            &self.cascade_info.as_container(),
        );
        // Keep the stylesheet for later structural restyles (inserted nodes).
77
        self.css_property_cache.downcast_mut().retained_author_css = css;
        // Apply UA CSS properties before computing inheritance
77
        self.css_property_cache
77
            .downcast_mut()
77
            .apply_ua_css(self.node_data.as_container().internal);
        // Compute inherited values after restyle and apply_ua_css (resolves em, %, etc.)
77
        self.css_property_cache
77
            .downcast_mut()
77
            .compute_inherited_values(
77
                self.node_hierarchy.as_container().internal,
77
                self.node_data.as_container().internal,
            );
        // The old compact_cache was built from the pre-restyle CSS. If we do not
        // rebuild it, layout-hot properties (display/overflow/background/clip,
        // resolved font sizes) keep their stale values and the restyle silently
        // no-ops for them. Drop it, rebuild via the _with_inheritance path (which
        // repopulates hot_flags), and invalidate the cached resolved font sizes.
77
        let prev_font_hashes: Vec<u64> = self
77
            .css_property_cache
77
            .downcast_mut()
77
            .compact_cache
77
            .as_ref()
77
            .map(|c| c.prev_font_hashes.clone())
77
            .unwrap_or_default();
77
        self.css_property_cache.downcast_mut().compact_cache = None;
77
        let compact = self
77
            .css_property_cache
77
            .downcast_mut()
77
            .build_compact_cache_with_inheritance(
77
                self.node_data.as_container().internal,
77
                self.node_hierarchy.as_container().internal,
77
                &prev_font_hashes,
            );
77
        self.css_property_cache.downcast_mut().compact_cache = Some(compact);
77
        self.css_property_cache
77
            .downcast_mut()
77
            .invalidate_resolved_font_sizes();
        // Regenerate tag_ids from the freshly rebuilt compact cache so the
        // hit-test map reflects the post-restyle display/overflow values.
77
        let new_tag_ids = self.css_property_cache.downcast_mut().generate_tag_ids(
77
            &self.node_data.as_container(),
77
            &self.node_hierarchy,
        );
77
        self.tag_ids_to_node_ids = new_tag_ids.into();
77
    }
    /// Returns the total number of nodes in this `StyledDom`.
    #[inline]
560
    #[must_use] pub const fn node_count(&self) -> usize {
560
        self.node_data.len()
560
    }
    /// Returns an immutable reference to the CSS property cache.
    #[inline]
671878
    #[must_use] pub fn get_css_property_cache(&self) -> &CssPropertyCache {
671878
        &self.css_property_cache.ptr
671878
    }
    /// Returns a mutable reference to the CSS property cache.
    #[inline]
8321
    pub fn get_css_property_cache_mut(&mut self) -> &mut CssPropertyCache {
8321
        &mut self.css_property_cache.ptr
8321
    }
    /// Returns the current state (hover, active, focus) of a styled node.
    #[inline]
31
    #[must_use] pub fn get_styled_node_state(&self, node_id: &NodeId) -> StyledNodeState {
31
        self.styled_nodes.as_container()[*node_id]
31
            .styled_node_state
31
    }
    /// Updates hover state for nodes and returns changed CSS properties.
    #[must_use]
147
    pub fn restyle_nodes_hover(
147
        &mut self,
147
        nodes: &[NodeId],
147
        new_hover_state: bool,
147
    ) -> RestyleNodes {
147
        self.restyle_nodes_state(
147
            nodes,
147
            new_hover_state,
170
            |state, val| state.hover = val,
147
            azul_css::dynamic_selector::PseudoStateType::Hover,
        )
147
    }
    /// Updates active state for nodes and returns changed CSS properties.
    #[must_use]
98
    pub fn restyle_nodes_active(
98
        &mut self,
98
        nodes: &[NodeId],
98
        new_active_state: bool,
98
    ) -> RestyleNodes {
98
        self.restyle_nodes_state(
98
            nodes,
98
            new_active_state,
96
            |state, val| state.active = val,
98
            azul_css::dynamic_selector::PseudoStateType::Active,
        )
98
    }
    /// Updates focus state for nodes and returns changed CSS properties.
    #[must_use]
260
    pub fn restyle_nodes_focus(
260
        &mut self,
260
        nodes: &[NodeId],
260
        new_focus_state: bool,
260
    ) -> RestyleNodes {
260
        self.restyle_nodes_state(
260
            nodes,
260
            new_focus_state,
259
            |state, val| state.focused = val,
260
            azul_css::dynamic_selector::PseudoStateType::Focus,
        )
260
    }
    /// Generic restyle method parameterized by the state field and pseudo-state type.
505
    fn restyle_nodes_state(
505
        &mut self,
505
        nodes: &[NodeId],
505
        new_state_value: bool,
505
        set_state: impl Fn(&mut StyledNodeState, bool),
505
        pseudo_state_type: azul_css::dynamic_selector::PseudoStateType,
505
    ) -> RestyleNodes {
        // Drop any stale NodeIds that no longer index into this DOM (e.g. left
        // over from a previous, larger tree). Indexing styled_nodes / node_data
        // with an out-of-range id would panic. Filtering here keeps the
        // downstream zip with `old_node_states` aligned.
505
        let node_count = self.node_count();
505
        let nodes: Vec<NodeId> = nodes
505
            .iter()
505
            .copied()
534
            .filter(|nid| nid.index() < node_count)
505
            .collect();
        // save the old node state
505
        let old_node_states = nodes
505
            .iter()
531
            .map(|nid| {
525
                self.styled_nodes.as_container()[*nid]
525
                    .styled_node_state
525
            })
505
            .collect::<Vec<_>>();
1030
        for nid in &nodes {
525
            set_state(
525
                &mut self.styled_nodes.as_container_mut()[*nid].styled_node_state,
525
                new_state_value,
525
            );
525
        }
505
        let css_property_cache = self.get_css_property_cache();
505
        let styled_nodes = self.styled_nodes.as_container();
505
        let node_data = self.node_data.as_container();
        // scan all properties that could have changed because of addition / removal
505
        let v = nodes
505
            .iter()
505
            .zip(old_node_states.iter())
531
            .filter_map(|(node_id, old_node_state)| {
525
                let mut keys_normal: Vec<_> = CssPropertyCache::prop_types_for_state(
525
                    css_property_cache.css_props.get_slice(node_id.index()),
525
                    pseudo_state_type,
525
                ).collect();
525
                let mut keys_inherited: Vec<_> = CssPropertyCache::prop_types_for_state(
525
                    css_property_cache.cascaded_props.get_slice(node_id.index()),
525
                    pseudo_state_type,
525
                ).collect();
525
                let keys_inline: Vec<CssPropertyType> = {
                    use azul_css::dynamic_selector::DynamicSelector;
525
                    node_data[*node_id]
525
                        .style
525
                        .iter_inline_properties()
1874
                        .filter_map(|(prop, conds)| {
1824
                            let matches = conds.as_slice().iter().any(|c| {
1349
                                matches!(c, DynamicSelector::PseudoState(pst) if *pst == pseudo_state_type)
1349
                            });
1824
                            if matches {
475
                                Some(prop.get_type())
                            } else {
1349
                                None
                            }
1824
                        })
525
                        .collect()
                };
525
                let mut keys_inline_ref: Vec<_> = keys_inline.iter().collect();
525
                keys_normal.append(&mut keys_inherited);
525
                keys_normal.append(&mut keys_inline_ref);
525
                let node_properties_that_could_have_changed = keys_normal;
525
                if node_properties_that_could_have_changed.is_empty() {
50
                    return None;
475
                }
475
                let new_node_state = &styled_nodes[*node_id].styled_node_state;
475
                let node_data = &node_data[*node_id];
475
                let changes = node_properties_that_could_have_changed
475
                    .into_iter()
475
                    .filter_map(|prop| {
                        // calculate both the old and the new state
475
                        let old = css_property_cache.get_property_slow(
475
                            node_data,
475
                            node_id,
475
                            old_node_state,
475
                            prop,
                        );
475
                        let new = css_property_cache.get_property_slow(
475
                            node_data,
475
                            node_id,
475
                            new_node_state,
475
                            prop,
                        );
475
                        if old == new {
57
                            None
                        } else {
                            Some(ChangedCssProperty {
418
                                previous_state: *old_node_state,
418
                                previous_prop: old.map_or_else(|| CssProperty::auto(*prop), Clone::clone),
418
                                current_state: *new_node_state,
418
                                current_prop: new.map_or_else(|| CssProperty::auto(*prop), Clone::clone),
                            })
                        }
475
                    })
475
                    .collect::<Vec<_>>();
475
                if changes.is_empty() {
57
                    None
                } else {
418
                    Some((*node_id, changes))
                }
525
            })
505
            .collect::<Vec<_>>();
505
        v.into_iter().collect()
505
    }
    /// Unified entry point for all CSS restyle operations.
    ///
    /// This function synchronizes the `StyledNodeState` with runtime state
    /// and computes which CSS properties have changed. It determines whether
    /// layout, display list, or GPU-only updates are needed.
    ///
    /// # Arguments
    /// * `focus_changes` - Nodes gaining/losing focus
    /// * `hover_changes` - Nodes gaining/losing hover
    /// * `active_changes` - Nodes gaining/losing active (mouse down)
    ///
    /// # Returns
    /// * `RestyleResult` containing changed nodes and what needs updating
    #[must_use]
405
    pub fn restyle_on_state_change(
405
        &mut self,
405
        focus_changes: Option<FocusChange>,
405
        hover_changes: Option<HoverChange>,
405
        active_changes: Option<ActiveChange>,
405
    ) -> RestyleResult {
        // Start with GPU-only assumption; refined below as changes are analyzed.
405
        let mut result = RestyleResult {
405
            gpu_only_changes: true,
405
            ..RestyleResult::default()
405
        };
        // Helper closure to merge changes and analyze property categories
496
        let mut process_changes = |changes: RestyleNodes| {
914
            for (node_id, props) in changes {
836
                for change in &props {
418
                    let prop_type = change.current_prop.get_type();
                    // Use the granular RelayoutScope instead of the binary
                    // can_trigger_relayout(). We pass node_is_ifc_member = true
                    // conservatively: this means font/text property changes will
                    // produce IfcOnly (rather than None). Phase 2c can refine
                    // this by checking whether the node actually participates
                    // in an IFC.
418
                    let scope = prop_type.relayout_scope(/* node_is_ifc_member */ true);
                    // Track the highest scope seen
418
                    if scope > result.max_relayout_scope {
19
                        result.max_relayout_scope = scope;
399
                    }
                    // Any scope above None triggers layout
418
                    if scope != RelayoutScope::None {
19
                        result.needs_layout = true;
19
                        result.gpu_only_changes = false;
399
                    }
                    // Check if this is a GPU-only property
418
                    if !prop_type.is_gpu_only_property() {
399
                        result.gpu_only_changes = false;
399
                    }
                    // Any visual change needs display list update (unless GPU-only)
418
                    result.needs_display_list = true;
                }
418
                result.changed_nodes.entry(node_id).or_default().extend(props);
            }
496
        };
        // 1. Process focus changes
405
        if let Some(focus) = focus_changes {
228
            if let Some(old) = focus.lost_focus {
49
                let changes = self.restyle_nodes_focus(&[old], false);
49
                process_changes(changes);
182
            }
228
            if let Some(new) = focus.gained_focus {
208
                let changes = self.restyle_nodes_focus(&[new], true);
208
                process_changes(changes);
208
            }
177
        }
        // 2. Process hover changes
405
        if let Some(hover) = hover_changes {
139
            if !hover.left_nodes.is_empty() {
22
                let changes = self.restyle_nodes_hover(&hover.left_nodes, false);
22
                process_changes(changes);
117
            }
139
            if !hover.entered_nodes.is_empty() {
120
                let changes = self.restyle_nodes_hover(&hover.entered_nodes, true);
120
                process_changes(changes);
120
            }
266
        }
        // 3. Process active changes
405
        if let Some(active) = active_changes {
96
            if !active.deactivated.is_empty() {
20
                let changes = self.restyle_nodes_active(&active.deactivated, false);
20
                process_changes(changes);
77
            }
96
            if !active.activated.is_empty() {
77
                let changes = self.restyle_nodes_active(&active.activated, true);
77
                process_changes(changes);
77
            }
309
        }
        // If no changes, reset display_list flag
405
        if result.changed_nodes.is_empty() {
44
            result.needs_display_list = false;
44
            result.gpu_only_changes = false;
386
        }
        // If layout is needed, display list is also needed
405
        if result.needs_layout {
19
            result.needs_display_list = true;
19
            result.gpu_only_changes = false;
386
        }
405
        result
405
    }
    /// Overrides CSS properties for a single node from user code (typically a
    /// callback). Writes into `CssPropertyCache::user_overridden_properties`,
    /// which `get_property_slow` / `get_property_fast` / `get_computed_value`
    /// consult at higher priority than the static CSS cascade — making this
    /// the fast path for animating a handful of properties per frame.
    ///
    /// Passing `CssProperty::Initial` for a property removes any override for
    /// that type, restoring the cascaded value. Returns the set of
    /// `ChangedCssProperty` entries the caller can feed into the incremental
    /// restyle pipeline.
    #[must_use]
274
    pub fn restyle_user_property(
274
        &mut self,
274
        node_id: &NodeId,
274
        new_properties: &[CssProperty],
274
    ) -> RestyleNodes {
274
        let mut map = BTreeMap::default();
274
        if new_properties.is_empty() {
1
            return map;
273
        }
273
        let node_count = self.node_data.as_ref().len();
273
        if node_id.index() >= node_count {
1
            return map;
272
        }
272
        let node_data = self.node_data.as_container();
272
        let node_data = &node_data[*node_id];
272
        let node_states = &self.styled_nodes.as_container();
272
        let old_node_state = &node_states[*node_id].styled_node_state;
272
        let changes: Vec<ChangedCssProperty> = {
272
            let css_property_cache = self.get_css_property_cache();
272
            new_properties
272
                .iter()
272
                .filter_map(|new_prop| {
272
                    let old_prop = css_property_cache.get_property_slow(
272
                        node_data,
272
                        node_id,
272
                        old_node_state,
272
                        &new_prop.get_type(),
                    );
272
                    let old_prop = old_prop.map_or_else(|| CssProperty::auto(new_prop.get_type()), Clone::clone);
272
                    if old_prop == *new_prop {
19
                        None
                    } else {
253
                        Some(ChangedCssProperty {
253
                            previous_state: *old_node_state,
253
                            previous_prop: old_prop,
253
                            // overriding a user property does not change the state
253
                            current_state: *old_node_state,
253
                            current_prop: new_prop.clone(),
253
                        })
                    }
272
                })
272
                .collect()
        };
272
        let css_property_cache_mut = self.get_css_property_cache_mut();
        // user_overridden_properties is built lazily (empty after StyledDom
        // construction). Grow to cover this node_id before indexing so the
        // override path works on any DOM, not just ones that already have
        // overrides from a prior mutation.
272
        if css_property_cache_mut.user_overridden_properties.len() < node_count {
24
            css_property_cache_mut
24
                .user_overridden_properties
24
                .resize(node_count, Vec::new());
259
        }
544
        for new_prop in new_properties {
272
            let prop_type = new_prop.get_type();
272
            let vec = &mut css_property_cache_mut
272
                .user_overridden_properties[node_id.index()];
272
            if new_prop.is_initial() {
                // CssProperty::Initial = remove overridden property
5
                if let Ok(idx) = vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
4
                    vec.remove(idx);
4
                }
            } else {
267
                match vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
242
                    Ok(idx) => vec[idx].1 = new_prop.clone(),
25
                    Err(idx) => vec.insert(idx, (prop_type, new_prop.clone())),
                }
            }
        }
        // The compact cache is a precomputed per-node array that the layout
        // getters read on their FAST PATH (`get_display`, `get_width`, ...)
        // BEFORE consulting `user_overridden_properties`. An override that
        // changes geometry would therefore be written, reported as changed,
        // and then ignored by layout — which is why a runtime
        // `display: none -> flex` patch (combobox list, popover, ribbon
        // gallery panel) left the node at zero size and invisible.
        //
        // REBUILD the cache rather than merely dropping it. The builder's
        // per-node walk applies `user_overridden_properties` as its last
        // step, so the rebuilt cache reflects the patch — and every consumer
        // that treats the compact cache as the source of truth keeps
        // working. Leaving it `None` until "the next full cascade" was a
        // trap: the font phase derives its requirements (font-stack
        // signature, font chains, the GC keep-set) from this cache, so the
        // very relayout that applies the patch resolved an EMPTY font world
        // — the chain cache was replaced with nothing and the patched-in
        // subtree's text laid out at zero size (the gallery panel opened as
        // an 18px blank strip). Overrides are user-interaction-rate, so the
        // rebuild is not a per-frame cost; the animation channel
        // (colour/opacity/transform) keeps the fast path untouched.
        // INHERITED paint props need the recompute too: a `color` override on
        // a container is READ by its text children through the precomputed
        // inheritance tables, so skipping the recompute left descendants at
        // the stale colour — a colour transition on a DIV animated nothing
        // visible (found by the css_anim_perf_transition damage law). The
        // per-tick animation channel avoids this whole fn via
        // `set_user_property_override_fast` + display-list patching.
272
        if new_properties
272
            .iter()
272
            .any(|p| p.get_type().can_trigger_relayout() || p.get_type().is_inheritable())
144
        {
144
            self.recompute_inheritance_and_compact_cache();
144
            self.get_css_property_cache_mut()
144
                .invalidate_resolved_font_sizes();
145
        }
272
        if !changes.is_empty() {
253
            map.insert(*node_id, changes);
264
        }
272
        map
274
    }
    /// Provide (or update) the window's `DynamicSelectorContext` — viewport
    /// size, theme, OS, media type — for this DOM's cascade.
    ///
    /// Inline conditional properties (`CssPropertyWithConditions` with
    /// viewport/@media/theme/OS selectors) evaluate against this context in
    /// BOTH production readers: `get_property_slow` (per lookup) and the
    /// compact-cache builder (at build time). A freshly created `StyledDom`
    /// has NO context — non-pseudo conditions do not apply until a window
    /// adopts the DOM and calls this, which the layout funnel
    /// (`LayoutWindow::layout_and_generate_display_list`) does before every
    /// pass.
    ///
    /// When the context actually changed AND the compact cache says some
    /// node's resting style depends on it (`has_dynamic_conditions`), the
    /// compact cache is rebuilt and hit-test tags are regenerated (a
    /// condition can flip `display`, which decides which nodes carry tags).
    /// For the common condition-free DOM a context change costs one bool
    /// read.
5885
    pub fn set_dynamic_selector_context(
5885
        &mut self,
5885
        context: azul_css::dynamic_selector::DynamicSelectorContext,
5885
    ) {
        {
5885
            let cache = self.get_css_property_cache_mut();
5885
            if cache.dynamic_context.as_deref() == Some(&context) {
141
                return;
5744
            }
5744
            cache.dynamic_context = Some(Box::new(context));
        }
        // Author-css @-rule conditions are baked at CASCADE time (restyle
        // drops non-matching rule blocks), so a context change must re-run
        // the author cascade — rebuilding the compact cache alone would
        // keep the stale rule selection. Only DOMs whose stylesheet
        // actually has conditional rules pay this.
5744
        let author_conditional = self
5744
            .get_css_property_cache()
5744
            .retained_author_css
5744
            .rules
5744
            .as_ref()
5744
            .iter()
16414
            .any(|r| !r.conditions.as_ref().is_empty());
5744
        if author_conditional {
73
            self.restyle_retained();
5671
        }
5744
        let needs_rebuild = self
5744
            .get_css_property_cache()
5744
            .compact_cache
5744
            .as_ref()
5744
            .is_none_or(|cc| cc.has_dynamic_conditions);
5744
        if needs_rebuild {
1474
            self.recompute_inheritance_and_compact_cache();
1474
            self.get_css_property_cache_mut()
1474
                .invalidate_resolved_font_sizes();
1474
            let new_tag_ids = self.css_property_cache.downcast_mut().generate_tag_ids(
1474
                &self.node_data.as_container(),
1474
                &self.node_hierarchy,
1474
            );
1474
            self.tag_ids_to_node_ids = new_tag_ids.into();
4270
        }
5885
    }
    /// The viewport-size thresholds (widths, heights, logical px) at which
    /// any conditional styling in this DOM can flip: the author
    /// stylesheet's `@media (min-/max-width/height)` bounds plus every
    /// inline conditional property's `ViewportWidth`/`ViewportHeight`
    /// bounds (harvested by the compact-cache builder). Sorted, deduped.
    ///
    /// `None` when the compact cache has not been built yet (no styling
    /// pass) — callers should treat that as "unknown" and fall back to a
    /// conservative policy. The engine's resize decision uses this instead
    /// of the old hardcoded `CSS_BREAKPOINTS` guess list, which failed both
    /// ways: a widget breakpoint like the ribbon's 720px was not on it (so
    /// shrinking onto the mobile layout never regenerated), and its eight
    /// guessed thresholds fired ~66ms full regenerations on every drag
    /// across 640/768/1024/...
    #[must_use]
9
    pub fn viewport_breakpoints(&self) -> Option<(Vec<f32>, Vec<f32>)> {
9
        let cache = self.get_css_property_cache();
9
        let cc = cache.compact_cache.as_ref()?;
9
        let (mut w, mut h) = cache.retained_author_css.viewport_breakpoints();
9
        w.extend(cc.inline_viewport_w.iter().copied().map(f32::from_bits));
9
        h.extend(cc.inline_viewport_h.iter().copied().map(f32::from_bits));
9
        w.sort_by_key(|v| v.to_bits());
9
        w.dedup_by_key(|v| v.to_bits());
9
        h.sort_by_key(|v| v.to_bits());
9
        h.dedup_by_key(|v| v.to_bits());
9
        Some((w, h))
9
    }
    /// Migrate runtime CSS overrides (`user_overridden_properties`) from a
    /// previous generation's property cache onto this DOM, following the
    /// reconciliation node matches.
    ///
    /// State follows node identity across a `RefreshDom` rebuild — exactly
    /// like datasets (`diff::transfer_states`), scroll offsets and text
    /// cursors already do. Without this, every runtime patch
    /// (`set_css_property`) silently reverted on the next app-driven DOM
    /// rebuild: the ribbon's collapsed band and an open combobox/gallery
    /// panel "un-toggled" whenever any callback returned `RefreshDom` (the
    /// ribbon's own tab-click does), because the fresh cascade knows nothing
    /// of the old override layer.
    ///
    /// Rebuilds the compact cache when anything migrated, so the layout fast
    /// path sees the carried-over values immediately.
24
    pub fn migrate_user_overrides_from(
24
        &mut self,
24
        old_cache: &CssPropertyCache,
24
        node_moves: &[crate::diff::NodeMove],
24
    ) {
24
        let node_count = self.node_data.as_ref().len();
24
        let mut migrated_any = false;
228
        for m in node_moves {
204
            let Some(old_vec) = old_cache
204
                .user_overridden_properties
204
                .get(m.old_node_id.index())
204
                .filter(|v| !v.is_empty())
            else {
204
                continue;
            };
            let new_idx = m.new_node_id.index();
            if new_idx >= node_count {
                continue;
            }
            let old_vec = old_vec.clone();
            let cache = self.get_css_property_cache_mut();
            if cache.user_overridden_properties.len() < node_count {
                cache
                    .user_overridden_properties
                    .resize(node_count, Vec::new());
            }
            cache.user_overridden_properties[new_idx] = old_vec;
            migrated_any = true;
        }
24
        if migrated_any {
            self.recompute_inheritance_and_compact_cache();
            self.get_css_property_cache_mut()
                .invalidate_resolved_font_sizes();
24
        }
24
    }
    /// Reconstruct a plain [`Dom`](crate::dom::Dom) from a subtree of this
    /// styled DOM by cloning each node's [`NodeData`](crate::dom::NodeData)
    /// (ids/classes, inline CSS, callbacks, dataset — `RefAny`/`ImageRef`
    /// fields are refcounted handles, so nothing heavy is copied).
    ///
    /// `root`: the subtree root, or `None` for the DOM's root node.
    ///
    /// The returned `Dom` CARRIES THE STYLESHEETS: the cascade retains the
    /// author CSS (`CssPropertyCache::retained_author_css`), and it is
    /// re-attached to the returned root's `css` field — re-styling the
    /// reconstruction reproduces the on-screen cascade. For a NON-root
    /// subtree this is an approximation: selectors that depended on
    /// ancestors OUTSIDE the subtree (descendant combinators through cut-off
    /// parents, `:nth-child` against removed siblings) may match differently
    /// in the new document. When exact pixel parity matters, hand the whole
    /// `StyledDom` clone to the consumer instead (e.g.
    /// `Pdf::from_styled_dom_with_resources`), which skips re-cascading
    /// entirely.
11
    #[must_use] pub fn reconstruct_dom_subtree(&self, root: Option<NodeId>) -> Dom {
        use crate::dom::NodeData;
11
        let hierarchy = self.node_hierarchy.as_container();
11
        let node_data = self.node_data.as_container();
11
        let root_id = root.unwrap_or(NodeId::ZERO);
99
        let make_dom = |id: NodeId| -> Dom {
99
            Dom {
99
                root: node_data
99
                    .get(id)
99
                    .cloned()
99
                    .unwrap_or_else(NodeData::create_div),
99
                children: Vec::new().into(),
99
                css: Vec::new().into(),
99
                estimated_total_children: 0,
99
            }
99
        };
        // Iterative post-order: a node is folded into its parent via
        // `add_child` (which maintains `estimated_total_children`) once all
        // of its own children are assembled, so arbitrary depth cannot
        // overflow the stack.
11
        let mut result_stack: Vec<Dom> = vec![make_dom(root_id)];
11
        let mut visit_stack: Vec<(NodeId, Option<NodeId>)> = vec![(
11
            root_id,
11
            hierarchy
11
                .get(root_id)
11
                .and_then(|n| n.first_child_id(root_id)),
        )];
187
        while let Some((node, next_child)) = visit_stack.pop() {
187
            if let Some(child) = next_child {
                // Come back to `node` for the sibling AFTER `child`,
                // then descend into `child`.
88
                let sibling = hierarchy
88
                    .get(child)
88
                    .and_then(NodeHierarchyItem::next_sibling_id);
88
                visit_stack.push((node, sibling));
88
                result_stack.push(make_dom(child));
88
                visit_stack.push((
88
                    child,
88
                    hierarchy.get(child).and_then(|c| c.first_child_id(child)),
                ));
            } else {
99
                let Some(finished) = result_stack.pop() else { break };
99
                if let Some(parent) = result_stack.last_mut() {
88
                    parent.add_child(finished);
88
                } else {
11
                    let mut finished = finished;
11
                    let author_css =
11
                        self.get_css_property_cache().retained_author_css.clone();
11
                    if !author_css.is_empty() {
11
                        finished.css = vec![author_css].into();
11
                    }
11
                    return finished;
                }
            }
        }
        // Unreachable for a well-formed hierarchy; degrade to an empty div.
        Dom::create_div()
11
    }
    /// Returns a HTML-formatted version of the DOM for easier debugging.
    ///
    /// For example, a DOM with a parent div containing a child div would return:
    ///
    /// ```xml,no_run,ignore
    /// <div id="hello">
    ///      <div id="test" />
    /// </div>
    /// ```
16
    #[must_use] pub fn get_html_string(&self, custom_head: &str, custom_body: &str, test_mode: bool) -> String {
16
        let css_property_cache = self.get_css_property_cache();
16
        let mut output = String::new();
        // After which nodes should a close tag be printed?
16
        let mut should_print_close_tag_after_node: BTreeMap<NodeId, Vec<(NodeId, usize)>> = BTreeMap::new();
16
        let should_print_close_tag_debug = self
16
            .non_leaf_nodes
16
            .iter()
46
            .filter_map(|p| {
46
                let parent_node_id = p.node_id.into_crate_internal()?;
46
                let mut total_last_child = None;
46
                recursive_get_last_child(
46
                    parent_node_id,
46
                    self.node_hierarchy.as_ref(),
46
                    &mut total_last_child,
                );
46
                let total_last_child = total_last_child?;
45
                Some((parent_node_id, (total_last_child, p.depth)))
46
            })
16
            .collect::<BTreeMap<_, _>>();
61
        for (parent_id, (last_child, parent_depth)) in should_print_close_tag_debug {
45
            should_print_close_tag_after_node
45
                .entry(last_child)
45
                .or_default()
45
                .push((parent_id, parent_depth));
45
        }
16
        let mut all_node_depths = self
16
            .non_leaf_nodes
16
            .iter()
46
            .filter_map(|p| {
46
                let parent_node_id = p.node_id.into_crate_internal()?;
46
                Some((parent_node_id, p.depth))
46
            })
16
            .collect::<BTreeMap<_, _>>();
46
        for (parent_node_id, parent_depth) in self
16
            .non_leaf_nodes
16
            .iter()
46
            .filter_map(|p| Some((p.node_id.into_crate_internal()?, p.depth)))
        {
315
            for child_id in parent_node_id.az_children(&self.node_hierarchy.as_container()) {
315
                all_node_depths.insert(child_id, parent_depth + 1);
315
            }
        }
331
        for node_id in self.node_hierarchy.as_container().linear_iter() {
            // A single-node DOM (or any node not reached as a non-leaf parent or
            // one of their children, e.g. a lone root) has no entry here; treat
            // its depth as 0 instead of panic-indexing the map.
331
            let depth = all_node_depths.get(&node_id).copied().unwrap_or(0);
331
            let node_data = &self.node_data.as_container()[node_id];
331
            let node_state = &self.styled_nodes.as_container()[node_id].styled_node_state;
331
            let tabs = String::from("    ").repeat(depth);
331
            output.push_str("\r\n");
331
            output.push_str(&tabs);
331
            output.push_str(&node_data.debug_print_start(css_property_cache, &node_id, node_state));
331
            if let Some(content) = node_data.get_node_type().format().as_ref() {
65
                output.push_str(content);
276
            }
331
            let node_has_children = self.node_hierarchy.as_container()[node_id]
331
                .first_child_id(node_id)
331
                .is_some();
331
            if !node_has_children {
286
                let node_data = &self.node_data.as_container()[node_id];
286
                output.push_str(&node_data.debug_print_end());
286
            }
331
            if let Some(close_tag_vec) = should_print_close_tag_after_node.get(&node_id) {
34
                let mut close_tag_vec = close_tag_vec.clone();
34
                close_tag_vec.sort_by(|a, b| b.1.cmp(&a.1)); // sort by depth descending
79
                for (close_tag_parent_id, close_tag_depth) in close_tag_vec {
45
                    let node_data = &self.node_data.as_container()[close_tag_parent_id];
45
                    let tabs = String::from("    ").repeat(close_tag_depth);
45
                    output.push_str("\r\n");
45
                    output.push_str(&tabs);
45
                    output.push_str(&node_data.debug_print_end());
45
                }
297
            }
        }
16
        if test_mode {
15
            output
        } else {
1
            format!(
1
                "
1
                <html>
1
                    <head>
1
                    <style>* {{ margin:0px; padding:0px; }}</style>
1
                    {custom_head}
1
                    </head>
1
                {output}
1
                {custom_body}
1
                </html>
1
            "
            )
        }
16
    }
    /// Returns nodes grouped by their rendering order (respects z-index and position).
2
    #[must_use] pub fn get_rects_in_rendering_order(&self) -> ContentGroup {
2
        Self::determine_rendering_order(
2
            self.non_leaf_nodes.as_ref(),
2
            &self.node_hierarchy.as_container(),
2
            &self.styled_nodes.as_container(),
2
            &self.node_data.as_container(),
2
            self.get_css_property_cache(),
        )
2
    }
    /// Returns the rendering order of the items (the rendering
    /// order doesn't have to be the original order)
3
    fn determine_rendering_order(
3
        non_leaf_nodes: &[ParentWithNodeDepth],
3
        node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
3
        styled_nodes: &NodeDataContainerRef<'_, StyledNode>,
3
        node_data_container: &NodeDataContainerRef<'_, NodeData>,
3
        css_property_cache: &CssPropertyCache,
3
    ) -> ContentGroup {
3
        let children_sorted = non_leaf_nodes
3
            .iter()
3
            .filter_map(|parent| {
                Some((
3
                    parent.node_id,
3
                    sort_children_by_position(
3
                        parent.node_id.into_crate_internal()?,
3
                        node_hierarchy,
3
                        styled_nodes,
3
                        node_data_container,
3
                        css_property_cache,
                    ),
                ))
3
            })
3
            .collect::<Vec<_>>();
3
        let children_sorted: BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>> =
3
            children_sorted.into_iter().collect();
3
        let mut root_content_group = ContentGroup {
3
            root: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
3
            children: Vec::new().into(),
3
        };
3
        fill_content_group_children(&mut root_content_group, &children_sorted);
3
        root_content_group
3
    }
    /// Replaces this `StyledDom` with default and returns the old value.
1
    #[must_use] pub fn swap_with_default(&mut self) -> Self {
1
        let mut new = Self::default();
1
        core::mem::swap(self, &mut new);
1
        new
1
    }
}
/// Same as `Dom`, but arena-based for more efficient memory layout and faster traversal.
#[derive(Debug, PartialEq, PartialOrd, Eq)]
pub struct CompactDom {
    /// The arena containing the hierarchical relationships (parent, child, sibling) of all nodes.
    pub node_hierarchy: NodeHierarchy,
    /// The arena containing the actual data (`NodeData`) for each node.
    pub node_data: NodeDataContainer<NodeData>,
    /// The ID of the root node of the DOM tree.
    pub root: NodeId,
}
impl CompactDom {
    /// Returns the number of nodes in this DOM.
    #[inline]
35431
    #[must_use] pub fn len(&self) -> usize {
35431
        self.node_hierarchy.as_ref().len()
35431
    }
    /// Returns `true` if this DOM has no nodes.
    #[inline]
3
    #[must_use] pub fn is_empty(&self) -> bool {
3
        self.node_hierarchy.as_ref().is_empty()
3
    }
}
impl From<Dom> for CompactDom {
32491
    fn from(dom: Dom) -> Self {
32491
        convert_dom_into_compact_dom(dom)
32491
    }
}
/// Converts a tree-based Dom into an arena-based `CompactDom` for efficient traversal.
32933
#[must_use] pub fn convert_dom_into_compact_dom(mut dom: Dom) -> CompactDom {
    // note: somehow convert this into a non-recursive form later on!
599914
    fn convert_dom_into_compact_dom_internal(
599914
        dom: &mut Dom,
599914
        node_hierarchy: &mut [Node],
599914
        node_data: &mut Vec<NodeData>,
599914
        parent_node_id: NodeId,
599914
        node: Node,
599914
        cur_node_id: &mut usize,
599914
    ) {
        // - parent [0]
        //    - child [1]
        //    - child [2]
        //        - child of child 2 [2]
        //        - child of child 2 [4]
        //    - child [5]
        //    - child [6]
        //        - child of child 4 [7]
        // Write node into the arena here!
599914
        node_hierarchy[parent_node_id.index()] = node;
        // MOVE the node's inline `style` AND its `extra` (NodeDataExt) box instead of relying on
        // copy_special's `self.style.clone()` / `self.extra.clone()`. Both derived Clones lower to
        // indirect-jump jump tables that remill mis-lifts on the web backend: CssProperty's clone
        // comes back with discriminant 0 (drops simple inline CSS) and for COMPLEX values (AzButton's
        // gradient; the NodeDataExt attributes Vec) the mis-lifted clone reads/writes wrong-sized data,
        // which clobbers the adjacent `style` temporary → "memory access out of bounds" later in the
        // cascade (StyledDom::create → restyle's inheritance loop reads the corrupted style). 2026-06-02:
        // copy_special_moving_complex mem::takes BOTH style+extra before copy_special, so copy_special
        // clones an EMPTY style + None extra (no broken clone runs) and restores them after. (Extra was
        // added after the AzButton ids/classes node — which lazily allocates NodeDataExt — OOB'd even
        // with the style-only take.) The Dom is consumed here, so the move is correct.
599914
        let copy = dom.root.copy_special_moving_complex();
599914
        node_data[parent_node_id.index()] = copy;
599914
        *cur_node_id += 1;
599914
        let mut previous_sibling_id = None;
599914
        let children_len = dom.children.len();
599914
        for (child_index, child_dom) in dom.children.as_mut().iter_mut().enumerate() {
566981
            let child_node_id = NodeId::new(*cur_node_id);
566981
            let is_last_child = (child_index + 1) == children_len;
566981
            let child_dom_is_empty = child_dom.children.is_empty();
566981
            let child_node = Node {
566981
                parent: Some(parent_node_id),
566981
                previous_sibling: previous_sibling_id,
566981
                next_sibling: if is_last_child {
322775
                    None
                } else {
244206
                    Some(child_node_id + child_dom.estimated_total_children + 1)
                },
566981
                last_child: if child_dom_is_empty {
273874
                    None
                } else {
293107
                    Some(child_node_id + child_dom.estimated_total_children)
                },
            };
566981
            previous_sibling_id = Some(child_node_id);
            // recurse BEFORE adding the next child
566981
            convert_dom_into_compact_dom_internal(
566981
                child_dom,
566981
                node_hierarchy,
566981
                node_data,
566981
                child_node_id,
566981
                child_node,
566981
                cur_node_id,
            );
        }
        // AUTHORITATIVE last_child. The per-child `last_child` set at construction used
        // `child_node_id + estimated_total_children`, which is the last node of the
        // whole SUBTREE (its deepest descendant), NOT the last DIRECT child — wrong
        // whenever that last child has children of its own. It corrupted `last_child_id()`
        // and, through it, append_child (which spliced onto the wrong node). The loop
        // above already tracked `previous_sibling_id`, which now holds the real last
        // direct child (None if there were none), so overwrite with it. This runs for
        // every node including the root, so it also corrects the root's own computation.
599914
        node_hierarchy[parent_node_id.index()].last_child = previous_sibling_id;
599914
    }
    // Pre-allocate all nodes (+ 1 root node)
32933
    let sum_nodes = dom.fixup_children_estimated();
32933
    let mut node_hierarchy = vec![Node::ROOT; sum_nodes + 1];
32933
    let mut node_data = vec![NodeData::create_div(); sum_nodes + 1];
32933
    let mut cur_node_id = 0;
32933
    let root_node_id = NodeId::ZERO;
32933
    let root_node = Node {
32933
        parent: None,
32933
        previous_sibling: None,
32933
        next_sibling: None,
32933
        last_child: if dom.children.is_empty() {
3265
            None
        } else {
29668
            Some(root_node_id + dom.estimated_total_children)
        },
    };
32933
    convert_dom_into_compact_dom_internal(
32933
        &mut dom,
32933
        &mut node_hierarchy,
32933
        &mut node_data,
32933
        root_node_id,
32933
        root_node,
32933
        &mut cur_node_id,
    );
32933
    CompactDom {
32933
        node_hierarchy: NodeHierarchy {
32933
            internal: node_hierarchy,
32933
        },
32933
        node_data: NodeDataContainer {
32933
            internal: node_data,
32933
        },
32933
        root: root_node_id,
32933
    }
32933
}
/// #47: scope every node's inline css to its own subtree. Walks the tree in the
/// SAME pre-order `convert_dom_into_compact_dom` uses to assign flat `NodeIds`, so the
/// `[flat_id, flat_id + estimated_total_children]` range pushed onto each rule (via
/// `CssPath::push_front_scope`) matches the ids the cascade will later see. After
/// this, a node's `with_css`/`set_css` rules can only match nodes inside its subtree
/// — they can no longer leak to the whole tree. `fixup_children_estimated()` must
/// have run first so `estimated_total_children` is populated/exact.
307960
fn scope_inline_css(dom: &mut Dom, next_id: &mut usize) {
307960
    let start = *next_id;
307960
    let end = start + dom.estimated_total_children;
307960
    for css in dom.css.as_mut().iter_mut() {
7497
        for rule in css.rules.as_mut().iter_mut() {
7497
            // Bare-decl wrappers (INLINE priority, from set_css/with_css
7497
            // selector-less declarations) are scoped node-only so a non-root
7497
            // background can't leak to descendants (#47). A stylesheet's
7497
            // `* { ... }` (AUTHOR/UA priority) scopes to the SUBTREE - the
7497
            // classic `* { margin: 0 }` reset must reach every element of the
7497
            // mounted document, not just the mount root.
7497
            let node_only = rule.priority >= azul_css::css::rule_priority::INLINE;
7497
            rule.path.push_front_scope_for(start, end, node_only);
7497
        }
    }
307960
    *next_id += 1;
307960
    for child in dom.children.as_mut().iter_mut() {
291229
        scope_inline_css(child, next_id);
291229
    }
307960
}
/// Recursively collect all CSS objects from a Dom tree (depth-first).
/// Inner (deeper) CSS objects come first, outer (shallower) CSS objects come last.
/// This means outer CSS has higher cascade priority when applied in order.
307960
fn collect_css_from_dom(dom: &Dom, out: &mut Vec<Css>) {
    // First, recurse into children (inner CSS = lower priority)
599190
    for child in &dom.children {
291230
        collect_css_from_dom(child, out);
291230
    }
    // Then, add this node's CSS objects (outer CSS = higher priority)
314712
    for css in &dom.css {
6752
        out.push(css.clone());
6752
    }
307960
}
/// Recursively strip CSS from all Dom nodes (sets css to empty vec).
/// Called after collecting CSS so the `CompactDom` doesn't carry CSS data.
307956
fn strip_css_from_dom(dom: &mut Dom) {
307956
    dom.css = Vec::new().into();
307956
    for child in dom.children.as_mut().iter_mut() {
291227
        strip_css_from_dom(child);
291227
    }
307956
}
13
fn fill_content_group_children(
13
    group: &mut ContentGroup,
13
    children_sorted: &BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>>,
13
) {
13
    if let Some(c) = children_sorted.get(&group.root) {
        // returns None for leaf nodes
5
        group.children = c
5
            .iter()
5
            .map(|child| ContentGroup {
8
                root: *child,
8
                children: Vec::new().into(),
8
            })
5
            .collect::<Vec<ContentGroup>>()
5
            .into();
8
        for c in group.children.as_mut() {
8
            fill_content_group_children(c, children_sorted);
8
        }
8
    }
13
}
5
fn sort_children_by_position(
5
    parent: NodeId,
5
    node_hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
5
    rectangles: &NodeDataContainerRef<'_, StyledNode>,
5
    node_data_container: &NodeDataContainerRef<'_, NodeData>,
5
    css_property_cache: &CssPropertyCache,
5
) -> Vec<NodeHierarchyItemId> {
    use azul_css::props::layout::LayoutPosition::Absolute;
5
    let children_positions = parent
5
        .az_children(node_hierarchy)
8
        .map(|nid| {
8
            let position = css_property_cache
8
                .get_position(
8
                    &node_data_container[nid],
8
                    &nid,
8
                    &rectangles[nid].styled_node_state,
                )
8
                .and_then(|p| (*p).get_property_or_default())
8
                .unwrap_or_default();
8
            let id = NodeHierarchyItemId::from_crate_internal(Some(nid));
8
            (id, position)
8
        })
5
        .collect::<Vec<_>>();
5
    let mut not_absolute_children = children_positions
5
        .iter()
8
        .filter_map(|(node_id, position)| {
8
            if *position == Absolute {
                None
            } else {
8
                Some(*node_id)
            }
8
        })
5
        .collect::<Vec<_>>();
5
    let mut absolute_children = children_positions
5
        .iter()
8
        .filter_map(|(node_id, position)| {
8
            if *position == Absolute {
                Some(*node_id)
            } else {
8
                None
            }
8
        })
5
        .collect::<Vec<_>>();
    // Append the position:absolute children after the regular children
5
    not_absolute_children.append(&mut absolute_children);
5
    not_absolute_children
5
}
// calls get_last_child() recursively until the last child of the last child of the ... has been
// found
107
fn recursive_get_last_child(
107
    node_id: NodeId,
107
    node_hierarchy: &[NodeHierarchyItem],
107
    target: &mut Option<NodeId>,
107
) {
107
    match node_hierarchy[node_id.index()].last_child_id() {
49
        None => (),
58
        Some(s) => {
58
            *target = Some(s);
58
            recursive_get_last_child(s, node_hierarchy, target);
58
        }
    }
107
}
// ============================================================================
// DOM TRAVERSAL FOR MULTI-NODE SELECTION
// ============================================================================
/// Determine if `node_a` comes before `node_b` in document order.
///
/// Document order is defined as pre-order depth-first traversal order.
/// This is equivalent to the order nodes appear in HTML source.
///
/// ## Algorithm
/// 1. Find the path from root to each node
/// 2. Find the Lowest Common Ancestor (LCA)
/// 3. At the divergence point, the child that appears first in sibling order comes first
25
#[must_use] pub fn is_before_in_document_order(
25
    hierarchy: &NodeHierarchyItemVec,
25
    node_a: NodeId,
25
    node_b: NodeId,
25
) -> bool {
25
    if node_a == node_b {
7
        return false;
18
    }
18
    let hierarchy = hierarchy.as_container();
    // Get paths from root to each node (stored as root-first order)
18
    let path_a = get_path_to_root(&hierarchy, node_a);
18
    let path_b = get_path_to_root(&hierarchy, node_b);
    // Find divergence point (last common ancestor)
18
    let min_len = path_a.len().min(path_b.len());
24
    for i in 0..min_len {
24
        if path_a[i] != path_b[i] {
            // Found divergence - check which sibling comes first
4
            let child_towards_a = path_a[i];
4
            let child_towards_b = path_b[i];
            // A smaller NodeId index means it was created earlier in DOM construction,
            // which means it comes first in document order for siblings
4
            return child_towards_a.index() < child_towards_b.index();
20
        }
    }
    // One path is a prefix of the other - the shorter path (ancestor) comes first
14
    path_a.len() < path_b.len()
25
}
/// Get the path from root to a node, returned in root-first order.
39
fn get_path_to_root(
39
    hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
39
    node: NodeId,
39
) -> Vec<NodeId> {
39
    let mut path = Vec::new();
39
    let mut current = Some(node);
110
    while let Some(node_id) = current {
71
        path.push(node_id);
71
        current = hierarchy.get(node_id).and_then(NodeHierarchyItem::parent_id);
71
    }
    // Reverse to get root-first order
39
    path.reverse();
39
    path
39
}
/// Collect all nodes between start and end (inclusive) in document order.
///
/// This performs a pre-order depth-first traversal starting from the root,
/// collecting nodes once we've seen `start` and stopping at `end`.
///
/// ## Parameters
/// * `hierarchy` - The node hierarchy
/// * `start_node` - First node in document order
/// * `end_node` - Last node in document order
///
/// ## Returns
/// Vector of `NodeIds` in document order, from start to end (inclusive)
7
#[must_use] pub fn collect_nodes_in_document_order(
7
    hierarchy: &NodeHierarchyItemVec,
7
    start_node: NodeId,
7
    end_node: NodeId,
7
) -> Vec<NodeId> {
7
    if start_node == end_node {
2
        return vec![start_node];
5
    }
5
    let hierarchy_container = hierarchy.as_container();
5
    let hierarchy_slice = hierarchy.as_ref();
5
    let mut result = Vec::new();
5
    let mut in_range = false;
    // Pre-order DFS using a stack
    // We need to traverse in document order, which is pre-order DFS
5
    let mut stack: Vec<NodeId> = vec![NodeId::ZERO]; // Start from root
17
    while let Some(current) = stack.pop() {
        // Check if we've entered the range
16
        if current == start_node {
4
            in_range = true;
12
        }
        // Collect if in range
16
        if in_range {
12
            result.push(current);
12
        }
        // Check if we've exited the range
16
        if current == end_node {
4
            break;
12
        }
        // Push children in reverse order so they pop in correct order
        // (first child should be processed first)
12
        if let Some(item) = hierarchy_container.get(current) {
            // Get first child
12
            if let Some(first_child) = item.first_child_id(current) {
                // Collect all children by following next_sibling
6
                let mut children = Vec::new();
6
                let mut child = Some(first_child);
20
                while let Some(child_id) = child {
14
                    children.push(child_id);
14
                    child = hierarchy_container.get(child_id).and_then(NodeHierarchyItem::next_sibling_id);
14
                }
                // Push in reverse order for correct DFS order
14
                for child_id in children.into_iter().rev() {
14
                    stack.push(child_id);
14
                }
6
            }
        }
    }
5
    result
7
}
/// Check if two `StyledDom`s are structurally equivalent for layout purposes.
///
/// Returns `true` if the DOMs have the same structure, node types, classes,
/// IDs, inline styles, and callback event registrations — meaning the
/// layout output would be identical.
///
/// Image callback nodes are compared by function pointer and `RefAny` type ID
/// rather than heap pointer, since each `layout()` call creates new `ImageRef`
/// allocations even when the callback is the same.
///
/// This is used to short-circuit the expensive layout pipeline when the DOM
/// hasn't actually changed (e.g., an animation timer fires but only the GL
/// texture content changed, not the DOM structure).
9
#[must_use] pub fn is_layout_equivalent(old: &StyledDom, new: &StyledDom) -> bool {
    use crate::dom::NodeType;
    use crate::resources::DecodedImage;
    // Quick check: node count must match
9
    let old_nodes = old.node_data.as_ref();
9
    let new_nodes = new.node_data.as_ref();
9
    if old_nodes.len() != new_nodes.len() {
2
        return false;
7
    }
    // Check hierarchy (parent/child/sibling structure)
7
    let old_hier = old.node_hierarchy.as_ref();
7
    let new_hier = new.node_hierarchy.as_ref();
7
    if old_hier.len() != new_hier.len() {
        return false;
7
    }
7
    if old_hier != new_hier {
1
        return false;
6
    }
    // Per-node comparison
15
    for (old_node, new_node) in old_nodes.iter().zip(new_nodes.iter()) {
        // Compare node type discriminant
15
        if core::mem::discriminant(&old_node.node_type)
15
            != core::mem::discriminant(&new_node.node_type)
        {
            return false;
15
        }
        // Compare node type content (with special handling for image callbacks)
15
        match (&old_node.node_type, &new_node.node_type) {
            (NodeType::Image(old_img), NodeType::Image(new_img)) => {
                match (old_img.get_data(), new_img.get_data()) {
                    (DecodedImage::Callback(old_cb), DecodedImage::Callback(new_cb)) => {
                        // Compare callback function pointer (stable across frames)
                        if old_cb.callback.cb != new_cb.callback.cb {
                            return false;
                        }
                        // Compare RefAny type ID (not instance pointer)
                        if old_cb.refany.get_type_id() != new_cb.refany.get_type_id() {
                            return false;
                        }
                    }
                    _ => {
                        // Raw images / GL textures: compare by pointer identity
                        if old_img != new_img {
                            return false;
                        }
                    }
                }
            }
            _ => {
15
                if old_node.node_type != new_node.node_type {
                    return false;
15
                }
            }
        }
        // Compare IDs and classes (now stored in attributes as AttributeType::Id/Class)
        {
            use crate::dom::AttributeType;
15
            let old_ids_classes: Vec<_> = old_node.attributes().as_ref().iter()
15
                .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
15
                .collect();
15
            let new_ids_classes: Vec<_> = new_node.attributes().as_ref().iter()
15
                .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
15
                .collect();
15
            if old_ids_classes != new_ids_classes {
1
                return false;
14
            }
        }
        // Compare inline CSS (direct layout input)
14
        if old_node.style != new_node.style {
            return false;
14
        }
        // Compare callback event types (affects hit-test tags)
        // We compare only event types, not function pointers or data
14
        let old_cbs = old_node.callbacks.as_ref();
14
        let new_cbs = new_node.callbacks.as_ref();
14
        if old_cbs.len() != new_cbs.len() {
            return false;
14
        }
14
        for (old_cb, new_cb) in old_cbs.iter().zip(new_cbs.iter()) {
            if old_cb.event != new_cb.event {
                return false;
            }
        }
        // Compare attributes (some affect layout, e.g. colspan)
14
        if old_node.attributes().as_ref() != new_node.attributes().as_ref() {
            return false;
14
        }
    }
    // Compare styled node states (hover/focus/active flags affect CSS resolution)
5
    let old_styled = old.styled_nodes.as_ref();
5
    let new_styled = new.styled_nodes.as_ref();
5
    if old_styled.len() != new_styled.len() {
        return false;
5
    }
5
    if old_styled != new_styled {
1
        return false;
4
    }
4
    true
9
}
#[cfg(test)]
mod audit_tests {
    use super::*;
    use azul_css::props::basic::StyleFontFamily;
6
    fn fam(name: &str) -> StyleFontFamily {
6
        StyleFontFamily::System(name.to_string().into())
6
    }
    #[test]
1
    fn style_font_families_hash_is_length_sensitive() {
        // The length prefix guarantees that lists of different lengths cannot
        // collide, and that hashing is deterministic.
1
        let a = StyleFontFamiliesHash::new(&[fam("Arial")]);
1
        let a2 = StyleFontFamiliesHash::new(&[fam("Arial")]);
1
        assert_eq!(a, a2, "hash must be deterministic");
1
        let two = StyleFontFamiliesHash::new(&[fam("Arial"), fam("Helvetica")]);
1
        assert_ne!(a, two, "different-length family lists must not collide");
1
        let empty = StyleFontFamiliesHash::new(&[]);
1
        assert_ne!(empty, a);
1
        assert_ne!(empty, two);
        // Order still matters.
1
        let rev = StyleFontFamiliesHash::new(&[fam("Helvetica"), fam("Arial")]);
1
        assert_ne!(two, rev);
1
    }
}
#[cfg(test)]
#[allow(clippy::too_many_lines)]
mod autotest_generated {
    use azul_css::{
        dynamic_selector::PseudoStateFlags,
        props::basic::StyleFontFamily,
    };
    use super::*;
    // ---------------------------------------------------------------------
    // helpers
    // ---------------------------------------------------------------------
    /// Builds a `NodeHierarchyItem` directly from the RAW (1-based) encoding:
    /// `0` = none, `n` = `NodeId(n - 1)`.
    const fn raw_item(parent: usize, prev: usize, next: usize, last: usize) -> NodeHierarchyItem {
        NodeHierarchyItem {
            parent,
            previous_sibling: prev,
            next_sibling: next,
            last_child: last,
        }
    }
    /// `<body>` with `n` leaf `<div>` children, cascaded against an empty stylesheet.
    /// Node ids are `0 = body`, `1..=n` = the children.
    fn flat_body(n: usize) -> StyledDom {
        let children: Vec<Dom> = (0..n).map(|_| Dom::create_div()).collect();
        let mut dom = Dom::create_body().with_children(children.into());
        StyledDom::create(&mut dom, Css::empty())
    }
    /// `<body> > <div> > <div>` — the last direct child of the root is itself a parent.
    fn nested_body() -> StyledDom {
        let mut dom = Dom::create_body().with_children(
            vec![Dom::create_div().with_children(vec![Dom::create_div()].into())].into(),
        );
        StyledDom::create(&mut dom, Css::empty())
    }
    fn parse_css(s: &str) -> Css {
        azul_css::parser2::new_from_str(s).0
    }
    fn family(name: &str) -> StyleFontFamily {
        StyleFontFamily::System(name.to_string().into())
    }
    const fn pseudo_flags(all: bool) -> PseudoStateFlags {
        PseudoStateFlags {
            hover: all,
            active: all,
            focused: all,
            disabled: all,
            checked: all,
            focus_within: all,
            visited: all,
            backdrop: all,
            dragging: all,
            drag_over: all,
        }
    }
    fn empty_menu() -> Menu {
        let items: Vec<crate::menu::MenuItem> = Vec::new();
        Menu::create(items.into())
    }
    // ---------------------------------------------------------------------
    // RestyleResult (predicate + merge)
    // ---------------------------------------------------------------------
    #[test]
    fn restyle_result_default_reports_no_changes() {
        let r = RestyleResult::default();
        assert!(!r.has_changes());
        assert!(!r.needs_layout);
        assert!(!r.needs_display_list);
        assert!(!r.gpu_only_changes);
        assert_eq!(r.max_relayout_scope, RelayoutScope::None);
    }
    #[test]
    fn restyle_result_has_changes_keys_off_node_map_not_property_count() {
        // A node entry with an EMPTY change list still counts as "changed":
        // has_changes() only looks at the node map, never at the inner Vec.
        let mut r = RestyleResult::default();
        r.changed_nodes.insert(NodeId::ZERO, Vec::new());
        assert!(r.has_changes());
        r.changed_nodes.clear();
        assert!(!r.has_changes());
    }
    #[test]
    fn restyle_result_merge_ors_layout_flags_and_ands_gpu_only() {
        let mut a = RestyleResult {
            needs_layout: false,
            needs_display_list: false,
            gpu_only_changes: true,
            ..RestyleResult::default()
        };
        let b = RestyleResult {
            needs_layout: true,
            needs_display_list: true,
            gpu_only_changes: true,
            ..RestyleResult::default()
        };
        a.merge(b);
        assert!(a.needs_layout, "needs_layout is OR-ed");
        assert!(a.needs_display_list, "needs_display_list is OR-ed");
        assert!(a.gpu_only_changes, "true && true stays true");
        // ...and a single non-GPU-only participant clears the flag.
        let mut c = RestyleResult {
            gpu_only_changes: true,
            ..RestyleResult::default()
        };
        c.merge(RestyleResult {
            gpu_only_changes: false,
            ..RestyleResult::default()
        });
        assert!(!c.gpu_only_changes, "gpu_only_changes is AND-ed");
    }
    #[test]
    fn restyle_result_merge_keeps_the_most_expensive_scope() {
        let mut low = RestyleResult {
            max_relayout_scope: RelayoutScope::None,
            ..RestyleResult::default()
        };
        low.merge(RestyleResult {
            max_relayout_scope: RelayoutScope::Full,
            ..RestyleResult::default()
        });
        assert_eq!(low.max_relayout_scope, RelayoutScope::Full);
        // ...and merging a cheaper scope must NOT downgrade it.
        let mut high = RestyleResult {
            max_relayout_scope: RelayoutScope::Full,
            ..RestyleResult::default()
        };
        high.merge(RestyleResult {
            max_relayout_scope: RelayoutScope::IfcOnly,
            ..RestyleResult::default()
        });
        assert_eq!(high.max_relayout_scope, RelayoutScope::Full);
    }
    #[test]
    fn restyle_result_merge_of_default_is_not_the_identity_for_gpu_only() {
        // `RestyleResult::default()` has gpu_only_changes == false, and merge()
        // AND-s that flag — so merging an EMPTY result still clears it. Pinned
        // here because it is a genuine footgun for callers that merge in a loop.
        let mut a = RestyleResult {
            gpu_only_changes: true,
            ..RestyleResult::default()
        };
        a.merge(RestyleResult::default());
        assert!(!a.gpu_only_changes);
        assert!(!a.has_changes());
    }
    #[test]
    fn restyle_result_merge_concatenates_changes_for_the_same_node() {
        let prop = |t| ChangedCssProperty {
            previous_state: StyledNodeState::new(),
            previous_prop: CssProperty::auto(t),
            current_state: StyledNodeState::new(),
            current_prop: CssProperty::initial(t),
        };
        let mut a = RestyleResult::default();
        a.changed_nodes
            .insert(NodeId::ZERO, vec![prop(CssPropertyType::Width)]);
        let mut b = RestyleResult::default();
        b.changed_nodes
            .insert(NodeId::ZERO, vec![prop(CssPropertyType::Height)]);
        b.changed_nodes
            .insert(NodeId::new(1), vec![prop(CssPropertyType::Opacity)]);
        a.merge(b);
        assert_eq!(a.changed_nodes.len(), 2);
        assert_eq!(
            a.changed_nodes[&NodeId::ZERO].len(),
            2,
            "changes for the same node are appended, not replaced"
        );
        assert_eq!(a.changed_nodes[&NodeId::new(1)].len(), 1);
        assert!(a.has_changes());
    }
    // ---------------------------------------------------------------------
    // StyledNodeState (constructor + predicates)
    // ---------------------------------------------------------------------
    #[test]
    fn styled_node_state_new_is_all_false_and_normal() {
        let s = StyledNodeState::new();
        assert!(s.is_normal());
        assert!(!s.hover);
        assert!(!s.active);
        assert!(!s.focused);
        assert!(!s.disabled);
        assert!(!s.checked);
        assert!(!s.focus_within);
        assert!(!s.visited);
        assert!(!s.backdrop);
        assert!(!s.dragging);
        assert!(!s.drag_over);
        assert_eq!(s, StyledNodeState::default());
    }
    #[test]
    fn styled_node_state_has_state_zero_is_always_true() {
        // 0 == "Normal", which is active regardless of the other flags.
        assert!(StyledNodeState::new().has_state(0));
        assert!(StyledNodeState::from_pseudo_state_flags(&pseudo_flags(true)).has_state(0));
    }
    #[test]
    fn styled_node_state_has_state_maps_every_index_exactly_once() {
        // Each setter must light up exactly one state index in 1..=10.
        let setters: [(u8, fn(&mut StyledNodeState)); 10] = [
            (1, |s| s.hover = true),
            (2, |s| s.active = true),
            (3, |s| s.focused = true),
            (4, |s| s.disabled = true),
            (5, |s| s.checked = true),
            (6, |s| s.focus_within = true),
            (7, |s| s.visited = true),
            (8, |s| s.backdrop = true),
            (9, |s| s.dragging = true),
            (10, |s| s.drag_over = true),
        ];
        for (expected_idx, set) in setters {
            let mut s = StyledNodeState::new();
            set(&mut s);
            assert!(!s.is_normal(), "state {expected_idx} must not be 'normal'");
            for idx in 1..=10u8 {
                assert_eq!(
                    s.has_state(idx),
                    idx == expected_idx,
                    "state index {idx} misreported for setter {expected_idx}"
                );
            }
        }
    }
    #[test]
    fn styled_node_state_has_state_is_false_for_every_out_of_range_u8() {
        let all_on = StyledNodeState::from_pseudo_state_flags(&pseudo_flags(true));
        for idx in 11..=u8::MAX {
            assert!(!StyledNodeState::new().has_state(idx));
            assert!(
                !all_on.has_state(idx),
                "unknown state index {idx} must be inactive even when every flag is set"
            );
        }
    }
    #[test]
    fn styled_node_state_from_pseudo_state_flags_roundtrips_every_field() {
        let all_on = StyledNodeState::from_pseudo_state_flags(&pseudo_flags(true));
        assert!(!all_on.is_normal());
        for idx in 0..=10u8 {
            assert!(all_on.has_state(idx), "state {idx} should be active");
        }
        let all_off = StyledNodeState::from_pseudo_state_flags(&pseudo_flags(false));
        assert!(all_off.is_normal());
        assert_eq!(all_off, StyledNodeState::new());
    }
    #[test]
    fn styled_node_state_debug_lists_active_states_and_normal_when_empty() {
        assert_eq!(format!("{:?}", StyledNodeState::new()), "[\"normal\"]");
        let mut s = StyledNodeState::new();
        s.hover = true;
        s.drag_over = true;
        let dbg = format!("{s:?}");
        assert!(dbg.contains("hover"), "{dbg}");
        assert!(dbg.contains("drag_over"), "{dbg}");
        assert!(!dbg.contains("normal"), "{dbg}");
    }
    // ---------------------------------------------------------------------
    // StyledNodeVec containers
    // ---------------------------------------------------------------------
    #[test]
    fn styled_node_vec_empty_container_is_empty_and_get_returns_none() {
        let v: StyledNodeVec = Vec::new().into();
        let c = v.as_container();
        assert_eq!(c.len(), 0);
        assert!(c.is_empty());
        assert!(c.get(NodeId::ZERO).is_none());
        assert!(c.get(NodeId::new(usize::MAX)).is_none());
    }
    #[test]
    fn styled_node_vec_container_mut_writes_are_visible_through_container() {
        let mut v: StyledNodeVec = vec![StyledNode::default(), StyledNode::default()].into();
        {
            let mut c = v.as_container_mut();
            c[NodeId::new(1)].styled_node_state.hover = true;
        }
        let c = v.as_container();
        assert_eq!(c.len(), 2);
        assert!(!c[NodeId::ZERO].styled_node_state.hover);
        assert!(c[NodeId::new(1)].styled_node_state.hover);
        assert!(c.get(NodeId::new(2)).is_none());
    }
    // ---------------------------------------------------------------------
    // Font family hashes
    // ---------------------------------------------------------------------
    #[test]
    fn style_font_family_hash_is_deterministic_and_input_sensitive() {
        assert_eq!(
            StyleFontFamilyHash::new(&family("Arial")),
            StyleFontFamilyHash::new(&family("Arial"))
        );
        assert_ne!(
            StyleFontFamilyHash::new(&family("Arial")),
            StyleFontFamilyHash::new(&family("Ariaĺ"))
        );
        // Same string, different variant → different cache key.
        assert_ne!(
            StyleFontFamilyHash::new(&StyleFontFamily::System("x".to_string().into())),
            StyleFontFamilyHash::new(&StyleFontFamily::File("x".to_string().into()))
        );
    }
    #[test]
    fn style_font_family_hash_handles_empty_unicode_and_huge_names() {
        let empty = family("");
        let unicode = family("🦀 ノート ﷽ عربى");
        let huge = family(&"A".repeat(100_000));
        // No panic, and each distinct input is stable across calls.
        assert_eq!(StyleFontFamilyHash::new(&empty), StyleFontFamilyHash::new(&empty));
        assert_eq!(
            StyleFontFamilyHash::new(&unicode),
            StyleFontFamilyHash::new(&unicode)
        );
        assert_eq!(StyleFontFamilyHash::new(&huge), StyleFontFamilyHash::new(&huge));
        assert_ne!(StyleFontFamilyHash::new(&empty), StyleFontFamilyHash::new(&unicode));
        assert_ne!(StyleFontFamilyHash::new(&empty), StyleFontFamilyHash::new(&huge));
    }
    #[test]
    fn style_font_families_hash_empty_slice_is_stable_and_distinct() {
        let empty = StyleFontFamiliesHash::new(&[]);
        assert_eq!(empty, StyleFontFamiliesHash::new(&[]));
        assert_ne!(empty, StyleFontFamiliesHash::new(&[family("")]));
    }
    #[test]
    fn style_font_families_hash_scales_to_large_lists_and_is_length_sensitive() {
        let big: Vec<StyleFontFamily> = (0..1000).map(|i| family(&format!("font-{i}"))).collect();
        let one_shorter = &big[..999];
        assert_eq!(
            StyleFontFamiliesHash::new(&big),
            StyleFontFamiliesHash::new(&big),
            "hashing 1000 families must be deterministic"
        );
        assert_ne!(
            StyleFontFamiliesHash::new(&big),
            StyleFontFamiliesHash::new(one_shorter),
            "the length prefix must separate [0..1000) from [0..999)"
        );
    }
    // ---------------------------------------------------------------------
    // NodeHierarchyItemId: 1-based encode/decode round-trip
    // ---------------------------------------------------------------------
    #[test]
    fn node_hierarchy_item_id_none_is_zero() {
        assert_eq!(NodeHierarchyItemId::NONE.into_raw(), 0);
        assert_eq!(NodeHierarchyItemId::NONE.into_crate_internal(), None);
        assert_eq!(NodeHierarchyItemId::from_crate_internal(None).into_raw(), 0);
        assert_eq!(NodeHierarchyItemId::from_raw(0).into_crate_internal(), None);
        assert_eq!(NodeHierarchyItemId::from_crate_internal(None), NodeHierarchyItemId::NONE);
    }
    #[test]
    fn node_hierarchy_item_id_encode_decode_roundtrip_at_boundaries() {
        // usize::MAX - 1 is the largest index that survives the +1 encoding.
        for idx in [0usize, 1, 2, 1023, usize::MAX / 2, usize::MAX - 1] {
            let id = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(idx)));
            assert_eq!(id.into_raw(), idx + 1, "1-based encoding for {idx}");
            assert_eq!(
                id.into_crate_internal(),
                Some(NodeId::new(idx)),
                "decode(encode(x)) == x for {idx}"
            );
        }
    }
    #[test]
    fn node_hierarchy_item_id_raw_roundtrip_is_identity_even_at_usize_max() {
        for raw in [0usize, 1, 2, 7, u32::MAX as usize, usize::MAX] {
            let decoded = NodeHierarchyItemId::from_raw(raw).into_crate_internal();
            let reencoded = NodeHierarchyItemId::from_crate_internal(decoded).into_raw();
            assert_eq!(reencoded, raw, "encode(decode(raw)) must be identity for {raw}");
        }
    }
    #[test]
    fn node_hierarchy_item_id_from_raw_decodes_one_based() {
        assert_eq!(
            NodeHierarchyItemId::from_raw(1).into_crate_internal(),
            Some(NodeId::ZERO),
            "raw 1 is NodeId(0), NOT NodeId(1)"
        );
        assert_eq!(
            NodeHierarchyItemId::from_raw(usize::MAX).into_crate_internal(),
            Some(NodeId::new(usize::MAX - 1))
        );
    }
    #[test]
    fn node_hierarchy_item_id_debug_and_display_agree() {
        let none = NodeHierarchyItemId::NONE;
        assert_eq!(format!("{none:?}"), "None");
        assert_eq!(format!("{none}"), format!("{none:?}"));
        let some = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(5)));
        assert_eq!(format!("{some:?}"), "Some(NodeId(5))");
        assert_eq!(format!("{some}"), format!("{some:?}"));
        // Extreme value: must not panic and must stay non-empty.
        let max = NodeHierarchyItemId::from_raw(usize::MAX);
        assert!(!format!("{max:?}").is_empty());
    }
    #[test]
    fn node_hierarchy_item_id_ordering_follows_raw_value() {
        let a = NodeHierarchyItemId::from_raw(0);
        let b = NodeHierarchyItemId::from_raw(1);
        let c = NodeHierarchyItemId::from_raw(usize::MAX);
        assert!(a < b);
        assert!(b < c);
        assert_eq!(a, NodeHierarchyItemId::NONE);
    }
    #[test]
    fn node_hierarchy_item_id_from_impls_match_the_explicit_ones() {
        let opt = Some(NodeId::new(41));
        let via_from: NodeHierarchyItemId = opt.into();
        assert_eq!(via_from, NodeHierarchyItemId::from_crate_internal(opt));
        let back: Option<NodeId> = via_from.into();
        assert_eq!(back, opt);
        let none: NodeHierarchyItemId = None.into();
        assert_eq!(none.into_raw(), 0);
    }
    // ---------------------------------------------------------------------
    // NodeHierarchyItem getters
    // ---------------------------------------------------------------------
    #[test]
    fn node_hierarchy_item_zeroed_has_no_links() {
        let z = NodeHierarchyItem::zeroed();
        assert_eq!(z.parent_id(), None);
        assert_eq!(z.previous_sibling_id(), None);
        assert_eq!(z.next_sibling_id(), None);
        assert_eq!(z.last_child_id(), None);
        assert_eq!(z.first_child_id(NodeId::ZERO), None);
        assert_eq!(z.first_child_id(NodeId::new(usize::MAX)), None);
        assert_eq!(z, NodeHierarchyItem::from(Node::ROOT));
    }
    #[test]
    fn node_hierarchy_item_getters_decode_the_one_based_fields() {
        let item = raw_item(1, 2, 3, 4);
        assert_eq!(item.parent_id(), Some(NodeId::new(0)));
        assert_eq!(item.previous_sibling_id(), Some(NodeId::new(1)));
        assert_eq!(item.next_sibling_id(), Some(NodeId::new(2)));
        assert_eq!(item.last_child_id(), Some(NodeId::new(3)));
        // first_child is derived: parent + 1, but only if the node has children.
        assert_eq!(item.first_child_id(NodeId::new(7)), Some(NodeId::new(8)));
    }
    #[test]
    fn node_hierarchy_item_getters_at_usize_max_do_not_overflow() {
        let item = raw_item(usize::MAX, usize::MAX, usize::MAX, usize::MAX);
        assert_eq!(item.parent_id(), Some(NodeId::new(usize::MAX - 1)));
        assert_eq!(item.previous_sibling_id(), Some(NodeId::new(usize::MAX - 1)));
        assert_eq!(item.next_sibling_id(), Some(NodeId::new(usize::MAX - 1)));
        assert_eq!(item.last_child_id(), Some(NodeId::new(usize::MAX - 1)));
        // NodeId's Add is saturating, so `current + 1` clamps instead of wrapping
        // to 0 (which would alias the root node).
        assert_eq!(
            item.first_child_id(NodeId::new(usize::MAX)),
            Some(NodeId::new(usize::MAX)),
            "first_child_id must saturate, never wrap to NodeId(0)"
        );
    }
    #[test]
    fn node_hierarchy_item_from_node_preserves_every_link() {
        let node = Node {
            parent: Some(NodeId::new(3)),
            previous_sibling: None,
            next_sibling: Some(NodeId::new(9)),
            last_child: Some(NodeId::new(12)),
        };
        let item: NodeHierarchyItem = node.into();
        assert_eq!(item.parent_id(), node.parent);
        assert_eq!(item.previous_sibling_id(), node.previous_sibling);
        assert_eq!(item.next_sibling_id(), node.next_sibling);
        assert_eq!(item.last_child_id(), node.last_child);
    }
    // ---------------------------------------------------------------------
    // NodeHierarchyItemVec container + subtree_len
    // ---------------------------------------------------------------------
    #[test]
    fn node_hierarchy_item_vec_containers_read_and_write() {
        let mut v: NodeHierarchyItemVec = vec![NodeHierarchyItem::zeroed(); 2].into();
        {
            let mut c = v.as_container_mut();
            c[NodeId::new(1)].parent = 1; // raw 1 == NodeId(0)
        }
        let c = v.as_container();
        assert_eq!(c.len(), 2);
        assert_eq!(c[NodeId::new(1)].parent_id(), Some(NodeId::ZERO));
        assert!(c.get(NodeId::new(2)).is_none());
        let empty: NodeHierarchyItemVec = Vec::new().into();
        assert!(empty.as_container().is_empty());
    }
    #[test]
    fn subtree_len_counts_descendants_of_a_real_tree() {
        // body(0) > div(1) > div(2)
        let sd = nested_body();
        let h = sd.node_hierarchy.as_container();
        assert_eq!(h.len(), 3);
        assert_eq!(h.subtree_len(NodeId::ZERO), 2, "root has 2 descendants");
        assert_eq!(h.subtree_len(NodeId::new(1)), 1);
        assert_eq!(h.subtree_len(NodeId::new(2)), 0, "a leaf has no descendants");
    }
    #[test]
    fn subtree_len_saturates_on_a_malformed_backwards_next_sibling() {
        // Node 2 claims its next sibling is node 0 — a backwards link a malformed
        // FastDom can produce. The subtraction must saturate, not underflow-panic.
        let v: NodeHierarchyItemVec = vec![
            raw_item(0, 0, 0, 0),
            raw_item(0, 0, 0, 0),
            raw_item(0, 0, /* next = NodeId(0) */ 1, 0),
        ]
        .into();
        let c = v.as_container();
        assert_eq!(c.subtree_len(NodeId::new(2)), 0);
        // Self-referential next_sibling (node 1 -> node 1) must also saturate.
        let v2: NodeHierarchyItemVec = vec![raw_item(0, 0, 0, 0), raw_item(0, 0, 2, 0)].into();
        assert_eq!(v2.as_container().subtree_len(NodeId::new(1)), 0);
    }
    // ---------------------------------------------------------------------
    // StyledDomMemoryReport
    // ---------------------------------------------------------------------
    #[test]
    fn memory_report_default_total_is_zero() {
        assert_eq!(StyledDomMemoryReport::default().total_bytes(), 0);
    }
    #[test]
    fn memory_report_total_bytes_sums_every_field() {
        let r = StyledDomMemoryReport {
            node_count: 3,
            node_hierarchy_bytes: 1,
            node_data_bytes: 2,
            styled_nodes_bytes: 4,
            cascade_info_bytes: 8,
            tag_ids_bytes: 16,
            non_leaf_nodes_bytes: 32,
            callback_vecs_bytes: 64,
            ..StyledDomMemoryReport::default()
        };
        assert_eq!(r.total_bytes(), 127, "node_count must NOT be part of the sum");
        // A single saturated field must not overflow the running sum.
        let extreme = StyledDomMemoryReport {
            node_data_bytes: usize::MAX,
            ..StyledDomMemoryReport::default()
        };
        assert_eq!(extreme.total_bytes(), usize::MAX);
    }
    #[test]
    fn memory_report_tracks_node_count_and_is_monotonic_in_dom_size() {
        let small = flat_body(1).memory_report();
        let large = flat_body(50).memory_report();
        assert_eq!(small.node_count, 2);
        assert_eq!(large.node_count, 51);
        assert!(large.total_bytes() > small.total_bytes());
        assert!(small.total_bytes() >= small.node_hierarchy_bytes + small.node_data_bytes);
        // Also fine on the smallest possible DOM.
        let d = StyledDom::default().memory_report();
        assert_eq!(d.node_count, 1);
        assert!(d.total_bytes() > 0);
    }
    // ---------------------------------------------------------------------
    // StyledDom construction
    // ---------------------------------------------------------------------
    #[test]
    fn default_styled_dom_is_a_single_rooted_body() {
        let sd = StyledDom::default();
        assert_eq!(sd.node_count(), 1);
        assert_eq!(sd.root.into_crate_internal(), Some(NodeId::ZERO));
        assert_eq!(sd.node_hierarchy.as_ref().len(), 1);
        assert_eq!(sd.styled_nodes.as_ref().len(), 1);
        assert_eq!(sd.cascade_info.as_ref().len(), 1);
        assert_eq!(sd.non_leaf_nodes.as_ref().len(), 1);
        assert_eq!(sd.non_leaf_nodes.as_ref()[0].depth, 0);
        assert!(sd.tag_ids_to_node_ids.as_ref().is_empty());
        assert!(sd.get_styled_node_state(&NodeId::ZERO).is_normal());
    }
    /// miniword ENGINE-ISSUE 4: `Dom::create_text_do_not_use_without_block_level_wrapper(..).with_css(..)` silently
    /// dropped EVERY declaration — the bare-decl wrapper parses to
    /// `* { .. }`, and the `Global` matcher refused text nodes even for
    /// rules scoped to exactly that node. All four reported strings now
    /// cascade onto the text node.
    #[test]
    fn with_css_on_a_text_node_applies_its_declarations() {
        use azul_css::props::basic::color::ColorU;
        let cases: &[(&str, ColorU, isize)] = &[
            ("font-size: 38px; color: #565656;", ColorU { r: 0x56, g: 0x56, b: 0x56, a: 255 }, 38),
            ("font-size: 38px; color: #565656; flex-grow: 0;", ColorU { r: 0x56, g: 0x56, b: 0x56, a: 255 }, 38),
            ("font-size: 16px; color: #2b579a; margin-bottom: 12px;", ColorU { r: 0x2b, g: 0x57, b: 0x9a, a: 255 }, 16),
            ("font-size: 13px; color: white;", ColorU { r: 255, g: 255, b: 255, a: 255 }, 13),
        ];
        for (css_str, want_color, want_px) in cases {
            let dom = crate::dom::Dom::create_body().with_child(
                crate::dom::Dom::create_div()
                    .with_css("color: #444444; font-size: 10px;")
                    .with_child(crate::dom::Dom::create_text_do_not_use_without_block_level_wrapper("X").with_css(css_str)),
            );
            // create_from_dom is the production path (scope_inline_css +
            // collect_css_from_dom); plain create() ignores dom.css.
            let styled = StyledDom::create_from_dom(dom);
            let cache = styled.get_css_property_cache();
            let n = styled.node_data.as_ref().len() - 1;
            let node_id = NodeId::new(n);
            let node_data = &styled.node_data.as_ref()[n];
            assert!(
                node_data.is_text_node(),
                "fixture: last node must be the text node"
            );
            let state = &styled.styled_nodes.as_ref()[n].styled_node_state;
            let color = cache
                .get_text_color(node_data, &node_id, state)
                .and_then(|p| p.get_property().copied())
                .map(|c| c.inner);
            assert_eq!(
                color,
                Some(*want_color),
                "inline color lost on text node for {css_str:?}"
            );
            let size = cache
                .get_font_size(node_data, &node_id, state)
                .and_then(|p| p.get_property().copied())
                .map(|s| s.inner.to_pixels_internal(16.0, 16.0, 16.0) as isize);
            assert_eq!(
                size,
                Some(*want_px),
                "inline font-size lost on text node for {css_str:?}"
            );
        }
        // Negative control: a text node WITHOUT inline css keeps taking its
        // color by INHERITANCE (the parent's #444444 arrives via
        // cascaded_props) — the matcher exception must not have rerouted or
        // broken the inheritance lane.
        let dom = crate::dom::Dom::create_body().with_child(
            crate::dom::Dom::create_div()
                .with_css("color: #444444;")
                .with_child(crate::dom::Dom::create_text_do_not_use_without_block_level_wrapper("X")),
        );
        let styled = StyledDom::create_from_dom(dom);
        let cache = styled.get_css_property_cache();
        let n = styled.node_data.as_ref().len() - 1;
        let node_id = NodeId::new(n);
        let node_data = &styled.node_data.as_ref()[n];
        let state = &styled.styled_nodes.as_ref()[n].styled_node_state;
        assert!(node_data.is_text_node());
        assert!(
            cache.css_props.get_slice(n).is_empty(),
            "an unstyled text node must have no OWN css_props"
        );
        let inherited = cache
            .get_text_color(node_data, &node_id, state)
            .and_then(|p| p.get_property().copied())
            .map(|c| c.inner);
        assert_eq!(
            inherited,
            Some(ColorU { r: 0x44, g: 0x44, b: 0x44, a: 255 }),
            "inheritance must still deliver the parent's color to the text node"
        );
    }
    #[test]
    fn create_empties_the_source_dom() {
        // Documented: "After calling this function, the DOM will be reset to an empty DOM."
        let mut dom = Dom::create_body().with_children(vec![Dom::create_div(); 3].into());
        let sd = StyledDom::create(&mut dom, Css::empty());
        assert_eq!(sd.node_count(), 4);
        assert!(
            dom.children.as_ref().is_empty(),
            "the source Dom must be left empty (it is swapped out, not cloned)"
        );
    }
    #[test]
    fn create_keeps_every_parallel_array_the_same_length() {
        for n in [0usize, 1, 3, 64] {
            let sd = flat_body(n);
            let count = sd.node_count();
            assert_eq!(count, n + 1);
            assert_eq!(sd.node_hierarchy.as_ref().len(), count);
            assert_eq!(sd.styled_nodes.as_ref().len(), count);
            assert_eq!(sd.cascade_info.as_ref().len(), count);
        }
    }
    #[test]
    fn create_survives_malformed_truncated_and_unicode_css() {
        let cases: Vec<String> = vec![
            String::new(),
            "}}}{{{".to_string(),
            "div {".to_string(),
            "div { color: }".to_string(),
            "div { : red; }".to_string(),
            "@media".to_string(),
            "/* unterminated comment".to_string(),
            "div { width: 99999999999999999999999px; }".to_string(),
            "div { width: -0px; opacity: 1e400; }".to_string(),
            "div { width: NaNpx; height: infpx; }".to_string(),
            "* { color: #ZZZZZZ; }".to_string(),
            "日本語 { content: \"🦀\"; }".to_string(),
            ".\u{202e}rtl { color: red; }".to_string(),
            "a".repeat(10_000),
            "div { color: red; }".repeat(500),
        ];
        for case in &cases {
            let css = parse_css(case);
            let mut dom = Dom::create_body().with_children(vec![Dom::create_div()].into());
            let sd = StyledDom::create(&mut dom, css);
            assert_eq!(
                sd.node_count(),
                2,
                "CSS must never change the node count; failing input: {case:?}"
            );
        }
    }
    #[test]
    fn create_handles_deep_and_wide_doms() {
        // deep: 64 nested divs under a body
        let mut deep = Dom::create_div();
        for _ in 0..63 {
            deep = Dom::create_div().with_children(vec![deep].into());
        }
        let mut deep_body = Dom::create_body().with_children(vec![deep].into());
        let sd = StyledDom::create(&mut deep_body, Css::empty());
        assert_eq!(sd.node_count(), 65);
        assert_eq!(
            sd.non_leaf_nodes.as_ref().len(),
            64,
            "every node except the innermost leaf is a parent"
        );
        // wide: 1000 siblings
        let wide = flat_body(1000);
        assert_eq!(wide.node_count(), 1001);
        assert_eq!(wide.node_hierarchy.as_container().subtree_len(NodeId::ZERO), 1000);
        assert_eq!(wide.non_leaf_nodes.as_ref().len(), 1);
    }
    #[test]
    fn create_from_dom_collects_scoped_css_without_changing_the_tree() {
        let dom = Dom::create_body().with_children(
            vec![
                Dom::create_div().with_css("color: red"),
                Dom::create_div().with_children(vec![Dom::create_div().with_css("width: 5px")].into()),
            ]
            .into(),
        );
        let sd = StyledDom::create_from_dom(dom);
        assert_eq!(sd.node_count(), 4);
        assert_eq!(sd.node_hierarchy.as_ref().len(), 4);
        assert!(sd.get_css_property_cache().compact_cache.is_some());
    }
    #[test]
    fn create_from_dom_on_a_bare_leaf_produces_one_node() {
        let sd = StyledDom::create_from_dom(Dom::create_div());
        assert_eq!(sd.node_count(), 1);
        assert_eq!(sd.root.into_crate_internal(), Some(NodeId::ZERO));
    }
    // ---------------------------------------------------------------------
    // append_child / append_child_with_index / finalize / with_child
    // ---------------------------------------------------------------------
    #[test]
    fn append_child_grows_the_node_count_by_the_child_dom_size() {
        let mut base = flat_body(2);
        base.append_child(flat_body(3));
        assert_eq!(base.node_count(), 3 + 4);
        assert_eq!(base.node_hierarchy.as_ref().len(), 7);
        assert_eq!(base.styled_nodes.as_ref().len(), 7);
        assert_eq!(base.cascade_info.as_ref().len(), 7);
    }
    #[test]
    fn append_child_links_the_new_root_as_the_last_sibling() {
        // Flat parent: body(0) > [div(1), div(2)], then append a 1-node StyledDom.
        let mut base = flat_body(2);
        base.append_child(StyledDom::default());
        let h = base.node_hierarchy.as_container();
        let children: Vec<NodeId> = NodeId::ZERO.az_children(&h).collect();
        assert_eq!(
            children,
            vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)],
            "the appended root must become the last direct child"
        );
        assert_eq!(h[NodeId::new(3)].parent_id(), Some(NodeId::ZERO));
        assert_eq!(h[NodeId::new(3)].previous_sibling_id(), Some(NodeId::new(2)));
        assert_eq!(h[NodeId::new(3)].next_sibling_id(), None);
    }
    /// ADVERSARIAL: `append_child` reads `last_child_id()` to find the current
    /// last sibling. If `last_child` names a *descendant* rather than the last
    /// *direct child*, the appended root is spliced into the wrong sibling chain
    /// and disappears from the root's children.
    #[test]
    fn append_child_keeps_the_root_children_reachable_for_a_nested_dom() {
        let mut base = nested_body(); // body(0) > div(1) > div(2)
        base.append_child(StyledDom::default());
        assert_eq!(base.node_count(), 4);
        let h = base.node_hierarchy.as_container();
        let children: Vec<NodeId> = NodeId::ZERO.az_children(&h).collect();
        assert_eq!(
            children,
            vec![NodeId::new(1), NodeId::new(3)],
            "after append_child the root must have exactly its old child plus the appended root"
        );
    }
    #[test]
    fn append_child_with_index_saturates_the_u32_cascade_index() {
        for (child_index, expected) in [
            (0usize, 0u32),
            (7, 7),
            (u32::MAX as usize, u32::MAX),
            (u32::MAX as usize + 1, u32::MAX),
            (usize::MAX, u32::MAX),
        ] {
            let mut base = flat_body(0); // single body node
            base.append_child_with_index(StyledDom::default(), child_index);
            // The appended root lands at index self_len == 1 in the merged arrays.
            assert_eq!(
                base.cascade_info.as_ref()[1].index_in_parent,
                expected,
                "child_index {child_index} must saturate to {expected}, never wrap"
            );
            assert!(base.cascade_info.as_ref()[1].is_last_child);
            assert_eq!(base.node_count(), 2);
        }
    }
    #[test]
    fn finalize_non_leaf_nodes_sorts_by_depth_and_is_idempotent() {
        let mut base = flat_body(1);
        base.append_child_with_index(flat_body(2), 1);
        base.append_child_with_index(flat_body(2), 2);
        base.finalize_non_leaf_nodes();
        let depths: Vec<usize> = base.non_leaf_nodes.as_ref().iter().map(|p| p.depth).collect();
        let mut sorted = depths.clone();
        sorted.sort_unstable();
        assert_eq!(depths, sorted, "non_leaf_nodes must be depth-ordered");
        base.finalize_non_leaf_nodes();
        let again: Vec<usize> = base.non_leaf_nodes.as_ref().iter().map(|p| p.depth).collect();
        assert_eq!(depths, again, "finalize must be idempotent");
    }
    #[test]
    fn with_child_matches_append_child() {
        let mut appended = flat_body(2);
        appended.append_child(flat_body(1));
        let built = flat_body(2).with_child(flat_body(1));
        assert_eq!(built.node_count(), appended.node_count());
        assert_eq!(
            built.node_hierarchy.as_ref(),
            appended.node_hierarchy.as_ref()
        );
    }
    #[test]
    fn swap_with_default_returns_the_old_dom_and_resets_self() {
        let mut sd = flat_body(3);
        let old = sd.swap_with_default();
        assert_eq!(old.node_count(), 4);
        assert_eq!(sd.node_count(), 1, "self must be left as the default StyledDom");
        assert_eq!(sd.root.into_crate_internal(), Some(NodeId::ZERO));
    }
    // ---------------------------------------------------------------------
    // Menus
    // ---------------------------------------------------------------------
    #[test]
    fn context_menu_and_menu_bar_are_stored_on_the_root_node() {
        let mut sd = flat_body(1);
        assert!(sd.node_data.as_container()[NodeId::ZERO].get_context_menu().is_none());
        sd.set_context_menu(empty_menu());
        sd.set_menu_bar(empty_menu());
        let data = sd.node_data.as_container();
        assert!(data[NodeId::ZERO].get_context_menu().is_some());
        assert!(data[NodeId::ZERO].get_menu_bar().is_some());
        // ...and the child must not have inherited either of them.
        assert!(data[NodeId::new(1)].get_context_menu().is_none());
        assert!(data[NodeId::new(1)].get_menu_bar().is_none());
    }
    #[test]
    fn menu_builders_are_equivalent_to_the_setters_and_dont_touch_the_tree() {
        let sd = StyledDom::default()
            .with_context_menu(empty_menu())
            .with_menu_bar(empty_menu());
        assert_eq!(sd.node_count(), 1);
        let data = sd.node_data.as_container();
        assert!(data[NodeId::ZERO].get_context_menu().is_some());
        assert!(data[NodeId::ZERO].get_menu_bar().is_some());
    }
    // ---------------------------------------------------------------------
    // restyle_nodes_* / restyle_on_state_change / restyle_user_property
    // ---------------------------------------------------------------------
    #[test]
    fn restyle_nodes_hover_sets_and_clears_the_state_flag() {
        let mut sd = flat_body(2);
        let _ = sd.restyle_nodes_hover(&[NodeId::new(1)], true);
        assert!(sd.get_styled_node_state(&NodeId::new(1)).hover);
        assert!(!sd.get_styled_node_state(&NodeId::new(2)).hover);
        let _ = sd.restyle_nodes_hover(&[NodeId::new(1)], false);
        assert!(!sd.get_styled_node_state(&NodeId::new(1)).hover);
        assert!(sd.get_styled_node_state(&NodeId::new(1)).is_normal());
    }
    #[test]
    fn restyle_nodes_active_and_focus_set_independent_flags() {
        let mut sd = flat_body(1);
        let _ = sd.restyle_nodes_active(&[NodeId::ZERO], true);
        let _ = sd.restyle_nodes_focus(&[NodeId::ZERO], true);
        let state = sd.get_styled_node_state(&NodeId::ZERO);
        assert!(state.active);
        assert!(state.focused);
        assert!(!state.hover, "hover must be untouched");
        assert!(!state.is_normal());
    }
    #[test]
    fn restyle_nodes_ignores_out_of_range_node_ids_instead_of_panicking() {
        let mut sd = flat_body(1); // valid ids: 0, 1
        let changed = sd.restyle_nodes_hover(&[NodeId::new(2), NodeId::new(usize::MAX)], true);
        assert!(changed.is_empty());
        assert!(!sd.get_styled_node_state(&NodeId::ZERO).hover);
        assert!(!sd.get_styled_node_state(&NodeId::new(1)).hover);
        // A mix of valid and stale ids must still apply the valid ones.
        let _ = sd.restyle_nodes_hover(&[NodeId::new(1), NodeId::new(999)], true);
        assert!(sd.get_styled_node_state(&NodeId::new(1)).hover);
    }
    #[test]
    fn restyle_nodes_handles_empty_and_duplicated_input() {
        let mut sd = flat_body(1);
        assert!(sd.restyle_nodes_focus(&[], true).is_empty());
        // Duplicates must be idempotent, not double-applied or panicking.
        let _ = sd.restyle_nodes_focus(&[NodeId::ZERO, NodeId::ZERO, NodeId::ZERO], true);
        assert!(sd.get_styled_node_state(&NodeId::ZERO).focused);
    }
    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn get_styled_node_state_panics_on_an_out_of_range_node_id() {
        // Documents the contract: unlike restyle_nodes_*, this getter does NOT
        // bounds-check — callers must pass an id that indexes into this DOM.
        let sd = flat_body(1);
        let _ = sd.get_styled_node_state(&NodeId::new(99));
    }
    #[test]
    fn restyle_on_state_change_with_no_changes_reports_nothing_to_do() {
        let mut sd = flat_body(2);
        let r = sd.restyle_on_state_change(None, None, None);
        assert!(!r.has_changes());
        assert!(!r.needs_layout);
        assert!(!r.needs_display_list);
        assert!(!r.gpu_only_changes);
        assert_eq!(r.max_relayout_scope, RelayoutScope::None);
    }
    #[test]
    fn restyle_on_state_change_tolerates_stale_node_ids() {
        let mut sd = flat_body(1);
        let r = sd.restyle_on_state_change(
            Some(FocusChange {
                lost_focus: Some(NodeId::new(500)),
                gained_focus: Some(NodeId::new(usize::MAX)),
            }),
            Some(HoverChange {
                left_nodes: vec![NodeId::new(700)],
                entered_nodes: vec![NodeId::new(800)],
            }),
            Some(ActiveChange {
                deactivated: vec![NodeId::new(900)],
                activated: vec![NodeId::new(1000)],
            }),
        );
        assert!(!r.has_changes(), "stale ids must be filtered, not applied");
        assert_eq!(sd.node_count(), 2);
    }
    #[test]
    fn restyle_on_state_change_applies_state_to_valid_nodes() {
        let mut sd = flat_body(1);
        let r = sd.restyle_on_state_change(
            None,
            Some(HoverChange {
                left_nodes: Vec::new(),
                entered_nodes: vec![NodeId::new(1)],
            }),
            None,
        );
        assert!(sd.get_styled_node_state(&NodeId::new(1)).hover);
        assert!(
            r.changed_nodes.keys().all(|n| *n == NodeId::new(1)),
            "only the node whose state actually changed may be reported"
        );
    }
    /// A geometry patch must leave the compact cache PRESENT and already
    /// reflecting the override. The old behaviour (drop the cache, "the next
    /// full cascade rebuilds it") broke every consumer that derives state
    /// from the cache during the very relayout that applies the patch: the
    /// font phase resolved an EMPTY font world (no stack signature, no
    /// chains, empty GC keep-set), so the patched-in subtree's text laid out
    /// at zero size and, pre-guard, the font GC evicted every loaded font
    /// mid-frame.
    #[test]
    fn restyle_user_property_rebuilds_the_compact_cache_with_the_patch() {
        use azul_css::props::layout::display::LayoutDisplay;
        use azul_css::props::property::CssProperty;
        let mut sd = flat_body(2);
        let node = NodeId::new(1);
        // Sanity: the fixture starts with a compact cache.
        assert!(
            sd.get_css_property_cache().compact_cache.is_some(),
            "fixture should carry a compact cache"
        );
        let changes = sd.restyle_user_property(
            &node,
            &[CssProperty::const_display(LayoutDisplay::None)],
        );
        assert!(!changes.is_empty(), "display default -> none must report a change");
        let cc = sd
            .get_css_property_cache()
            .compact_cache
            .as_ref()
            .expect("compact cache must be REBUILT by a geometry patch, not dropped");
        assert_eq!(
            cc.get_display(node.index()),
            LayoutDisplay::None,
            "the rebuilt compact cache must already reflect the patched value"
        );
        // And back again - repeated patches keep rebuilding, not accumulating.
        let _ = sd.restyle_user_property(
            &node,
            &[CssProperty::const_display(LayoutDisplay::Flex)],
        );
        let cc = sd
            .get_css_property_cache()
            .compact_cache
            .as_ref()
            .expect("second patch keeps the cache present");
        assert_eq!(cc.get_display(node.index()), LayoutDisplay::Flex);
    }
    #[test]
    fn restyle_user_property_rejects_empty_lists_and_stale_nodes() {
        let mut sd = flat_body(1);
        assert!(sd.restyle_user_property(&NodeId::ZERO, &[]).is_empty());
        assert!(
            sd.restyle_user_property(
                &NodeId::new(50),
                &[CssProperty::auto(CssPropertyType::Width)]
            )
            .is_empty(),
            "an out-of-range node id must be a no-op, not a panic"
        );
        assert!(
            sd.get_css_property_cache()
                .user_overridden_properties
                .iter()
                .all(Vec::is_empty),
            "a rejected call must not record an override"
        );
    }
    #[test]
    fn restyle_user_property_stores_the_override_and_initial_removes_it() {
        let mut sd = flat_body(1);
        let node = NodeId::ZERO;
        let _ = sd.restyle_user_property(&node, &[CssProperty::auto(CssPropertyType::Width)]);
        {
            let overrides = &sd.get_css_property_cache().user_overridden_properties;
            assert_eq!(overrides.len(), sd.node_count(), "table grows to cover the DOM");
            assert_eq!(overrides[0].len(), 1);
            assert_eq!(overrides[0][0].0, CssPropertyType::Width);
        }
        // Re-setting the same type replaces rather than duplicating.
        let _ = sd.restyle_user_property(&node, &[CssProperty::none(CssPropertyType::Width)]);
        assert_eq!(sd.get_css_property_cache().user_overridden_properties[0].len(), 1);
        // CssProperty::Initial removes the override again.
        let _ = sd.restyle_user_property(&node, &[CssProperty::initial(CssPropertyType::Width)]);
        assert!(sd.get_css_property_cache().user_overridden_properties[0].is_empty());
        // Removing a property that was never set must not panic.
        let _ = sd.restyle_user_property(&node, &[CssProperty::initial(CssPropertyType::Height)]);
        assert!(sd.get_css_property_cache().user_overridden_properties[0].is_empty());
    }
    #[test]
    fn restyle_and_recompute_preserve_the_tree_and_rebuild_the_compact_cache() {
        let mut sd = flat_body(3);
        let before = sd.node_count();
        sd.restyle(parse_css("div { color: red; } body > div:hover { color: blue; }"));
        assert_eq!(sd.node_count(), before);
        assert!(sd.get_css_property_cache().compact_cache.is_some());
        // A second restyle with garbage CSS must not corrupt the structure.
        sd.restyle(parse_css("}}} div { : ; }"));
        assert_eq!(sd.node_count(), before);
        sd.recompute_inheritance_and_compact_cache();
        assert_eq!(sd.node_count(), before);
        assert!(sd.get_css_property_cache().compact_cache.is_some());
    }
    #[test]
    fn get_css_property_cache_mut_sees_the_same_cache_as_the_shared_getter() {
        let mut sd = flat_body(1);
        let node_count = sd.node_count();
        sd.get_css_property_cache_mut()
            .user_overridden_properties
            .resize(node_count, Vec::new());
        assert_eq!(
            sd.get_css_property_cache().user_overridden_properties.len(),
            node_count
        );
    }
    // ---------------------------------------------------------------------
    // get_html_string
    // ---------------------------------------------------------------------
    #[test]
    fn get_html_string_test_mode_omits_the_html_wrapper() {
        let sd = flat_body(2);
        let out = sd.get_html_string("HEAD_MARK", "BODY_MARK", true);
        assert!(!out.is_empty());
        assert!(!out.contains("HEAD_MARK"), "test_mode must not emit the custom head");
        assert!(!out.contains("BODY_MARK"), "test_mode must not emit the custom body");
        assert!(!out.contains("<html>"));
    }
    #[test]
    fn get_html_string_embeds_custom_head_and_body_verbatim() {
        let sd = flat_body(1);
        let head = "🦀 <meta charset=\"utf-8\"> & ünïcödé";
        let body = "x".repeat(10_000);
        let out = sd.get_html_string(head, &body, false);
        assert!(out.contains("<html>"));
        assert!(out.contains(head));
        assert!(out.contains(&body));
    }
    #[test]
    fn get_html_string_does_not_panic_on_extreme_doms() {
        // A single-node DOM has no non_leaf parent entry for its root — the depth
        // lookup must fall back to 0 rather than panic-indexing the map.
        assert!(!StyledDom::default().get_html_string("", "", true).is_empty());
        assert!(!flat_body(0).get_html_string("", "", true).is_empty());
        assert!(!nested_body().get_html_string("", "", true).is_empty());
        assert!(!flat_body(200).get_html_string("", "", true).is_empty());
    }
    // ---------------------------------------------------------------------
    // rendering order
    // ---------------------------------------------------------------------
    #[test]
    fn get_rects_in_rendering_order_is_a_permutation_of_the_children() {
        let sd = flat_body(3);
        let group = sd.get_rects_in_rendering_order();
        assert_eq!(group.root.into_crate_internal(), Some(NodeId::ZERO));
        let mut ids: Vec<usize> = group
            .children
            .as_ref()
            .iter()
            .filter_map(|c| c.root.into_crate_internal())
            .map(|n| n.index())
            .collect();
        ids.sort_unstable();
        assert_eq!(ids, vec![1, 2, 3], "every child appears exactly once");
    }
    #[test]
    fn get_rects_in_rendering_order_nests_grandchildren() {
        let sd = nested_body(); // body(0) > div(1) > div(2)
        let group = sd.get_rects_in_rendering_order();
        assert_eq!(group.children.as_ref().len(), 1);
        let child = &group.children.as_ref()[0];
        assert_eq!(child.root.into_crate_internal(), Some(NodeId::new(1)));
        assert_eq!(child.children.as_ref().len(), 1);
        assert_eq!(
            child.children.as_ref()[0].root.into_crate_internal(),
            Some(NodeId::new(2))
        );
    }
    #[test]
    fn determine_rendering_order_with_no_parents_yields_a_childless_root() {
        let sd = StyledDom::default();
        let hierarchy = sd.node_hierarchy.as_container();
        let styled = sd.styled_nodes.as_container();
        let data = sd.node_data.as_container();
        let group = StyledDom::determine_rendering_order(
            &[],
            &hierarchy,
            &styled,
            &data,
            sd.get_css_property_cache(),
        );
        assert_eq!(group.root.into_crate_internal(), Some(NodeId::ZERO));
        assert!(group.children.as_ref().is_empty());
    }
    #[test]
    fn sort_children_by_position_returns_every_child_of_a_leaf_free_parent() {
        let sd = flat_body(3);
        let hierarchy = sd.node_hierarchy.as_container();
        let styled = sd.styled_nodes.as_container();
        let data = sd.node_data.as_container();
        let sorted = sort_children_by_position(
            NodeId::ZERO,
            &hierarchy,
            &styled,
            &data,
            sd.get_css_property_cache(),
        );
        assert_eq!(sorted.len(), 3);
        // A leaf parent has no children at all.
        let leaf = sort_children_by_position(
            NodeId::new(3),
            &hierarchy,
            &styled,
            &data,
            sd.get_css_property_cache(),
        );
        assert!(leaf.is_empty());
    }
    #[test]
    fn fill_content_group_children_builds_the_nested_group_tree() {
        let id = |i: usize| NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(i)));
        let mut sorted: BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>> = BTreeMap::new();
        sorted.insert(id(0), vec![id(1), id(2)]);
        sorted.insert(id(1), vec![id(3)]);
        let mut group = ContentGroup {
            root: id(0),
            children: Vec::new().into(),
        };
        fill_content_group_children(&mut group, &sorted);
        assert_eq!(group.children.as_ref().len(), 2);
        assert_eq!(group.children.as_ref()[0].root, id(1));
        assert_eq!(group.children.as_ref()[0].children.as_ref().len(), 1);
        assert_eq!(group.children.as_ref()[0].children.as_ref()[0].root, id(3));
        assert!(
            group.children.as_ref()[1].children.as_ref().is_empty(),
            "a node with no entry in the map is a leaf"
        );
    }
    #[test]
    fn fill_content_group_children_leaves_an_unknown_root_untouched() {
        let sorted: BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>> = BTreeMap::new();
        let mut group = ContentGroup {
            root: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(9))),
            children: Vec::new().into(),
        };
        fill_content_group_children(&mut group, &sorted);
        assert!(group.children.as_ref().is_empty());
    }
    // ---------------------------------------------------------------------
    // recursive_get_last_child / get_path_to_root
    // ---------------------------------------------------------------------
    #[test]
    fn recursive_get_last_child_descends_to_the_deepest_last_child() {
        // 0 -> 1 -> 2 (2 is a leaf)
        let items = vec![
            raw_item(0, 0, 0, 2), // node 0, last_child = NodeId(1)
            raw_item(1, 0, 0, 3), // node 1, last_child = NodeId(2)
            raw_item(2, 0, 0, 0), // node 2, leaf
        ];
        let mut target = None;
        recursive_get_last_child(NodeId::ZERO, &items, &mut target);
        assert_eq!(target, Some(NodeId::new(2)));
    }
    #[test]
    fn recursive_get_last_child_leaves_the_target_untouched_for_a_leaf() {
        let items = vec![raw_item(0, 0, 0, 0)];
        let mut target = None;
        recursive_get_last_child(NodeId::ZERO, &items, &mut target);
        assert_eq!(target, None);
        // A pre-set target is also left alone.
        let mut preset = Some(NodeId::new(7));
        recursive_get_last_child(NodeId::ZERO, &items, &mut preset);
        assert_eq!(preset, Some(NodeId::new(7)));
    }
    #[test]
    fn get_path_to_root_is_root_first_and_tolerates_unknown_nodes() {
        let sd = nested_body(); // body(0) > div(1) > div(2)
        let h = sd.node_hierarchy.as_container();
        assert_eq!(get_path_to_root(&h, NodeId::ZERO), vec![NodeId::ZERO]);
        assert_eq!(
            get_path_to_root(&h, NodeId::new(2)),
            vec![NodeId::ZERO, NodeId::new(1), NodeId::new(2)]
        );
        // An id outside the arena yields a one-element path instead of panicking.
        assert_eq!(
            get_path_to_root(&h, NodeId::new(9999)),
            vec![NodeId::new(9999)]
        );
    }
    // ---------------------------------------------------------------------
    // document order
    // ---------------------------------------------------------------------
    #[test]
    fn is_before_in_document_order_is_false_for_identical_nodes() {
        let sd = flat_body(2);
        assert!(!is_before_in_document_order(
            &sd.node_hierarchy,
            NodeId::new(1),
            NodeId::new(1)
        ));
    }
    #[test]
    fn is_before_in_document_order_orders_ancestors_and_siblings() {
        let sd = flat_body(3); // body(0) > [1, 2, 3]
        let h = &sd.node_hierarchy;
        assert!(is_before_in_document_order(h, NodeId::ZERO, NodeId::new(1)));
        assert!(!is_before_in_document_order(h, NodeId::new(1), NodeId::ZERO));
        assert!(is_before_in_document_order(h, NodeId::new(1), NodeId::new(3)));
        assert!(!is_before_in_document_order(h, NodeId::new(3), NodeId::new(1)));
    }
    #[test]
    fn is_before_in_document_order_is_antisymmetric_across_a_nested_tree() {
        let sd = nested_body();
        let h = &sd.node_hierarchy;
        for a in 0..3 {
            for b in 0..3 {
                let ab = is_before_in_document_order(h, NodeId::new(a), NodeId::new(b));
                let ba = is_before_in_document_order(h, NodeId::new(b), NodeId::new(a));
                if a == b {
                    assert!(!ab && !ba, "a node is never before itself");
                } else {
                    assert_ne!(ab, ba, "exactly one of ({a},{b}) / ({b},{a}) must hold");
                }
            }
        }
    }
    #[test]
    fn is_before_in_document_order_is_deterministic_for_unknown_nodes() {
        let sd = flat_body(1);
        let h = &sd.node_hierarchy;
        // Out-of-range ids fall back to a single-element path; the comparison must
        // still terminate and return a stable answer instead of panicking.
        assert!(is_before_in_document_order(h, NodeId::ZERO, NodeId::new(usize::MAX)));
        assert!(!is_before_in_document_order(h, NodeId::new(usize::MAX), NodeId::ZERO));
    }
    #[test]
    fn collect_nodes_in_document_order_start_equals_end() {
        let sd = flat_body(2);
        assert_eq!(
            collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::new(2), NodeId::new(2)),
            vec![NodeId::new(2)]
        );
        // Even a bogus id short-circuits to itself (documented start == end path).
        assert_eq!(
            collect_nodes_in_document_order(
                &sd.node_hierarchy,
                NodeId::new(usize::MAX),
                NodeId::new(usize::MAX)
            ),
            vec![NodeId::new(usize::MAX)]
        );
    }
    #[test]
    fn collect_nodes_in_document_order_walks_the_tree_in_pre_order() {
        let sd = flat_body(3); // body(0) > [1, 2, 3]
        assert_eq!(
            collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::ZERO, NodeId::new(3)),
            vec![NodeId::ZERO, NodeId::new(1), NodeId::new(2), NodeId::new(3)]
        );
        assert_eq!(
            collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::new(1), NodeId::new(2)),
            vec![NodeId::new(1), NodeId::new(2)]
        );
        // Nested: body(0) > div(1) > div(2) — pre-order is 0, 1, 2.
        let nested = nested_body();
        assert_eq!(
            collect_nodes_in_document_order(&nested.node_hierarchy, NodeId::ZERO, NodeId::new(2)),
            vec![NodeId::ZERO, NodeId::new(1), NodeId::new(2)]
        );
    }
    #[test]
    fn collect_nodes_in_document_order_terminates_when_end_precedes_start() {
        // The traversal hits `end` before it ever enters the range, so it bails
        // out with an empty result rather than looping forever.
        let sd = flat_body(3);
        let out = collect_nodes_in_document_order(&sd.node_hierarchy, NodeId::new(2), NodeId::new(1));
        assert!(out.is_empty());
    }
    #[test]
    fn collect_nodes_in_document_order_with_an_unreachable_end_stops_at_the_tree_end() {
        let sd = flat_body(3);
        let out = collect_nodes_in_document_order(
            &sd.node_hierarchy,
            NodeId::new(1),
            NodeId::new(usize::MAX),
        );
        assert_eq!(
            out,
            vec![NodeId::new(1), NodeId::new(2), NodeId::new(3)],
            "an end node that is never reached must terminate at the end of the traversal"
        );
    }
    // ---------------------------------------------------------------------
    // is_layout_equivalent
    // ---------------------------------------------------------------------
    #[test]
    fn is_layout_equivalent_holds_for_independently_built_identical_doms() {
        assert!(is_layout_equivalent(&flat_body(3), &flat_body(3)));
        assert!(is_layout_equivalent(
            &StyledDom::default(),
            &StyledDom::default()
        ));
        assert!(is_layout_equivalent(&nested_body(), &nested_body()));
    }
    #[test]
    fn is_layout_equivalent_rejects_a_different_node_count() {
        assert!(!is_layout_equivalent(&flat_body(3), &flat_body(4)));
        assert!(!is_layout_equivalent(&flat_body(0), &flat_body(1)));
    }
    #[test]
    fn is_layout_equivalent_rejects_a_different_structure() {
        // Same node count (3), different shape: [body > div > div] vs [body > div, div]
        assert!(!is_layout_equivalent(&nested_body(), &flat_body(2)));
    }
    #[test]
    fn is_layout_equivalent_rejects_a_changed_class() {
        let build = |class: &str| {
            let mut dom = Dom::create_body().with_children(
                vec![Dom::create_div().with_class(class.to_string().into())].into(),
            );
            StyledDom::create(&mut dom, Css::empty())
        };
        assert!(is_layout_equivalent(&build("a"), &build("a")));
        assert!(!is_layout_equivalent(&build("a"), &build("b")));
    }
    #[test]
    fn is_layout_equivalent_rejects_a_changed_pseudo_state() {
        let base = flat_body(2);
        let mut hovered = flat_body(2);
        let _ = hovered.restyle_nodes_hover(&[NodeId::new(1)], true);
        assert!(
            !is_layout_equivalent(&base, &hovered),
            ":hover changes CSS resolution, so the DOMs are not layout-equivalent"
        );
    }
    // ---------------------------------------------------------------------
    // CompactDom + convert_dom_into_compact_dom
    // ---------------------------------------------------------------------
    #[test]
    fn compact_dom_len_and_is_empty() {
        let single = convert_dom_into_compact_dom(Dom::create_div());
        assert_eq!(single.len(), 1);
        assert!(!single.is_empty());
        let tree = convert_dom_into_compact_dom(
            Dom::create_body().with_children(vec![Dom::create_div(); 4].into()),
        );
        assert_eq!(tree.len(), 5);
        assert!(!tree.is_empty());
        // A hand-built zero-node arena is the only way to observe is_empty() == true.
        let empty = CompactDom {
            node_hierarchy: NodeHierarchy {
                internal: Vec::new(),
            },
            node_data: NodeDataContainer {
                internal: Vec::new(),
            },
            root: NodeId::ZERO,
        };
        assert_eq!(empty.len(), 0);
        assert!(empty.is_empty());
    }
    #[test]
    fn convert_dom_into_compact_dom_links_flat_siblings() {
        let compact = convert_dom_into_compact_dom(
            Dom::create_body().with_children(vec![Dom::create_div(); 3].into()),
        );
        assert_eq!(compact.len(), 4);
        assert_eq!(compact.root, NodeId::ZERO);
        let h = compact.node_hierarchy.as_ref();
        assert_eq!(h[NodeId::ZERO].parent, None);
        assert_eq!(h[NodeId::ZERO].last_child, Some(NodeId::new(3)));
        for i in 1..=3usize {
            assert_eq!(h[NodeId::new(i)].parent, Some(NodeId::ZERO));
            let expected_next = if i == 3 { None } else { Some(NodeId::new(i + 1)) };
            assert_eq!(h[NodeId::new(i)].next_sibling, expected_next);
            let expected_prev = if i == 1 { None } else { Some(NodeId::new(i - 1)) };
            assert_eq!(h[NodeId::new(i)].previous_sibling, expected_prev);
            assert_eq!(h[NodeId::new(i)].last_child, None, "the children are leaves");
        }
    }
    /// ADVERSARIAL: `last_child` must name the last DIRECT child — that is the
    /// contract `NodeHierarchyItem::last_child_id()` documents, the one
    /// `az_reverse_children` walks backwards from, and the one `append_child`
    /// splices new siblings onto. The flat encoding computes it as
    /// `node_id + estimated_total_children`, which is the last node of the whole
    /// SUBTREE — those coincide only when the last direct child is a leaf.
    #[test]
    fn convert_dom_into_compact_dom_last_child_is_the_last_direct_child() {
        // body(0) > div(1) > div(2): the body's only direct child is node 1.
        let sd = nested_body();
        let h = sd.node_hierarchy.as_container();
        let last_direct_child = NodeId::ZERO.az_children(&h).last();
        assert_eq!(last_direct_child, Some(NodeId::new(1)));
        assert_eq!(
            h[NodeId::ZERO].last_child_id(),
            last_direct_child,
            "last_child_id() must agree with the forward child iteration"
        );
    }
    #[test]
    fn convert_dom_into_compact_dom_handles_an_empty_and_a_deep_tree() {
        assert_eq!(convert_dom_into_compact_dom(Dom::create_body()).len(), 1);
        let mut deep = Dom::create_div();
        for _ in 0..64 {
            deep = Dom::create_div().with_children(vec![deep].into());
        }
        let compact = convert_dom_into_compact_dom(deep);
        assert_eq!(compact.len(), 65);
        // Pre-order ids: every node's parent is the node right before it.
        let h = compact.node_hierarchy.as_ref();
        for i in 1..65usize {
            assert_eq!(h[NodeId::new(i)].parent, Some(NodeId::new(i - 1)));
        }
    }
    // ---------------------------------------------------------------------
    // scope_inline_css / collect_css_from_dom / strip_css_from_dom
    // ---------------------------------------------------------------------
    #[test]
    fn scope_inline_css_advances_next_id_once_per_node() {
        let mut dom = Dom::create_body().with_children(
            vec![
                Dom::create_div().with_children(vec![Dom::create_div()].into()),
                Dom::create_div(),
            ]
            .into(),
        );
        let _ = dom.fixup_children_estimated();
        let mut next = 0usize;
        scope_inline_css(&mut dom, &mut next);
        assert_eq!(next, 4, "4 nodes → the counter must land on 4 (pre-order ids 0..3)");
    }
    #[test]
    fn scope_inline_css_from_zero_and_from_a_large_offset() {
        let mut leaf = Dom::create_div();
        let _ = leaf.fixup_children_estimated();
        let mut next = 0usize;
        scope_inline_css(&mut leaf, &mut next);
        assert_eq!(next, 1, "a single leaf consumes exactly one id");
        // A large (but non-saturating) starting id must not panic or wrap.
        let mut dom = Dom::create_body().with_children(vec![Dom::create_div(); 2].into());
        let _ = dom.fixup_children_estimated();
        let mut big = 1_000_000usize;
        scope_inline_css(&mut dom, &mut big);
        assert_eq!(big, 1_000_003);
    }
    #[test]
    fn scope_inline_css_preserves_the_rule_count_of_every_node() {
        let mut dom = Dom::create_body()
            .with_css("color: red")
            .with_children(vec![Dom::create_div().with_css("width: 5px")].into());
        let _ = dom.fixup_children_estimated();
        let rules_before: usize = dom
            .css
            .as_ref()
            .iter()
            .map(|c| c.rules.as_ref().len())
            .sum::<usize>()
            + dom.children.as_ref()[0]
                .css
                .as_ref()
                .iter()
                .map(|c| c.rules.as_ref().len())
                .sum::<usize>();
        assert!(rules_before > 0, "with_css must produce at least one rule");
        let mut next = 0usize;
        scope_inline_css(&mut dom, &mut next);
        let rules_after: usize = dom
            .css
            .as_ref()
            .iter()
            .map(|c| c.rules.as_ref().len())
            .sum::<usize>()
            + dom.children.as_ref()[0]
                .css
                .as_ref()
                .iter()
                .map(|c| c.rules.as_ref().len())
                .sum::<usize>();
        assert_eq!(
            rules_before, rules_after,
            "scoping rewrites paths in place; it must not add or drop rules"
        );
        assert_eq!(next, 2);
    }
    #[test]
    fn collect_css_from_dom_yields_inner_css_before_outer_css() {
        let outer = parse_css("div { color: red; } span { color: blue; }");
        let inner = parse_css("p { color: green; }");
        let outer_rules = outer.rules.as_ref().len();
        let inner_rules = inner.rules.as_ref().len();
        assert_ne!(
            outer_rules, inner_rules,
            "the two stylesheets must be distinguishable by rule count"
        );
        let mut child = Dom::create_div();
        child.add_component_css(inner);
        let mut dom = Dom::create_body().with_children(vec![child].into());
        dom.add_component_css(outer);
        let mut out = Vec::new();
        collect_css_from_dom(&dom, &mut out);
        assert_eq!(out.len(), 2);
        assert_eq!(
            out[0].rules.as_ref().len(),
            inner_rules,
            "deeper CSS is collected first (lower cascade priority)"
        );
        assert_eq!(out[1].rules.as_ref().len(), outer_rules);
    }
    #[test]
    fn collect_css_from_dom_on_a_css_free_tree_appends_nothing() {
        let dom = Dom::create_body().with_children(vec![Dom::create_div(); 3].into());
        let mut out = Vec::new();
        collect_css_from_dom(&dom, &mut out);
        assert!(out.is_empty());
        // ...and an already-populated `out` is appended to, not replaced.
        let mut prefilled = vec![Css::empty()];
        collect_css_from_dom(&dom, &mut prefilled);
        assert_eq!(prefilled.len(), 1);
    }
    #[test]
    fn strip_css_from_dom_clears_every_node_recursively() {
        let mut dom = Dom::create_body()
            .with_css("color: red")
            .with_children(
                vec![Dom::create_div()
                    .with_css("width: 5px")
                    .with_children(vec![Dom::create_div().with_css("height: 5px")].into())]
                .into(),
            );
        assert!(!dom.css.as_ref().is_empty());
        strip_css_from_dom(&mut dom);
        assert!(dom.css.as_ref().is_empty());
        let child = &dom.children.as_ref()[0];
        assert!(child.css.as_ref().is_empty());
        assert!(child.children.as_ref()[0].css.as_ref().is_empty());
        // Idempotent.
        strip_css_from_dom(&mut dom);
        assert!(dom.css.as_ref().is_empty());
    }
}