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, PartialEq)]
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)]
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)]
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
12
    pub fn has_changes(&self) -> bool {
166
12
        !self.changed_nodes.is_empty()
167
12
    }
168

            
169
    /// Merge another RestyleResult into this one
170
17
    pub fn merge(&mut self, other: RestyleResult) {
171
34
        for (node_id, changes) in other.changed_nodes {
172
17
            self.changed_nodes.entry(node_id).or_default().extend(changes);
173
17
        }
174
17
        self.needs_layout = self.needs_layout || other.needs_layout;
175
17
        self.needs_display_list = self.needs_display_list || other.needs_display_list;
176
17
        self.gpu_only_changes = self.gpu_only_changes && other.gpu_only_changes;
177
        // Keep the highest (most expensive) scope
178
17
        if other.max_relayout_scope > self.max_relayout_scope {
179
            self.max_relayout_scope = other.max_relayout_scope;
180
17
        }
181
17
    }
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 core::fmt::Debug for StyledNodeState {
214
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
215
        let mut v = Vec::new();
216
        if self.hover {
217
            v.push("hover");
218
        }
219
        if self.active {
220
            v.push("active");
221
        }
222
        if self.focused {
223
            v.push("focused");
224
        }
225
        if self.disabled {
226
            v.push("disabled");
227
        }
228
        if self.checked {
229
            v.push("checked");
230
        }
231
        if self.focus_within {
232
            v.push("focus_within");
233
        }
234
        if self.visited {
235
            v.push("visited");
236
        }
237
        if self.backdrop {
238
            v.push("backdrop");
239
        }
240
        if self.dragging {
241
            v.push("dragging");
242
        }
243
        if self.drag_over {
244
            v.push("drag_over");
245
        }
246
        if v.is_empty() {
247
            v.push("normal");
248
        }
249
        write!(f, "{:?}", v)
250
    }
251
}
252

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

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

            
288
    /// Returns true if no special state is active (just normal)
289
4408572
    pub fn is_normal(&self) -> bool {
290
4408572
        !self.hover
291
4408572
            && !self.active
292
4408572
            && !self.focused
293
4408572
            && !self.disabled
294
4408572
            && !self.checked
295
4408572
            && !self.focus_within
296
4408572
            && !self.visited
297
4408572
            && !self.backdrop
298
4408572
            && !self.dragging
299
4408572
            && !self.drag_over
300
4408572
    }
301

            
302
    /// Create from PseudoStateFlags
303
    pub fn from_pseudo_state_flags(flags: &azul_css::dynamic_selector::PseudoStateFlags) -> Self {
304
        StyledNodeState {
305
            hover: flags.hover,
306
            active: flags.active,
307
            focused: flags.focused,
308
            disabled: flags.disabled,
309
            checked: flags.checked,
310
            focus_within: flags.focus_within,
311
            visited: flags.visited,
312
            backdrop: flags.backdrop,
313
            dragging: flags.dragging,
314
            drag_over: flags.drag_over,
315
        }
316
    }
317
}
318

            
319
/// A styled Dom node
320
#[repr(C)]
321
#[derive(Debug, Default, Clone, PartialEq, PartialOrd)]
322
pub struct StyledNode {
323
    /// Current state of this styled node (used later for caching the style / layout)
324
    pub styled_node_state: StyledNodeState,
325
}
326

            
327
impl_option!(
328
    StyledNode,
329
    OptionStyledNode,
330
    copy = false,
331
    [Debug, Clone, PartialEq, PartialOrd]
332
);
333

            
334
impl_vec!(StyledNode, StyledNodeVec, StyledNodeVecDestructor, StyledNodeVecDestructorType, StyledNodeVecSlice, OptionStyledNode);
335
impl_vec_mut!(StyledNode, StyledNodeVec);
336
impl_vec_debug!(StyledNode, StyledNodeVec);
337
impl_vec_partialord!(StyledNode, StyledNodeVec);
338
impl_vec_clone!(StyledNode, StyledNodeVec, StyledNodeVecDestructor);
339
impl_vec_partialeq!(StyledNode, StyledNodeVec);
340

            
341
impl StyledNodeVec {
342
    /// Returns an immutable container reference for indexed access.
343
2335088
    pub fn as_container<'a>(&'a self) -> NodeDataContainerRef<'a, StyledNode> {
344
2335088
        NodeDataContainerRef {
345
2335088
            internal: self.as_ref(),
346
2335088
        }
347
2335088
    }
348
    /// Returns a mutable container reference for indexed access.
349
425
    pub fn as_container_mut<'a>(&'a mut self) -> NodeDataContainerRefMut<'a, StyledNode> {
350
425
        NodeDataContainerRefMut {
351
425
            internal: self.as_mut(),
352
425
        }
353
425
    }
354
}
355

            
356
#[test]
357
1
fn test_css_styling_with_nested_divs() {
358
1
    let s = "
359
1
        html, body, p {
360
1
            margin: 0;
361
1
            padding: 0;
362
1
        }
363
1
        #div1 {
364
1
            border: solid black;
365
1
            height: 2in;
366
1
            position: absolute;
367
1
            top: 1in;
368
1
            width: 3in;
369
1
        }
370
1
        div div {
371
1
            background: blue;
372
1
            height: 1in;
373
1
            position: fixed;
374
1
            width: 1in;
375
1
        }
376
1
    ";
377

            
378
1
    let css = azul_css::parser2::new_from_str(s);
379
1
    let _styled_dom = Dom::create_body()
380
1
        .with_children(
381
1
            vec![Dom::create_div()
382
1
                .with_ids_and_classes(
383
1
                    vec![crate::dom::IdOrClass::Id("div1".to_string().into())].into(),
384
                )
385
1
                .with_children(vec![Dom::create_div()].into())]
386
1
            .into(),
387
        )
388
1
        .with_component_css(css.0);
389
1
}
390

            
391
/// Regression test for the calc.c "frame ≥2 loses all backgrounds" bug:
392
/// `recompute_inheritance_and_compact_cache()` must reproduce the
393
/// `hot_flags` that `create_from_compact_dom` produced on frame 1. If the
394
/// recompute path silently drops to the getters-only `build_compact_cache`
395
/// variant, `HOT_FLAG_HAS_BACKGROUND` is never written, the renderer's
396
/// `has_any_background()` negative fast-path returns false for every node,
397
/// and every painted background vanishes on the next layout pass.
398
#[test]
399
1
fn test_recompute_preserves_hot_flag_has_background() {
400
    use azul_css::compact_cache::HOT_FLAG_HAS_BACKGROUND;
401

            
402
1
    let css_str = "
403
1
        body { margin: 0; padding: 0; }
404
1
        .painted { background: red; width: 100px; height: 100px; }
405
1
    ";
406
1
    let css = azul_css::parser2::new_from_str(css_str).0;
407

            
408
1
    let mut dom = Dom::create_body().with_children(
409
1
        vec![Dom::create_div().with_class("painted".to_string().into())].into(),
410
    );
411
1
    let mut styled = StyledDom::create(&mut dom, css);
412

            
413
    // Frame 1: find the painted node by walking its hot_flags.
414
1
    let any_bg_frame1 = {
415
1
        let cache = styled
416
1
            .css_property_cache
417
1
            .ptr
418
1
            .compact_cache
419
1
            .as_ref()
420
1
            .expect("compact_cache populated by create_from_compact_dom");
421
1
        (0..styled.node_hierarchy.as_ref().len())
422
2
            .any(|i| cache.tier2_cold[i].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0)
423
    };
424
1
    assert!(
425
1
        any_bg_frame1,
426
        "frame 1: expected HOT_FLAG_HAS_BACKGROUND on the .painted node",
427
    );
428

            
429
    // Frame 2+: simulate regenerate_layout rebuilding the compact cache.
430
    // This is the path the calculator hit on every resize tick, and the
431
    // one that had silently regressed to the getter-only builder.
432
1
    styled.recompute_inheritance_and_compact_cache();
433

            
434
1
    let any_bg_frame2 = {
435
1
        let cache = styled
436
1
            .css_property_cache
437
1
            .ptr
438
1
            .compact_cache
439
1
            .as_ref()
440
1
            .expect("compact_cache rebuilt by recompute_inheritance_and_compact_cache");
441
1
        (0..styled.node_hierarchy.as_ref().len())
442
2
            .any(|i| cache.tier2_cold[i].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0)
443
    };
444
1
    assert!(
445
1
        any_bg_frame2,
446
        "frame ≥2 after recompute_inheritance_and_compact_cache: \
447
         HOT_FLAG_HAS_BACKGROUND disappeared. The recompute path must \
448
         use build_compact_cache_with_inheritance (not plain \
449
         build_compact_cache) so apply_css_property_to_compact runs and \
450
         populates hot_flags for the renderer's negative fast-paths.",
451
    );
452
1
}
453

            
454
/// Calculated hash of a font-family
455
#[derive(Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
456
pub struct StyleFontFamilyHash(pub u64);
457

            
458
impl ::core::fmt::Debug for StyleFontFamilyHash {
459
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
460
        write!(f, "StyleFontFamilyHash({})", self.0)
461
    }
462
}
463

            
464
impl StyleFontFamilyHash {
465
    /// Computes a 64-bit hash of a font family for cache lookups.
466
    pub fn new(family: &StyleFontFamily) -> Self {
467
        use core::hash::Hasher;
468
        let mut hasher = crate::hash::DefaultHasher::new();
469
        family.hash(&mut hasher);
470
        Self(hasher.finish())
471
    }
472
}
473

            
474
/// Calculated hash of a font-family
475
#[derive(Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
476
pub struct StyleFontFamiliesHash(pub u64);
477

            
478
impl ::core::fmt::Debug for StyleFontFamiliesHash {
479
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
480
        write!(f, "StyleFontFamiliesHash({})", self.0)
481
    }
482
}
483

            
484
impl StyleFontFamiliesHash {
485
    /// Computes a 64-bit hash of multiple font families for cache lookups.
486
    pub fn new(families: &[StyleFontFamily]) -> Self {
487
        use core::hash::Hasher;
488
        let mut hasher = crate::hash::DefaultHasher::new();
489
        for f in families.iter() {
490
            f.hash(&mut hasher);
491
        }
492
        Self(hasher.finish())
493
    }
494
}
495

            
496
/// FFI-safe representation of `Option<NodeId>` as a single `usize`.
497
///
498
/// # Encoding (1-based)
499
///
500
/// - `inner = 0` → `None` (no node)
501
/// - `inner = n > 0` → `Some(NodeId(n - 1))`
502
///
503
/// This type exists because C/C++ cannot use Rust's `Option` type.
504
/// Use [`NodeHierarchyItemId::into_crate_internal`] to decode and
505
/// [`NodeHierarchyItemId::from_crate_internal`] to encode.
506
///
507
/// # Difference from `NodeId`
508
///
509
/// - **`NodeId`**: A 0-based array index. `NodeId::new(0)` refers to the first node.
510
///   Use directly for array indexing: `nodes[node_id.index()]`.
511
///
512
/// - **`NodeHierarchyItemId`**: A 1-based encoded `Option<NodeId>`.
513
///   `inner = 0` means `None`, `inner = 1` means `Some(NodeId(0))`.
514
///   **Never use `inner` as an array index!** Always decode first.
515
///
516
/// # Warning
517
///
518
/// The `inner` field uses **1-based encoding**, not a direct index!
519
/// Never use `inner` directly as an array index - always decode first.
520
///
521
/// # Example
522
///
523
/// ```ignore
524
/// // Encoding: Option<NodeId> -> NodeHierarchyItemId
525
/// let opt = NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(5)));
526
/// assert_eq!(opt.into_raw(), 6);  // 5 + 1 = 6
527
///
528
/// // Decoding: NodeHierarchyItemId -> Option<NodeId>
529
/// let decoded = opt.into_crate_internal();
530
/// assert_eq!(decoded, Some(NodeId::new(5)));
531
///
532
/// // None case
533
/// let none = NodeHierarchyItemId::NONE;
534
/// assert_eq!(none.into_raw(), 0);
535
/// assert_eq!(none.into_crate_internal(), None);
536
/// ```
537
#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
538
#[repr(C)]
539
pub struct NodeHierarchyItemId {
540
    // Uses 1-based encoding: 0 = None, n > 0 = Some(NodeId(n-1))
541
    // Do NOT use directly as an array index!
542
    inner: usize,
543
}
544

            
545
impl fmt::Debug for NodeHierarchyItemId {
546
84
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
547
84
        match self.into_crate_internal() {
548
84
            Some(n) => write!(f, "Some(NodeId({}))", n),
549
            None => write!(f, "None"),
550
        }
551
84
    }
552
}
553

            
554
impl fmt::Display for NodeHierarchyItemId {
555
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
556
        write!(f, "{:?}", self)
557
    }
558
}
559

            
560
impl NodeHierarchyItemId {
561
    /// Represents `None` (no node). Encoded as `inner = 0`.
562
    pub const NONE: NodeHierarchyItemId = NodeHierarchyItemId { inner: 0 };
563

            
564
    /// Creates an `NodeHierarchyItemId` from a raw 1-based encoded value.
565
    ///
566
    /// # Warning
567
    ///
568
    /// The value must use 1-based encoding (0 = None, n = NodeId(n-1)).
569
    /// Prefer using [`NodeHierarchyItemId::from_crate_internal`] instead.
570
    #[inline]
571
    pub const fn from_raw(value: usize) -> Self {
572
        Self { inner: value }
573
    }
574

            
575
    /// Returns the raw 1-based encoded value.
576
    ///
577
    /// # Warning
578
    ///
579
    /// The returned value uses 1-based encoding. Do NOT use as an array index!
580
    #[inline]
581
    pub const fn into_raw(&self) -> usize {
582
        self.inner
583
    }
584
}
585

            
586
impl_option!(
587
    NodeHierarchyItemId,
588
    OptionNodeHierarchyItemId,
589
    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
590
);
591

            
592
impl_vec!(NodeHierarchyItemId, NodeHierarchyItemIdVec, NodeHierarchyItemIdVecDestructor, NodeHierarchyItemIdVecDestructorType, NodeHierarchyItemIdVecSlice, OptionNodeHierarchyItemId);
593
impl_vec_mut!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
594
impl_vec_debug!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
595
impl_vec_ord!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
596
impl_vec_eq!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
597
impl_vec_hash!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
598
impl_vec_partialord!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
599
impl_vec_clone!(NodeHierarchyItemId, NodeHierarchyItemIdVec, NodeHierarchyItemIdVecDestructor);
600
impl_vec_partialeq!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
601

            
602
impl NodeHierarchyItemId {
603
    /// Decodes to `Option<NodeId>` (0 = None, n > 0 = Some(NodeId(n-1))).
604
    #[inline]
605
117092
    pub const fn into_crate_internal(&self) -> Option<NodeId> {
606
117092
        NodeId::from_usize(self.inner)
607
117092
    }
608

            
609
    /// Encodes from `Option<NodeId>` (None → 0, Some(NodeId(n)) → n+1).
610
    #[inline]
611
70964
    pub const fn from_crate_internal(t: Option<NodeId>) -> Self {
612
70964
        Self {
613
70964
            inner: NodeId::into_raw(&t),
614
70964
        }
615
70964
    }
616
}
617

            
618
impl From<Option<NodeId>> for NodeHierarchyItemId {
619
    #[inline]
620
34
    fn from(opt: Option<NodeId>) -> Self {
621
34
        NodeHierarchyItemId::from_crate_internal(opt)
622
34
    }
623
}
624

            
625
impl From<NodeHierarchyItemId> for Option<NodeId> {
626
    #[inline]
627
    fn from(id: NodeHierarchyItemId) -> Self {
628
        id.into_crate_internal()
629
    }
630
}
631

            
632
#[derive(Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
633
#[repr(C)]
634
pub struct NodeHierarchyItem {
635
    pub parent: usize,
636
    pub previous_sibling: usize,
637
    pub next_sibling: usize,
638
    pub last_child: usize,
639
}
640

            
641
impl_option!(
642
    NodeHierarchyItem,
643
    OptionNodeHierarchyItem,
644
    [Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash]
645
);
646

            
647
impl NodeHierarchyItem {
648
    /// Creates a zeroed hierarchy item (no parent, siblings, or children).
649
    pub const fn zeroed() -> Self {
650
        Self {
651
            parent: 0,
652
            previous_sibling: 0,
653
            next_sibling: 0,
654
            last_child: 0,
655
        }
656
    }
657
}
658

            
659
impl From<Node> for NodeHierarchyItem {
660
26137
    fn from(node: Node) -> NodeHierarchyItem {
661
26137
        NodeHierarchyItem {
662
26137
            parent: NodeId::into_raw(&node.parent),
663
26137
            previous_sibling: NodeId::into_raw(&node.previous_sibling),
664
26137
            next_sibling: NodeId::into_raw(&node.next_sibling),
665
26137
            last_child: NodeId::into_raw(&node.last_child),
666
26137
        }
667
26137
    }
668
}
669

            
670
impl NodeHierarchyItem {
671
    /// Returns the parent node ID, if any.
672
947584
    pub fn parent_id(&self) -> Option<NodeId> {
673
947584
        NodeId::from_usize(self.parent)
674
947584
    }
675
    /// Returns the previous sibling node ID, if any.
676
255
    pub fn previous_sibling_id(&self) -> Option<NodeId> {
677
255
        NodeId::from_usize(self.previous_sibling)
678
255
    }
679
    /// Returns the next sibling node ID, if any.
680
154103
    pub fn next_sibling_id(&self) -> Option<NodeId> {
681
154103
        NodeId::from_usize(self.next_sibling)
682
154103
    }
683
    /// Returns the first child node ID (current_node_id + 1 if has children).
684
267642
    pub fn first_child_id(&self, current_node_id: NodeId) -> Option<NodeId> {
685
267642
        self.last_child_id().map(|_| current_node_id + 1)
686
267642
    }
687
    /// Returns the last child node ID, if any.
688
293025
    pub fn last_child_id(&self) -> Option<NodeId> {
689
293025
        NodeId::from_usize(self.last_child)
690
293025
    }
691
}
692

            
693
impl_vec!(NodeHierarchyItem, NodeHierarchyItemVec, NodeHierarchyItemVecDestructor, NodeHierarchyItemVecDestructorType, NodeHierarchyItemVecSlice, OptionNodeHierarchyItem);
694
impl_vec_mut!(NodeHierarchyItem, NodeHierarchyItemVec);
695
impl_vec_debug!(AzNode, NodeHierarchyItemVec);
696
impl_vec_partialord!(AzNode, NodeHierarchyItemVec);
697
impl_vec_clone!(
698
    NodeHierarchyItem,
699
    NodeHierarchyItemVec,
700
    NodeHierarchyItemVecDestructor
701
);
702
impl_vec_partialeq!(AzNode, NodeHierarchyItemVec);
703

            
704
impl NodeHierarchyItemVec {
705
    /// Returns an immutable container reference for indexed access.
706
1213899
    pub fn as_container<'a>(&'a self) -> NodeDataContainerRef<'a, NodeHierarchyItem> {
707
1213899
        NodeDataContainerRef {
708
1213899
            internal: self.as_ref(),
709
1213899
        }
710
1213899
    }
711
    /// Returns a mutable container reference for indexed access.
712
884
    pub fn as_container_mut<'a>(&'a mut self) -> NodeDataContainerRefMut<'a, NodeHierarchyItem> {
713
884
        NodeDataContainerRefMut {
714
884
            internal: self.as_mut(),
715
884
        }
716
884
    }
717
}
718

            
719
impl<'a> NodeDataContainerRef<'a, NodeHierarchyItem> {
720
    /// Returns the number of descendant nodes under the given parent.
721
    #[inline]
722
    pub fn subtree_len(&self, parent_id: NodeId) -> usize {
723
        let self_item_index = parent_id.index();
724
        let next_item_index = match self[parent_id].next_sibling_id() {
725
            None => self.len(),
726
            Some(s) => s.index(),
727
        };
728
        next_item_index - self_item_index - 1
729
    }
730
}
731

            
732
#[derive(Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash)]
733
#[repr(C)]
734
pub struct ParentWithNodeDepth {
735
    pub depth: usize,
736
    pub node_id: NodeHierarchyItemId,
737
}
738

            
739
impl_option!(
740
    ParentWithNodeDepth,
741
    OptionParentWithNodeDepth,
742
    [Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash]
743
);
744

            
745
impl core::fmt::Debug for ParentWithNodeDepth {
746
    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
747
        write!(
748
            f,
749
            "{{ depth: {}, node: {:?} }}",
750
            self.depth,
751
            self.node_id.into_crate_internal()
752
        )
753
    }
754
}
755

            
756
impl_vec!(ParentWithNodeDepth, ParentWithNodeDepthVec, ParentWithNodeDepthVecDestructor, ParentWithNodeDepthVecDestructorType, ParentWithNodeDepthVecSlice, OptionParentWithNodeDepth);
757
impl_vec_mut!(ParentWithNodeDepth, ParentWithNodeDepthVec);
758
impl_vec_debug!(ParentWithNodeDepth, ParentWithNodeDepthVec);
759
impl_vec_partialord!(ParentWithNodeDepth, ParentWithNodeDepthVec);
760
impl_vec_clone!(
761
    ParentWithNodeDepth,
762
    ParentWithNodeDepthVec,
763
    ParentWithNodeDepthVecDestructor
764
);
765
impl_vec_partialeq!(ParentWithNodeDepth, ParentWithNodeDepthVec);
766

            
767
#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
768
#[repr(C)]
769
pub struct TagIdToNodeIdMapping {
770
    // Hit-testing tag ID (not all nodes have a tag, only nodes that are hit-testable)
771
    pub tag_id: TagId,
772
    /// Node ID of the node that has a tag
773
    pub node_id: NodeHierarchyItemId,
774
    /// Whether this node has a tab-index field
775
    pub tab_index: OptionTabIndex,
776
}
777

            
778
impl_option!(
779
    TagIdToNodeIdMapping,
780
    OptionTagIdToNodeIdMapping,
781
    copy = false,
782
    [Debug, Clone, PartialEq, Eq, Ord, PartialOrd]
783
);
784

            
785
impl_vec!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec, TagIdToNodeIdMappingVecDestructor, TagIdToNodeIdMappingVecDestructorType, TagIdToNodeIdMappingVecSlice, OptionTagIdToNodeIdMapping);
786
impl_vec_mut!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
787
impl_vec_debug!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
788
impl_vec_partialord!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
789
impl_vec_clone!(
790
    TagIdToNodeIdMapping,
791
    TagIdToNodeIdMappingVec,
792
    TagIdToNodeIdMappingVecDestructor
793
);
794
impl_vec_partialeq!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
795

            
796
#[derive(Debug, Clone, PartialEq, PartialOrd)]
797
#[repr(C)]
798
pub struct ContentGroup {
799
    /// The parent of the current node group, i.e. either the root node (0)
800
    /// or the last positioned node ()
801
    pub root: NodeHierarchyItemId,
802
    /// Node ids in order of drawing
803
    pub children: ContentGroupVec,
804
}
805

            
806
impl_option!(
807
    ContentGroup,
808
    OptionContentGroup,
809
    copy = false,
810
    [Debug, Clone, PartialEq, PartialOrd]
811
);
812

            
813
impl_vec!(ContentGroup, ContentGroupVec, ContentGroupVecDestructor, ContentGroupVecDestructorType, ContentGroupVecSlice, OptionContentGroup);
814
impl_vec_mut!(ContentGroup, ContentGroupVec);
815
impl_vec_debug!(ContentGroup, ContentGroupVec);
816
impl_vec_partialord!(ContentGroup, ContentGroupVec);
817
impl_vec_clone!(ContentGroup, ContentGroupVec, ContentGroupVecDestructor);
818
impl_vec_partialeq!(ContentGroup, ContentGroupVec);
819

            
820
#[derive(Debug, PartialEq, Clone)]
821
#[repr(C)]
822
pub struct StyledDom {
823
    pub root: NodeHierarchyItemId,
824
    pub node_hierarchy: NodeHierarchyItemVec,
825
    pub node_data: NodeDataVec,
826
    pub styled_nodes: StyledNodeVec,
827
    pub cascade_info: CascadeInfoVec,
828
    pub nodes_with_window_callbacks: NodeHierarchyItemIdVec,
829
    pub nodes_with_datasets: NodeHierarchyItemIdVec,
830
    pub tag_ids_to_node_ids: TagIdToNodeIdMappingVec,
831
    pub non_leaf_nodes: ParentWithNodeDepthVec,
832
    pub css_property_cache: CssPropertyCachePtr,
833
    /// The ID of this DOM in the layout tree (for multi-DOM support with VirtualViews)
834
    pub dom_id: DomId,
835
}
836
impl_option!(
837
    StyledDom,
838
    OptionStyledDom,
839
    copy = false,
840
    [Debug, Clone, PartialEq]
841
);
842

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

            
872
/// Per-field heap-byte breakdown of a `StyledDom`.
873
#[derive(Debug, Clone, Default)]
874
pub struct StyledDomMemoryReport {
875
    pub node_count: usize,
876
    pub node_hierarchy_bytes: usize,
877
    pub node_data_bytes: usize,
878
    pub styled_nodes_bytes: usize,
879
    pub cascade_info_bytes: usize,
880
    pub tag_ids_bytes: usize,
881
    pub non_leaf_nodes_bytes: usize,
882
    pub callback_vecs_bytes: usize,
883
    pub css_property_cache: crate::prop_cache::CssPropertyCacheBreakdown,
884
}
885

            
886
impl StyledDomMemoryReport {
887
    pub fn total_bytes(&self) -> usize {
888
        self.node_hierarchy_bytes
889
            + self.node_data_bytes
890
            + self.styled_nodes_bytes
891
            + self.cascade_info_bytes
892
            + self.tag_ids_bytes
893
            + self.non_leaf_nodes_bytes
894
            + self.callback_vecs_bytes
895
            + self.css_property_cache.total_bytes()
896
    }
897
}
898

            
899
impl StyledDom {
900
    /// Approximate heap bytes retained by this StyledDom, broken out by field.
901
    pub fn memory_report(&self) -> StyledDomMemoryReport {
902
        let n = self.node_data.len();
903
        StyledDomMemoryReport {
904
            node_count: n,
905
            node_hierarchy_bytes: self.node_hierarchy.as_ref().len()
906
                * core::mem::size_of::<NodeHierarchyItem>(),
907
            node_data_bytes: {
908
                let base = n * core::mem::size_of::<crate::dom::NodeData>();
909
                // NodeData contains inline Vecs (callbacks, css_props, datasets)
910
                // that have their own heap allocations. Approximate:
911
                let mut inner = 0usize;
912
                for nd in self.node_data.as_ref().iter() {
913
                    inner += nd.get_callbacks().len() * 64; // rough per-callback
914
                    // Each rule = path + decls Vec + conditions Vec + priority byte.
915
                    // Approximate at 64 bytes per rule + the heap for declarations.
916
                    inner += nd.style.rules.as_ref().len() * 64;
917
                }
918
                base + inner
919
            },
920
            styled_nodes_bytes: n * core::mem::size_of::<StyledNode>(),
921
            cascade_info_bytes: n * core::mem::size_of::<CascadeInfo>(),
922
            tag_ids_bytes: self.tag_ids_to_node_ids.as_ref().len()
923
                * core::mem::size_of::<TagIdToNodeIdMapping>(),
924
            non_leaf_nodes_bytes: self.non_leaf_nodes.as_ref().len()
925
                * core::mem::size_of::<ParentWithNodeDepth>(),
926
            callback_vecs_bytes:
927
                self.nodes_with_window_callbacks.as_ref().len() * 8
928
                + self.nodes_with_datasets.as_ref().len() * 8,
929
            css_property_cache: self.css_property_cache.ptr.memory_breakdown(),
930
        }
931
    }
932

            
933
    /// Creates a new StyledDom by applying CSS styles to a DOM tree.
934
    ///
935
    /// NOTE: After calling this function, the DOM will be reset to an empty DOM.
936
    // This is for memory optimization, so that the DOM does not need to be cloned.
937
    //
938
    // The CSS will be left in-place, but will be re-ordered
939
5531
    pub fn create(dom: &mut Dom, css: Css) -> Self {
940
        use core::mem;
941

            
942
5531
        let mut swap_dom = Dom::create_body();
943
5531
        mem::swap(dom, &mut swap_dom);
944

            
945
5531
        let compact_dom: CompactDom = swap_dom.into();
946
5531
        let node_hierarchy: NodeHierarchyItemVec = compact_dom
947
5531
            .node_hierarchy
948
5531
            .as_ref()
949
5531
            .internal
950
5531
            .iter()
951
25279
            .map(|i| (*i).into())
952
5531
            .collect::<Vec<NodeHierarchyItem>>()
953
5531
            .into();
954

            
955
5531
        Self::create_from_compact_dom(compact_dom, css, node_hierarchy)
956
5531
    }
957

            
958
    /// Creates a StyledDom from a `FastDom` (arena-based DOM).
959
    ///
960
    /// This skips the `convert_dom_into_compact_dom` tree→arena conversion
961
    /// entirely since `FastDom` already has flat `NodeHierarchyItemVec` and
962
    /// `NodeDataVec`. CSS is collected from `CssWithNodeIdVec`.
963
3150
    pub fn create_from_fast_dom(fast_dom: crate::dom::FastDom) -> Self {
964
        use azul_css::css::Css;
965

            
966
        // 1. Merge CSS from CssWithNodeIdVec into a single Css
967
        //    (TODO: respect node_id scoping for sub-tree cascading)
968
3150
        let mut combined_rules: Vec<azul_css::css::CssRuleBlock> = Vec::new();
969
3150
        for css_with_id in fast_dom.css.into_library_owned_vec() {
970
3150
            combined_rules.extend(css_with_id.css.rules.into_library_owned_vec());
971
3150
        }
972
3150
        let combined_css = if combined_rules.is_empty() {
973
840
            Css::empty()
974
        } else {
975
2310
            Css::new(combined_rules)
976
        };
977

            
978
        // 2. Convert NodeHierarchyItemVec → NodeHierarchy (Vec<Node>)
979
        //    for cascade tree computation
980
3150
        let node_hierarchy_items = fast_dom.node_hierarchy;
981
3150
        let nodes: Vec<crate::id::Node> = node_hierarchy_items.as_ref()
982
3150
            .iter()
983
3150
            .map(|item| crate::id::Node {
984
20454
                parent: NodeId::from_usize(item.parent),
985
20454
                previous_sibling: NodeId::from_usize(item.previous_sibling),
986
20454
                next_sibling: NodeId::from_usize(item.next_sibling),
987
20454
                last_child: NodeId::from_usize(item.last_child),
988
20454
            })
989
3150
            .collect();
990
3150
        let node_hierarchy_internal = crate::id::NodeHierarchy { internal: nodes };
991

            
992
        // 3. Build CompactDom from the flat arenas (no conversion needed)
993
3150
        let node_data_vec = fast_dom.node_data.into_library_owned_vec();
994
3150
        let compact_dom = CompactDom {
995
3150
            node_hierarchy: node_hierarchy_internal,
996
3150
            node_data: crate::id::NodeDataContainer { internal: node_data_vec },
997
3150
            root: NodeId::ZERO,
998
3150
        };
999

            
        // 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.
3150
        Self::create_from_compact_dom(compact_dom, combined_css, node_hierarchy_items)
3150
    }
    /// 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).
8681
    fn create_from_compact_dom(
8681
        compact_dom: CompactDom,
8681
        mut css: Css,
8681
        node_hierarchy: NodeHierarchyItemVec,
8681
    ) -> Self {
        use crate::dom::EventFilter;
        static CASCADE_BREAKDOWN: crate::sync::OnceLock<bool> = crate::sync::OnceLock::new();
8681
        let cascade_dbg = *CASCADE_BREAKDOWN.get_or_init(crate::profile::memory_enabled);
8681
        let node_count = compact_dom.len();
8681
        let non_leaf_nodes = compact_dom
8681
            .node_hierarchy
8681
            .as_ref()
8681
            .get_parents_sorted_by_depth();
8681
        let mut styled_nodes = vec![
8681
            StyledNode {
8681
                styled_node_state: StyledNodeState::new()
8681
            };
8681
            node_count
        ];
8681
        let mut css_property_cache = CssPropertyCache::empty(compact_dom.node_data.len());
8681
        let html_tree = construct_html_cascade_tree(
8681
            &compact_dom.node_hierarchy.as_ref(),
8681
            &non_leaf_nodes[..],
8681
            &compact_dom.node_data.as_ref(),
        );
8681
        let non_leaf_nodes = non_leaf_nodes
8681
            .iter()
8681
            .map(|(depth, node_id)| ParentWithNodeDepth {
27363
                depth: *depth,
27363
                node_id: NodeHierarchyItemId::from_crate_internal(Some(*node_id)),
27363
            })
8681
            .collect::<Vec<_>>();
8681
        let non_leaf_nodes: ParentWithNodeDepthVec = non_leaf_nodes.into();
8681
        let _restyle_tag_ids = css_property_cache.restyle(
8681
            &mut css,
8681
            &compact_dom.node_data.as_ref(),
8681
            &node_hierarchy,
8681
            &non_leaf_nodes,
8681
            &html_tree.as_ref(),
        );
        // Drop the CSS object now — selectors/declarations are no longer needed
        // after restyle has populated css_props. This frees ~500 KiB of stylesheet
        // data structures (CssRuleBlock, CssPathSelector, CssDeclaration).
8681
        drop(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.
8681
        css_property_cache.apply_ua_css(compact_dom.node_data.as_ref().internal);
8681
        css_property_cache.compute_inherited_values(
8681
            node_hierarchy.as_container().internal,
8681
            compact_dom.node_data.as_ref().internal,
        );
8681
        let prev_font_hashes: Vec<u64> = css_property_cache.compact_cache
8681
            .as_ref()
8681
            .map(|c| c.prev_font_hashes.clone())
8681
            .unwrap_or_default();
8681
        let compact = css_property_cache.build_compact_cache_with_inheritance(
8681
            compact_dom.node_data.as_ref().internal,
8681
            node_hierarchy.as_container().internal,
8681
            &prev_font_hashes,
        );
8681
        css_property_cache.compact_cache = Some(compact);
8681
        let pre_prune = if cascade_dbg {
            Some(css_property_cache.memory_breakdown())
8681
        } else { None };
8681
        css_property_cache.prune_compact_normal_props();
8681
        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;
8681
        }
8681
        let tag_ids = css_property_cache.generate_tag_ids(
8681
            &compact_dom.node_data.as_ref(),
8681
            &node_hierarchy,
        );
8681
        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;
8681
        }
        // Collect callback/dataset nodes in a single pass (avoids 3 separate 50K scans).
        // For XHTML-parsed DOMs with no callbacks, this early-exits immediately.
8681
        let has_any_callbacks = compact_dom.node_data.as_ref().internal.iter()
45733
            .any(|c| !c.get_callbacks().is_empty() || c.get_dataset().is_some());
8681
        let (nodes_with_window_callbacks, nodes_with_datasets) = if has_any_callbacks {
            let mut win_cbs = Vec::new();
            let mut datasets = Vec::new();
            for (node_id, c) in compact_dom.node_data.as_ref().internal.iter().enumerate() {
                let cbs = c.get_callbacks();
                let has_dataset = c.get_dataset().is_some();
                if !cbs.is_empty() || has_dataset {
                    datasets.push(NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_id))));
                }
                for cb in cbs.iter() {
                    if let EventFilter::Window(_) = cb.event {
                        win_cbs.push(NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_id))));
                        break;
                    }
                }
            }
            (win_cbs, datasets)
        } else {
8681
            (Vec::new(), Vec::new())
        };
8681
        let mut styled_dom = StyledDom {
8681
            root: NodeHierarchyItemId::from_crate_internal(Some(compact_dom.root)),
8681
            node_hierarchy,
8681
            node_data: compact_dom.node_data.internal.into(),
8681
            cascade_info: html_tree.internal.into(),
8681
            styled_nodes: styled_nodes.into(),
8681
            tag_ids_to_node_ids: tag_ids.into(),
8681
            nodes_with_window_callbacks: nodes_with_window_callbacks.into(),
8681
            nodes_with_datasets: nodes_with_datasets.into(),
8681
            non_leaf_nodes,
8681
            css_property_cache: CssPropertyCachePtr::new(css_property_cache),
8681
            dom_id: DomId::ROOT_ID,
8681
        };
        #[cfg(feature = "table_layout")]
        if let Err(_e) = crate::dom_table::generate_anonymous_table_elements(&mut styled_dom) {
        }
8681
        styled_dom
8681
    }
    /// 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
    pub fn create_from_dom(mut dom: Dom) -> Self {
        use azul_css::css::Css;
        // 1. Collect all CSS objects from the recursive Dom tree
        let mut all_css = Vec::new();
        collect_css_from_dom(&dom, &mut all_css);
        // 2. Merge all CSS objects into one combined Css
        let mut combined_css = if all_css.is_empty() {
            Css::empty()
        } else {
            let mut combined_rules: Vec<azul_css::css::CssRuleBlock> = Vec::new();
            for css in all_css {
                combined_rules.extend(css.rules.into_library_owned_vec());
            }
            Css::new(combined_rules)
        };
        // 3. Strip CSS from all Dom nodes before flattening
        //    (CSS is already collected, don't need it in the flat tree)
        strip_css_from_dom(&mut dom);
        // 4. Use existing StyledDom::create to flatten + cascade
        Self::create(&mut dom, combined_css)
    }
    /// Appends another `StyledDom` as a child to the `self.root`
    /// without re-styling the DOM itself
204
    pub fn append_child(&mut self, other: Self) {
204
        let self_root_id = self.root.into_crate_internal().unwrap_or(NodeId::ZERO);
204
        let current_root_children_count = self_root_id
204
            .az_children(&self.node_hierarchy.as_container())
204
            .count();
204
        self.append_child_with_index(other, current_root_children_count);
204
        self.finalize_non_leaf_nodes();
204
    }
    /// Optimized version of `append_child` that takes the child index directly
    /// instead of counting existing children (O(1) instead of O(n))
204
    pub fn append_child_with_index(&mut self, mut other: Self, child_index: usize) {
        // shift all the node ids in other by self.len()
204
        let self_len = self.node_hierarchy.as_ref().len();
204
        let other_len = other.node_hierarchy.as_ref().len();
204
        let self_root_id = self.root.into_crate_internal().unwrap_or(NodeId::ZERO);
204
        let other_root_id = other.root.into_crate_internal().unwrap_or(NodeId::ZERO);
        // Use provided index instead of counting children
204
        other.cascade_info.as_mut()[other_root_id.index()].index_in_parent = child_index as u32;
204
        other.cascade_info.as_mut()[other_root_id.index()].is_last_child = true;
204
        self.cascade_info.append(&mut other.cascade_info);
        // adjust node hierarchy
323
        for other in other.node_hierarchy.as_mut().iter_mut() {
323
            if other.parent != 0 {
119
                other.parent += self_len;
204
            }
323
            if other.previous_sibling != 0 {
                other.previous_sibling += self_len;
323
            }
323
            if other.next_sibling != 0 {
                other.next_sibling += self_len;
323
            }
323
            if other.last_child != 0 {
119
                other.last_child += self_len;
204
            }
        }
204
        other.node_hierarchy.as_container_mut()[other_root_id].parent =
204
            NodeId::into_raw(&Some(self_root_id));
204
        let current_last_child = self.node_hierarchy.as_container()[self_root_id].last_child_id();
204
        other.node_hierarchy.as_container_mut()[other_root_id].previous_sibling =
204
            NodeId::into_raw(&current_last_child);
204
        if let Some(current_last) = current_last_child {
136
            if self.node_hierarchy.as_container_mut()[current_last]
136
                .next_sibling_id()
136
                .is_some()
            {
                self.node_hierarchy.as_container_mut()[current_last].next_sibling +=
                    other_root_id.index() + other_len;
136
            } else {
136
                self.node_hierarchy.as_container_mut()[current_last].next_sibling =
136
                    NodeId::into_raw(&Some(NodeId::new(self_len + other_root_id.index())));
136
            }
68
        }
204
        self.node_hierarchy.as_container_mut()[self_root_id].last_child =
204
            NodeId::into_raw(&Some(NodeId::new(self_len + other_root_id.index())));
204
        self.node_hierarchy.append(&mut other.node_hierarchy);
204
        self.node_data.append(&mut other.node_data);
204
        self.styled_nodes.append(&mut other.styled_nodes);
204
        self.get_css_property_cache_mut()
204
            .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).
204
        for tag_id_node_id in other.tag_ids_to_node_ids.iter_mut() {
102
            tag_id_node_id.node_id.inner += self_len;
102
        }
204
        self.tag_ids_to_node_ids
204
            .append(&mut other.tag_ids_to_node_ids);
204
        for nid in other.nodes_with_window_callbacks.iter_mut() {
            nid.inner += self_len;
        }
204
        self.nodes_with_window_callbacks
204
            .append(&mut other.nodes_with_window_callbacks);
204
        for nid in other.nodes_with_datasets.iter_mut() {
            nid.inner += self_len;
        }
204
        self.nodes_with_datasets
204
            .append(&mut other.nodes_with_datasets);
        // edge case: if the other StyledDom consists of only one node
        // then it is not a parent itself
204
        if other_len != 1 {
119
            for other_non_leaf_node in other.non_leaf_nodes.iter_mut() {
119
                other_non_leaf_node.node_id.inner += self_len;
119
                other_non_leaf_node.depth += 1;
119
            }
102
            self.non_leaf_nodes.append(&mut other.non_leaf_nodes);
            // NOTE: Sorting deferred - call finalize_non_leaf_nodes() after all appends
102
        }
204
    }
    /// Call this after all append_child_with_index operations are complete
    /// to sort non_leaf_nodes by depth (required for correct rendering)
204
    pub fn finalize_non_leaf_nodes(&mut self) {
289
        self.non_leaf_nodes.sort_by(|a, b| a.depth.cmp(&b.depth));
204
    }
    /// Same as `append_child()`, but as a builder method
    pub fn with_child(mut self, other: Self) -> Self {
        self.append_child(other);
        self
    }
    /// Sets the context menu for the root node
    pub fn set_context_menu(&mut self, context_menu: Menu) {
        if let Some(root_id) = self.root.into_crate_internal() {
            self.node_data.as_container_mut()[root_id].set_context_menu(context_menu);
        }
    }
    /// Builder method for setting the context menu
    pub fn with_context_menu(mut self, context_menu: Menu) -> Self {
        self.set_context_menu(context_menu);
        self
    }
    /// Sets the menu bar for the root node
    pub fn set_menu_bar(&mut self, menu_bar: Menu) {
        if let Some(root_id) = self.root.into_crate_internal() {
            self.node_data.as_container_mut()[root_id].set_menu_bar(menu_bar);
        }
    }
    /// Builder method for setting the menu bar
    pub fn with_menu_bar(mut self, menu_bar: Menu) -> Self {
        self.set_menu_bar(menu_bar);
        self
    }
    /// 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.
1
    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.
1
        let prev_font_hashes: Vec<u64> = self.css_property_cache
1
            .downcast_mut()
1
            .compact_cache
1
            .as_ref()
1
            .map(|c| c.prev_font_hashes.clone())
1
            .unwrap_or_default();
1
        let compact = self.css_property_cache
1
            .downcast_mut()
1
            .build_compact_cache_with_inheritance(
1
                self.node_data.as_container().internal,
1
                self.node_hierarchy.as_container().internal,
1
                &prev_font_hashes,
            );
1
        self.css_property_cache.downcast_mut().compact_cache = Some(compact);
1
    }
    /// Re-applies CSS styles to the existing DOM structure.
    pub fn restyle(&mut self, mut css: Css) {
        let new_tag_ids = self.css_property_cache.downcast_mut().restyle(
            &mut css,
            &self.node_data.as_container(),
            &self.node_hierarchy,
            &self.non_leaf_nodes,
            &self.cascade_info.as_container(),
        );
        // Apply UA CSS properties before computing inheritance
        self.css_property_cache
            .downcast_mut()
            .apply_ua_css(self.node_data.as_container().internal);
        // Compute inherited values after restyle and apply_ua_css (resolves em, %, etc.)
        self.css_property_cache
            .downcast_mut()
            .compute_inherited_values(
                self.node_hierarchy.as_container().internal,
                self.node_data.as_container().internal,
            );
        self.tag_ids_to_node_ids = new_tag_ids.into();
    }
    /// Returns the total number of nodes in this StyledDom.
    #[inline]
    pub fn node_count(&self) -> usize {
        self.node_data.len()
    }
    /// Returns an immutable reference to the CSS property cache.
    #[inline]
9186
    pub fn get_css_property_cache<'a>(&'a self) -> &'a CssPropertyCache {
9186
        &*self.css_property_cache.ptr
9186
    }
    /// Returns a mutable reference to the CSS property cache.
    #[inline]
408
    pub fn get_css_property_cache_mut<'a>(&'a mut self) -> &'a mut CssPropertyCache {
408
        &mut *self.css_property_cache.ptr
408
    }
    /// Returns the current state (hover, active, focus) of a styled node.
    #[inline]
    pub fn get_styled_node_state(&self, node_id: &NodeId) -> StyledNodeState {
        self.styled_nodes.as_container()[*node_id]
            .styled_node_state
            .clone()
    }
    /// Updates hover state for nodes and returns changed CSS properties.
    #[must_use]
119
    pub fn restyle_nodes_hover(
119
        &mut self,
119
        nodes: &[NodeId],
119
        new_hover_state: bool,
119
    ) -> RestyleNodes {
119
        self.restyle_nodes_state(
119
            nodes,
119
            new_hover_state,
136
            |state, val| state.hover = val,
119
            azul_css::dynamic_selector::PseudoStateType::Hover,
        )
119
    }
    /// Updates active state for nodes and returns changed CSS properties.
    #[must_use]
85
    pub fn restyle_nodes_active(
85
        &mut self,
85
        nodes: &[NodeId],
85
        new_active_state: bool,
85
    ) -> RestyleNodes {
85
        self.restyle_nodes_state(
85
            nodes,
85
            new_active_state,
85
            |state, val| state.active = val,
85
            azul_css::dynamic_selector::PseudoStateType::Active,
        )
85
    }
    /// Updates focus state for nodes and returns changed CSS properties.
    #[must_use]
204
    pub fn restyle_nodes_focus(
204
        &mut self,
204
        nodes: &[NodeId],
204
        new_focus_state: bool,
204
    ) -> RestyleNodes {
204
        self.restyle_nodes_state(
204
            nodes,
204
            new_focus_state,
204
            |state, val| state.focused = val,
204
            azul_css::dynamic_selector::PseudoStateType::Focus,
        )
204
    }
    /// Generic restyle method parameterized by the state field and pseudo-state type.
408
    fn restyle_nodes_state(
408
        &mut self,
408
        nodes: &[NodeId],
408
        new_state_value: bool,
408
        set_state: impl Fn(&mut StyledNodeState, bool),
408
        pseudo_state_type: azul_css::dynamic_selector::PseudoStateType,
408
    ) -> RestyleNodes {
        // save the old node state
408
        let old_node_states = nodes
408
            .iter()
425
            .map(|nid| {
425
                self.styled_nodes.as_container()[*nid]
425
                    .styled_node_state
425
                    .clone()
425
            })
408
            .collect::<Vec<_>>();
425
        for nid in nodes.iter() {
425
            set_state(
425
                &mut self.styled_nodes.as_container_mut()[*nid].styled_node_state,
425
                new_state_value,
425
            );
425
        }
408
        let css_property_cache = self.get_css_property_cache();
408
        let styled_nodes = self.styled_nodes.as_container();
408
        let node_data = self.node_data.as_container();
        // scan all properties that could have changed because of addition / removal
408
        let v = nodes
408
            .iter()
408
            .zip(old_node_states.iter())
425
            .filter_map(|(node_id, old_node_state)| {
425
                let mut keys_normal: Vec<_> = CssPropertyCache::prop_types_for_state(
425
                    css_property_cache.css_props.get_slice(node_id.index()),
425
                    pseudo_state_type,
425
                ).collect();
425
                let mut keys_inherited: Vec<_> = CssPropertyCache::prop_types_for_state(
425
                    css_property_cache.cascaded_props.get_slice(node_id.index()),
425
                    pseudo_state_type,
425
                ).collect();
425
                let keys_inline: Vec<CssPropertyType> = {
                    use azul_css::dynamic_selector::DynamicSelector;
425
                    node_data[*node_id]
425
                        .style
425
                        .iter_inline_properties()
1632
                        .filter_map(|(prop, conds)| {
1632
                            let matches = conds.as_slice().iter().any(|c| {
1207
                                matches!(c, DynamicSelector::PseudoState(pst) if *pst == pseudo_state_type)
1207
                            });
1632
                            if matches {
425
                                Some(prop.get_type())
                            } else {
1207
                                None
                            }
1632
                        })
425
                        .collect()
                };
425
                let mut keys_inline_ref: Vec<_> = keys_inline.iter().collect();
425
                keys_normal.append(&mut keys_inherited);
425
                keys_normal.append(&mut keys_inline_ref);
425
                let node_properties_that_could_have_changed = keys_normal;
425
                if node_properties_that_could_have_changed.is_empty() {
                    return None;
425
                }
425
                let new_node_state = &styled_nodes[*node_id].styled_node_state;
425
                let node_data = &node_data[*node_id];
425
                let changes = node_properties_that_could_have_changed
425
                    .into_iter()
425
                    .filter_map(|prop| {
                        // calculate both the old and the new state
425
                        let old = css_property_cache.get_property_slow(
425
                            node_data,
425
                            node_id,
425
                            old_node_state,
425
                            prop,
                        );
425
                        let new = css_property_cache.get_property_slow(
425
                            node_data,
425
                            node_id,
425
                            new_node_state,
425
                            prop,
                        );
425
                        if old == new {
51
                            None
                        } else {
                            Some(ChangedCssProperty {
374
                                previous_state: old_node_state.clone(),
374
                                previous_prop: match old {
                                    None => CssProperty::auto(*prop),
374
                                    Some(s) => s.clone(),
                                },
374
                                current_state: new_node_state.clone(),
374
                                current_prop: match new {
                                    None => CssProperty::auto(*prop),
374
                                    Some(s) => s.clone(),
                                },
                            })
                        }
425
                    })
425
                    .collect::<Vec<_>>();
425
                if changes.is_empty() {
51
                    None
                } else {
374
                    Some((*node_id, changes))
                }
425
            })
408
            .collect::<Vec<_>>();
408
        v.into_iter().collect()
408
    }
    /// 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]
340
    pub fn restyle_on_state_change(
340
        &mut self,
340
        focus_changes: Option<FocusChange>,
340
        hover_changes: Option<HoverChange>,
340
        active_changes: Option<ActiveChange>,
340
    ) -> RestyleResult {
340
        let mut result = RestyleResult::default();
340
        result.gpu_only_changes = true; // Start with GPU-only assumption
        // Helper closure to merge changes and analyze property categories
408
        let mut process_changes = |changes: RestyleNodes| {
782
            for (node_id, props) in changes {
748
                for change in &props {
374
                    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.
374
                    let scope = prop_type.relayout_scope(/* node_is_ifc_member */ true);
                    // Track the highest scope seen
374
                    if scope > result.max_relayout_scope {
17
                        result.max_relayout_scope = scope;
357
                    }
                    // Any scope above None triggers layout
374
                    if scope != RelayoutScope::None {
17
                        result.needs_layout = true;
17
                        result.gpu_only_changes = false;
357
                    }
                    // Check if this is a GPU-only property
374
                    if !prop_type.is_gpu_only_property() {
357
                        result.gpu_only_changes = false;
357
                    }
                    // Any visual change needs display list update (unless GPU-only)
374
                    result.needs_display_list = true;
                }
374
                result.changed_nodes.entry(node_id).or_default().extend(props);
            }
408
        };
        // 1. Process focus changes
340
        if let Some(focus) = focus_changes {
187
            if let Some(old) = focus.lost_focus {
34
                let changes = self.restyle_nodes_focus(&[old], false);
34
                process_changes(changes);
153
            }
187
            if let Some(new) = focus.gained_focus {
170
                let changes = self.restyle_nodes_focus(&[new], true);
170
                process_changes(changes);
170
            }
153
        }
        // 2. Process hover changes
340
        if let Some(hover) = hover_changes {
119
            if !hover.left_nodes.is_empty() {
17
                let changes = self.restyle_nodes_hover(&hover.left_nodes, false);
17
                process_changes(changes);
102
            }
119
            if !hover.entered_nodes.is_empty() {
102
                let changes = self.restyle_nodes_hover(&hover.entered_nodes, true);
102
                process_changes(changes);
102
            }
221
        }
        // 3. Process active changes
340
        if let Some(active) = active_changes {
85
            if !active.deactivated.is_empty() {
17
                let changes = self.restyle_nodes_active(&active.deactivated, false);
17
                process_changes(changes);
68
            }
85
            if !active.activated.is_empty() {
68
                let changes = self.restyle_nodes_active(&active.activated, true);
68
                process_changes(changes);
68
            }
255
        }
        // If no changes, reset display_list flag
340
        if result.changed_nodes.is_empty() {
17
            result.needs_display_list = false;
17
            result.gpu_only_changes = false;
323
        }
        // If layout is needed, display list is also needed
340
        if result.needs_layout {
17
            result.needs_display_list = true;
17
            result.gpu_only_changes = false;
323
        }
340
        result
340
    }
    /// 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]
    pub fn restyle_user_property(
        &mut self,
        node_id: &NodeId,
        new_properties: &[CssProperty],
    ) -> RestyleNodes {
        let mut map = BTreeMap::default();
        if new_properties.is_empty() {
            return map;
        }
        let node_count = self.node_data.as_ref().len();
        if node_id.index() >= node_count {
            return map;
        }
        let node_data = self.node_data.as_container();
        let node_data = &node_data[*node_id];
        let node_states = &self.styled_nodes.as_container();
        let old_node_state = &node_states[*node_id].styled_node_state;
        let changes: Vec<ChangedCssProperty> = {
            let css_property_cache = self.get_css_property_cache();
            new_properties
                .iter()
                .filter_map(|new_prop| {
                    let old_prop = css_property_cache.get_property_slow(
                        node_data,
                        node_id,
                        old_node_state,
                        &new_prop.get_type(),
                    );
                    let old_prop = match old_prop {
                        None => CssProperty::auto(new_prop.get_type()),
                        Some(s) => s.clone(),
                    };
                    if old_prop == *new_prop {
                        None
                    } else {
                        Some(ChangedCssProperty {
                            previous_state: old_node_state.clone(),
                            previous_prop: old_prop,
                            // overriding a user property does not change the state
                            current_state: old_node_state.clone(),
                            current_prop: new_prop.clone(),
                        })
                    }
                })
                .collect()
        };
        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.
        if css_property_cache_mut.user_overridden_properties.len() < node_count {
            css_property_cache_mut
                .user_overridden_properties
                .resize(node_count, Vec::new());
        }
        for new_prop in new_properties.iter() {
            let prop_type = new_prop.get_type();
            let vec = &mut css_property_cache_mut
                .user_overridden_properties[node_id.index()];
            if new_prop.is_initial() {
                // CssProperty::Initial = remove overridden property
                if let Ok(idx) = vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
                    vec.remove(idx);
                }
            } else {
                match vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
                    Ok(idx) => vec[idx].1 = new_prop.clone(),
                    Err(idx) => vec.insert(idx, (prop_type, new_prop.clone())),
                }
            }
        }
        if !changes.is_empty() {
            map.insert(*node_id, changes);
        }
        map
    }
    /// 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>
    /// ```
    pub fn get_html_string(&self, custom_head: &str, custom_body: &str, test_mode: bool) -> String {
        let css_property_cache = self.get_css_property_cache();
        let mut output = String::new();
        // After which nodes should a close tag be printed?
        let mut should_print_close_tag_after_node: BTreeMap<NodeId, Vec<(NodeId, usize)>> = BTreeMap::new();
        let should_print_close_tag_debug = self
            .non_leaf_nodes
            .iter()
            .filter_map(|p| {
                let parent_node_id = p.node_id.into_crate_internal()?;
                let mut total_last_child = None;
                recursive_get_last_child(
                    parent_node_id,
                    &self.node_hierarchy.as_ref(),
                    &mut total_last_child,
                );
                let total_last_child = total_last_child?;
                Some((parent_node_id, (total_last_child, p.depth)))
            })
            .collect::<BTreeMap<_, _>>();
        for (parent_id, (last_child, parent_depth)) in should_print_close_tag_debug {
            should_print_close_tag_after_node
                .entry(last_child)
                .or_default()
                .push((parent_id, parent_depth));
        }
        let mut all_node_depths = self
            .non_leaf_nodes
            .iter()
            .filter_map(|p| {
                let parent_node_id = p.node_id.into_crate_internal()?;
                Some((parent_node_id, p.depth))
            })
            .collect::<BTreeMap<_, _>>();
        for (parent_node_id, parent_depth) in self
            .non_leaf_nodes
            .iter()
            .filter_map(|p| Some((p.node_id.into_crate_internal()?, p.depth)))
        {
            for child_id in parent_node_id.az_children(&self.node_hierarchy.as_container()) {
                all_node_depths.insert(child_id, parent_depth + 1);
            }
        }
        for node_id in self.node_hierarchy.as_container().linear_iter() {
            let depth = all_node_depths[&node_id];
            let node_data = &self.node_data.as_container()[node_id];
            let node_state = &self.styled_nodes.as_container()[node_id].styled_node_state;
            let tabs = String::from("    ").repeat(depth);
            output.push_str("\r\n");
            output.push_str(&tabs);
            output.push_str(&node_data.debug_print_start(css_property_cache, &node_id, node_state));
            if let Some(content) = node_data.get_node_type().format().as_ref() {
                output.push_str(content);
            }
            let node_has_children = self.node_hierarchy.as_container()[node_id]
                .first_child_id(node_id)
                .is_some();
            if !node_has_children {
                let node_data = &self.node_data.as_container()[node_id];
                output.push_str(&node_data.debug_print_end());
            }
            if let Some(close_tag_vec) = should_print_close_tag_after_node.get(&node_id) {
                let mut close_tag_vec = close_tag_vec.clone();
                close_tag_vec.sort_by(|a, b| b.1.cmp(&a.1)); // sort by depth descending
                for (close_tag_parent_id, close_tag_depth) in close_tag_vec {
                    let node_data = &self.node_data.as_container()[close_tag_parent_id];
                    let tabs = String::from("    ").repeat(close_tag_depth);
                    output.push_str("\r\n");
                    output.push_str(&tabs);
                    output.push_str(&node_data.debug_print_end());
                }
            }
        }
        if !test_mode {
            format!(
                "
                <html>
                    <head>
                    <style>* {{ margin:0px; padding:0px; }}</style>
                    {custom_head}
                    </head>
                {output}
                {custom_body}
                </html>
            "
            )
        } else {
            output
        }
    }
    /// Returns nodes grouped by their rendering order (respects z-index and position).
    pub fn get_rects_in_rendering_order(&self) -> ContentGroup {
        Self::determine_rendering_order(
            &self.non_leaf_nodes.as_ref(),
            &self.node_hierarchy.as_container(),
            &self.styled_nodes.as_container(),
            &self.node_data.as_container(),
            &self.get_css_property_cache(),
        )
    }
    /// Returns the rendering order of the items (the rendering
    /// order doesn't have to be the original order)
    fn determine_rendering_order<'a>(
        non_leaf_nodes: &[ParentWithNodeDepth],
        node_hierarchy: &NodeDataContainerRef<'a, NodeHierarchyItem>,
        styled_nodes: &NodeDataContainerRef<StyledNode>,
        node_data_container: &NodeDataContainerRef<NodeData>,
        css_property_cache: &CssPropertyCache,
    ) -> ContentGroup {
        let children_sorted = non_leaf_nodes
            .iter()
            .filter_map(|parent| {
                Some((
                    parent.node_id,
                    sort_children_by_position(
                        parent.node_id.into_crate_internal()?,
                        node_hierarchy,
                        styled_nodes,
                        node_data_container,
                        css_property_cache,
                    ),
                ))
            })
            .collect::<Vec<_>>();
        let children_sorted: BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>> =
            children_sorted.into_iter().collect();
        let mut root_content_group = ContentGroup {
            root: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
            children: Vec::new().into(),
        };
        fill_content_group_children(&mut root_content_group, &children_sorted);
        root_content_group
    }
    /// Replaces this StyledDom with default and returns the old value.
    pub fn swap_with_default(&mut self) -> Self {
        let mut new = Self::default();
        core::mem::swap(self, &mut new);
        new
    }
}
/// 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(always)]
8681
    pub fn len(&self) -> usize {
8681
        self.node_hierarchy.as_ref().len()
8681
    }
}
impl From<Dom> for CompactDom {
5531
    fn from(dom: Dom) -> Self {
5531
        convert_dom_into_compact_dom(dom)
5531
    }
}
/// Converts a tree-based Dom into an arena-based CompactDom for efficient traversal.
5922
pub fn convert_dom_into_compact_dom(mut dom: Dom) -> CompactDom {
    // note: somehow convert this into a non-recursive form later on!
25993
    fn convert_dom_into_compact_dom_internal(
25993
        dom: &mut Dom,
25993
        node_hierarchy: &mut [Node],
25993
        node_data: &mut Vec<NodeData>,
25993
        parent_node_id: NodeId,
25993
        node: Node,
25993
        cur_node_id: &mut usize,
25993
    ) {
        // - 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!
25993
        node_hierarchy[parent_node_id.index()] = node.clone();
25993
        let copy = dom.root.copy_special();
25993
        node_data[parent_node_id.index()] = copy;
25993
        *cur_node_id += 1;
25993
        let mut previous_sibling_id = None;
25993
        let children_len = dom.children.len();
25993
        for (child_index, child_dom) in dom.children.as_mut().iter_mut().enumerate() {
20071
            let child_node_id = NodeId::new(*cur_node_id);
20071
            let is_last_child = (child_index + 1) == children_len;
20071
            let child_dom_is_empty = child_dom.children.is_empty();
20071
            let child_node = Node {
20071
                parent: Some(parent_node_id),
20071
                previous_sibling: previous_sibling_id,
20071
                next_sibling: if is_last_child {
16712
                    None
                } else {
3359
                    Some(child_node_id + child_dom.estimated_total_children + 1)
                },
20071
                last_child: if child_dom_is_empty {
7898
                    None
                } else {
12173
                    Some(child_node_id + child_dom.estimated_total_children)
                },
            };
20071
            previous_sibling_id = Some(child_node_id);
            // recurse BEFORE adding the next child
20071
            convert_dom_into_compact_dom_internal(
20071
                child_dom,
20071
                node_hierarchy,
20071
                node_data,
20071
                child_node_id,
20071
                child_node,
20071
                cur_node_id,
            );
        }
25993
    }
    // Pre-allocate all nodes (+ 1 root node)
5922
    let sum_nodes = dom.fixup_children_estimated();
5922
    let mut node_hierarchy = vec![Node::ROOT; sum_nodes + 1];
5922
    let mut node_data = vec![NodeData::create_div(); sum_nodes + 1];
5922
    let mut cur_node_id = 0;
5922
    let root_node_id = NodeId::ZERO;
5922
    let root_node = Node {
5922
        parent: None,
5922
        previous_sibling: None,
5922
        next_sibling: None,
5922
        last_child: if dom.children.is_empty() {
1383
            None
        } else {
4539
            Some(root_node_id + dom.estimated_total_children)
        },
    };
5922
    convert_dom_into_compact_dom_internal(
5922
        &mut dom,
5922
        &mut node_hierarchy,
5922
        &mut node_data,
5922
        root_node_id,
5922
        root_node,
5922
        &mut cur_node_id,
    );
5922
    CompactDom {
5922
        node_hierarchy: NodeHierarchy {
5922
            internal: node_hierarchy,
5922
        },
5922
        node_data: NodeDataContainer {
5922
            internal: node_data,
5922
        },
5922
        root: root_node_id,
5922
    }
5922
}
/// 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.
fn collect_css_from_dom(dom: &Dom, out: &mut Vec<azul_css::css::Css>) {
    // First, recurse into children (inner CSS = lower priority)
    for child in dom.children.iter() {
        collect_css_from_dom(child, out);
    }
    // Then, add this node's CSS objects (outer CSS = higher priority)
    for css in dom.css.iter() {
        out.push(css.clone());
    }
}
/// Recursively strip CSS from all Dom nodes (sets css to empty vec).
/// Called after collecting CSS so the CompactDom doesn't carry CSS data.
fn strip_css_from_dom(dom: &mut Dom) {
    dom.css = Vec::new().into();
    for child in dom.children.as_mut().iter_mut() {
        strip_css_from_dom(child);
    }
}
fn fill_content_group_children(
    group: &mut ContentGroup,
    children_sorted: &BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>>,
) {
    if let Some(c) = children_sorted.get(&group.root) {
        // returns None for leaf nodes
        group.children = c
            .iter()
            .map(|child| ContentGroup {
                root: *child,
                children: Vec::new().into(),
            })
            .collect::<Vec<ContentGroup>>()
            .into();
        for c in group.children.as_mut() {
            fill_content_group_children(c, children_sorted);
        }
    }
}
fn sort_children_by_position<'a>(
    parent: NodeId,
    node_hierarchy: &NodeDataContainerRef<'a, NodeHierarchyItem>,
    rectangles: &NodeDataContainerRef<StyledNode>,
    node_data_container: &NodeDataContainerRef<NodeData>,
    css_property_cache: &CssPropertyCache,
) -> Vec<NodeHierarchyItemId> {
    use azul_css::props::layout::LayoutPosition::*;
    let children_positions = parent
        .az_children(node_hierarchy)
        .map(|nid| {
            let position = css_property_cache
                .get_position(
                    &node_data_container[nid],
                    &nid,
                    &rectangles[nid].styled_node_state,
                )
                .and_then(|p| p.clone().get_property_or_default())
                .unwrap_or_default();
            let id = NodeHierarchyItemId::from_crate_internal(Some(nid));
            (id, position)
        })
        .collect::<Vec<_>>();
    let mut not_absolute_children = children_positions
        .iter()
        .filter_map(|(node_id, position)| {
            if *position != Absolute {
                Some(*node_id)
            } else {
                None
            }
        })
        .collect::<Vec<_>>();
    let mut absolute_children = children_positions
        .iter()
        .filter_map(|(node_id, position)| {
            if *position == Absolute {
                Some(*node_id)
            } else {
                None
            }
        })
        .collect::<Vec<_>>();
    // Append the position:absolute children after the regular children
    not_absolute_children.append(&mut absolute_children);
    not_absolute_children
}
// calls get_last_child() recursively until the last child of the last child of the ... has been
// found
fn recursive_get_last_child(
    node_id: NodeId,
    node_hierarchy: &[NodeHierarchyItem],
    target: &mut Option<NodeId>,
) {
    match node_hierarchy[node_id.index()].last_child_id() {
        None => return,
        Some(s) => {
            *target = Some(s);
            recursive_get_last_child(s, node_hierarchy, target);
        }
    }
}
// ============================================================================
// 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
pub fn is_before_in_document_order(
    hierarchy: &NodeHierarchyItemVec,
    node_a: NodeId,
    node_b: NodeId,
) -> bool {
    if node_a == node_b {
        return false;
    }
    let hierarchy = hierarchy.as_container();
    // Get paths from root to each node (stored as root-first order)
    let path_a = get_path_to_root(&hierarchy, node_a);
    let path_b = get_path_to_root(&hierarchy, node_b);
    // Find divergence point (last common ancestor)
    let min_len = path_a.len().min(path_b.len());
    for i in 0..min_len {
        if path_a[i] != path_b[i] {
            // Found divergence - check which sibling comes first
            let child_towards_a = path_a[i];
            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
            return child_towards_a.index() < child_towards_b.index();
        }
    }
    // One path is a prefix of the other - the shorter path (ancestor) comes first
    path_a.len() < path_b.len()
}
/// Get the path from root to a node, returned in root-first order.
fn get_path_to_root(
    hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
    node: NodeId,
) -> Vec<NodeId> {
    let mut path = Vec::new();
    let mut current = Some(node);
    while let Some(node_id) = current {
        path.push(node_id);
        current = hierarchy.get(node_id).and_then(|h| h.parent_id());
    }
    // Reverse to get root-first order
    path.reverse();
    path
}
/// 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)
pub fn collect_nodes_in_document_order(
    hierarchy: &NodeHierarchyItemVec,
    start_node: NodeId,
    end_node: NodeId,
) -> Vec<NodeId> {
    if start_node == end_node {
        return vec![start_node];
    }
    let hierarchy_container = hierarchy.as_container();
    let hierarchy_slice = hierarchy.as_ref();
    let mut result = Vec::new();
    let mut in_range = false;
    // Pre-order DFS using a stack
    // We need to traverse in document order, which is pre-order DFS
    let mut stack: Vec<NodeId> = vec![NodeId::ZERO]; // Start from root
    while let Some(current) = stack.pop() {
        // Check if we've entered the range
        if current == start_node {
            in_range = true;
        }
        // Collect if in range
        if in_range {
            result.push(current);
        }
        // Check if we've exited the range
        if current == end_node {
            break;
        }
        // Push children in reverse order so they pop in correct order
        // (first child should be processed first)
        if let Some(item) = hierarchy_container.get(current) {
            // Get first child
            if let Some(first_child) = item.first_child_id(current) {
                // Collect all children by following next_sibling
                let mut children = Vec::new();
                let mut child = Some(first_child);
                while let Some(child_id) = child {
                    children.push(child_id);
                    child = hierarchy_container.get(child_id).and_then(|h| h.next_sibling_id());
                }
                // Push in reverse order for correct DFS order
                for child_id in children.into_iter().rev() {
                    stack.push(child_id);
                }
            }
        }
    }
    result
}
/// 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).
pub fn is_layout_equivalent(old: &StyledDom, new: &StyledDom) -> bool {
    use crate::dom::NodeType;
    use crate::resources::DecodedImage;
    // Quick check: node count must match
    let old_nodes = old.node_data.as_ref();
    let new_nodes = new.node_data.as_ref();
    if old_nodes.len() != new_nodes.len() {
        return false;
    }
    // Check hierarchy (parent/child/sibling structure)
    let old_hier = old.node_hierarchy.as_ref();
    let new_hier = new.node_hierarchy.as_ref();
    if old_hier.len() != new_hier.len() {
        return false;
    }
    if old_hier != new_hier {
        return false;
    }
    // Per-node comparison
    for (old_node, new_node) in old_nodes.iter().zip(new_nodes.iter()) {
        // Compare node type discriminant
        if core::mem::discriminant(&old_node.node_type)
            != core::mem::discriminant(&new_node.node_type)
        {
            return false;
        }
        // Compare node type content (with special handling for image callbacks)
        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;
                        }
                    }
                }
            }
            _ => {
                if old_node.node_type != new_node.node_type {
                    return false;
                }
            }
        }
        // Compare IDs and classes (now stored in attributes as AttributeType::Id/Class)
        {
            use crate::dom::AttributeType;
            let old_ids_classes: Vec<_> = old_node.attributes().as_ref().iter()
                .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
                .collect();
            let new_ids_classes: Vec<_> = new_node.attributes().as_ref().iter()
                .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
                .collect();
            if old_ids_classes != new_ids_classes {
                return false;
            }
        }
        // Compare inline CSS (direct layout input)
        if old_node.style != new_node.style {
            return false;
        }
        // Compare callback event types (affects hit-test tags)
        // We compare only event types, not function pointers or data
        let old_cbs = old_node.callbacks.as_ref();
        let new_cbs = new_node.callbacks.as_ref();
        if old_cbs.len() != new_cbs.len() {
            return false;
        }
        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)
        if old_node.attributes().as_ref() != new_node.attributes().as_ref() {
            return false;
        }
    }
    // Compare styled node states (hover/focus/active flags affect CSS resolution)
    let old_styled = old.styled_nodes.as_ref();
    let new_styled = new.styled_nodes.as_ref();
    if old_styled.len() != new_styled.len() {
        return false;
    }
    if old_styled != new_styled {
        return false;
    }
    true
}