Skip to main content

azul_core/
styled_dom.rs

1//! `StyledDom` — the result of applying CSS styles to a DOM tree.
2//!
3//! This module contains [`StyledDom`], which is produced by combining a [`Dom`]
4//! with a [`Css`] stylesheet via [`StyledDom::create`]. It stores the flattened
5//! node hierarchy, per-node styled states, cascade information, and the CSS
6//! property cache. Restyle operations (`restyle_nodes_hover`, etc.) allow
7//! incremental updates when pseudo-class states change at runtime.
8//!
9//! `StyledDom` is the primary input to the layout engine.
10
11use alloc::{boxed::Box, collections::btree_map::BTreeMap, string::String, vec::Vec};
12use core::{
13    fmt,
14    hash::{Hash, Hasher},
15};
16
17use azul_css::{
18    css::Css,
19    props::{
20        basic::{StyleFontFamily, StyleFontFamilyVec, StyleFontSize},
21        property::{
22            BoxDecorationBreakValue, BreakInsideValue, CaretAnimationDurationValue,
23            CaretColorValue, ColumnCountValue, ColumnFillValue, ColumnRuleColorValue,
24            ColumnRuleStyleValue, ColumnRuleWidthValue, ColumnSpanValue, ColumnWidthValue,
25            ContentValue, CounterIncrementValue, CounterResetValue, CssProperty, CssPropertyType,
26            RelayoutScope,
27            FlowFromValue, FlowIntoValue, LayoutAlignContentValue, LayoutAlignItemsValue,
28            LayoutAlignSelfValue, LayoutBorderBottomWidthValue, LayoutBorderLeftWidthValue,
29            LayoutBorderRightWidthValue, LayoutBorderTopWidthValue, LayoutBoxSizingValue,
30            LayoutClearValue, LayoutColumnGapValue, LayoutDisplayValue, LayoutFlexBasisValue,
31            LayoutFlexDirectionValue, LayoutFlexGrowValue, LayoutFlexShrinkValue,
32            LayoutFlexWrapValue, LayoutFloatValue, LayoutGapValue, LayoutGridAutoColumnsValue,
33            LayoutGridAutoFlowValue, LayoutGridAutoRowsValue, LayoutGridColumnValue,
34            LayoutGridRowValue, LayoutGridTemplateColumnsValue, LayoutGridTemplateRowsValue,
35            LayoutHeightValue, LayoutInsetBottomValue, LayoutJustifyContentValue,
36            LayoutJustifyItemsValue, LayoutJustifySelfValue, LayoutLeftValue,
37            LayoutMarginBottomValue, LayoutMarginLeftValue, LayoutMarginRightValue,
38            LayoutMarginTopValue, LayoutMaxHeightValue, LayoutMaxWidthValue, LayoutMinHeightValue,
39            LayoutMinWidthValue, LayoutOverflowValue, LayoutPaddingBottomValue,
40            LayoutPaddingLeftValue, LayoutPaddingRightValue, LayoutPaddingTopValue,
41            LayoutPositionValue, LayoutRightValue, LayoutRowGapValue, LayoutScrollbarWidthValue,
42            LayoutTextJustifyValue, LayoutTopValue, LayoutWidthValue, LayoutWritingModeValue,
43            LayoutZIndexValue, OrphansValue, PageBreakValue,
44            SelectionBackgroundColorValue, SelectionColorValue, ShapeImageThresholdValue,
45            ShapeMarginValue, ShapeOutsideValue, StringSetValue, StyleBackfaceVisibilityValue,
46            StyleBackgroundContentVecValue, StyleBackgroundPositionVecValue,
47            StyleBackgroundRepeatVecValue, StyleBackgroundSizeVecValue,
48            StyleBorderBottomColorValue, StyleBorderBottomLeftRadiusValue,
49            StyleBorderBottomRightRadiusValue, StyleBorderBottomStyleValue,
50            StyleBorderLeftColorValue, StyleBorderLeftStyleValue, StyleBorderRightColorValue,
51            StyleBorderRightStyleValue, StyleBorderTopColorValue, StyleBorderTopLeftRadiusValue,
52            StyleBorderTopRightRadiusValue, StyleBorderTopStyleValue, StyleBoxShadowValue,
53            StyleCursorValue, StyleDirectionValue, StyleFilterVecValue, StyleFontFamilyVecValue,
54            StyleFontSizeValue, StyleFontValue, StyleHyphensValue, StyleLetterSpacingValue,
55            StyleLineHeightValue, StyleMixBlendModeValue, StyleOpacityValue,
56            StylePerspectiveOriginValue, StyleScrollbarColorValue, StyleTabSizeValue,
57            StyleTextAlignValue, StyleTextColorValue, StyleTransformOriginValue,
58            StyleTransformVecValue, StyleVisibilityValue, StyleWhiteSpaceValue,
59            StyleWordSpacingValue, WidowsValue,
60        },
61        style::StyleTextColor,
62    },
63    AzString,
64};
65
66use crate::{
67    callbacks::Update,
68    dom::{Dom, DomId, NodeData, NodeDataVec, OptionTabIndex, TabIndex, TagId},
69    events::{RelayoutNodes, RestyleNodes},
70    id::{
71        Node, NodeDataContainer, NodeDataContainerRef, NodeDataContainerRefMut, NodeHierarchy,
72        NodeId,
73    },
74    menu::Menu,
75    prop_cache::{CssPropertyCache, CssPropertyCachePtr},
76    refany::RefAny,
77    resources::{Au, ImageCache, ImageRef, ImmediateFontId, RendererResources},
78    style::{
79        construct_html_cascade_tree, matches_html_element, rule_ends_with, CascadeInfo,
80        CascadeInfoVec,
81    },
82    FastBTreeSet, OrderedMap,
83};
84
85#[repr(C)]
86#[derive(Debug, Clone, PartialEq, Hash, PartialOrd, Eq, Ord)]
87pub struct ChangedCssProperty {
88    pub previous_state: StyledNodeState,
89    pub previous_prop: CssProperty,
90    pub current_state: StyledNodeState,
91    pub current_prop: CssProperty,
92}
93
94impl_option!(
95    ChangedCssProperty,
96    OptionChangedCssProperty,
97    copy = false,
98    [Debug, Clone, PartialEq, Hash, PartialOrd, Eq, Ord]
99);
100
101impl_vec!(ChangedCssProperty, ChangedCssPropertyVec, ChangedCssPropertyVecDestructor, ChangedCssPropertyVecDestructorType, ChangedCssPropertyVecSlice, OptionChangedCssProperty);
102impl_vec_debug!(ChangedCssProperty, ChangedCssPropertyVec);
103impl_vec_partialord!(ChangedCssProperty, ChangedCssPropertyVec);
104impl_vec_clone!(
105    ChangedCssProperty,
106    ChangedCssPropertyVec,
107    ChangedCssPropertyVecDestructor
108);
109impl_vec_partialeq!(ChangedCssProperty, ChangedCssPropertyVec);
110
111/// Focus state change for restyle operations
112#[derive(Debug, Clone, PartialEq)]
113pub struct FocusChange {
114    /// Node that lost focus (if any)
115    pub lost_focus: Option<NodeId>,
116    /// Node that gained focus (if any)
117    pub gained_focus: Option<NodeId>,
118}
119
120/// Hover state change for restyle operations
121#[derive(Debug, Clone, PartialEq)]
122pub struct HoverChange {
123    /// Nodes that the mouse left
124    pub left_nodes: Vec<NodeId>,
125    /// Nodes that the mouse entered
126    pub entered_nodes: Vec<NodeId>,
127}
128
129/// Active (mouse down) state change for restyle operations
130#[derive(Debug, Clone, PartialEq)]
131pub struct ActiveChange {
132    /// Nodes that were deactivated (mouse up)
133    pub deactivated: Vec<NodeId>,
134    /// Nodes that were activated (mouse down)
135    pub activated: Vec<NodeId>,
136}
137
138/// Result of a restyle operation, indicating what needs to be updated
139#[derive(Debug, Clone, Default)]
140pub struct RestyleResult {
141    /// Nodes whose CSS properties changed, with details of the changes
142    pub changed_nodes: RestyleNodes,
143    /// Whether layout needs to be recalculated (layout properties changed)
144    pub needs_layout: bool,
145    /// Whether display list needs regeneration (visual properties changed)
146    pub needs_display_list: bool,
147    /// Whether only GPU-level properties changed (opacity, transform)
148    /// If true and needs_display_list is false, we can update via GPU without display list rebuild
149    pub gpu_only_changes: bool,
150    /// The highest `RelayoutScope` seen across all property changes.
151    ///
152    /// This enables the IFC incremental layout optimization (Phase 2):
153    /// - `None`      → repaint only, zero layout work
154    /// - `IfcOnly`   → only the affected IFC needs re-shaping/repositioning
155    /// - `SizingOnly`→ this node's size changed, parent repositions siblings
156    /// - `Full`      → full subtree relayout
157    ///
158    /// When `max_relayout_scope <= IfcOnly`, the layout engine can skip
159    /// full `calculate_layout_for_subtree` and use the IFC fast path instead.
160    pub max_relayout_scope: RelayoutScope,
161}
162
163impl RestyleResult {
164    /// Returns true if any changes occurred
165    pub fn has_changes(&self) -> bool {
166        !self.changed_nodes.is_empty()
167    }
168
169    /// Merge another RestyleResult into this one
170    pub fn merge(&mut self, other: RestyleResult) {
171        for (node_id, changes) in other.changed_nodes {
172            self.changed_nodes.entry(node_id).or_default().extend(changes);
173        }
174        self.needs_layout = self.needs_layout || other.needs_layout;
175        self.needs_display_list = self.needs_display_list || other.needs_display_list;
176        self.gpu_only_changes = self.gpu_only_changes && other.gpu_only_changes;
177        // Keep the highest (most expensive) scope
178        if other.max_relayout_scope > self.max_relayout_scope {
179            self.max_relayout_scope = other.max_relayout_scope;
180        }
181    }
182}
183
184/// NOTE: multiple states can be active at the same time
185///
186/// Tracks all CSS pseudo-class states for a node.
187/// Each flag is independent - a node can be both :hover and :focus simultaneously.
188#[repr(C)]
189#[derive(Clone, Copy, PartialEq, Hash, PartialOrd, Eq, Ord, Default)]
190pub struct StyledNodeState {
191    /// Element is being hovered (:hover)
192    pub hover: bool,
193    /// Element is active/being clicked (:active)
194    pub active: bool,
195    /// Element has focus (:focus)
196    pub focused: bool,
197    /// Element is disabled (:disabled)
198    pub disabled: bool,
199    /// Element is checked/selected (:checked)
200    pub checked: bool,
201    /// Element or descendant has focus (:focus-within)
202    pub focus_within: bool,
203    /// Link has been visited (:visited)
204    pub visited: bool,
205    /// Window is not focused (:backdrop) - GTK compatibility
206    pub backdrop: bool,
207    /// Element is currently being dragged (:dragging)
208    pub dragging: bool,
209    /// A dragged element is over this drop target (:drag-over)
210    pub drag_over: bool,
211}
212
213impl 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
253impl StyledNodeState {
254    /// Creates a new state with all states set to false (normal state).
255    pub const fn new() -> Self {
256        StyledNodeState {
257            hover: false,
258            active: false,
259            focused: false,
260            disabled: false,
261            checked: false,
262            focus_within: false,
263            visited: false,
264            backdrop: false,
265            dragging: false,
266            drag_over: false,
267        }
268    }
269
270    /// Check if a specific pseudo-state is active
271    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    pub fn is_normal(&self) -> bool {
290        !self.hover
291            && !self.active
292            && !self.focused
293            && !self.disabled
294            && !self.checked
295            && !self.focus_within
296            && !self.visited
297            && !self.backdrop
298            && !self.dragging
299            && !self.drag_over
300    }
301
302    /// Create from PseudoStateFlags
303    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)]
322pub struct StyledNode {
323    /// Current state of this styled node (used later for caching the style / layout)
324    pub styled_node_state: StyledNodeState,
325}
326
327impl_option!(
328    StyledNode,
329    OptionStyledNode,
330    copy = false,
331    [Debug, Clone, PartialEq, PartialOrd]
332);
333
334impl_vec!(StyledNode, StyledNodeVec, StyledNodeVecDestructor, StyledNodeVecDestructorType, StyledNodeVecSlice, OptionStyledNode);
335impl_vec_mut!(StyledNode, StyledNodeVec);
336impl_vec_debug!(StyledNode, StyledNodeVec);
337impl_vec_partialord!(StyledNode, StyledNodeVec);
338impl_vec_clone!(StyledNode, StyledNodeVec, StyledNodeVecDestructor);
339impl_vec_partialeq!(StyledNode, StyledNodeVec);
340
341impl StyledNodeVec {
342    /// Returns an immutable container reference for indexed access.
343    pub fn as_container<'a>(&'a self) -> NodeDataContainerRef<'a, StyledNode> {
344        NodeDataContainerRef {
345            internal: self.as_ref(),
346        }
347    }
348    /// Returns a mutable container reference for indexed access.
349    pub fn as_container_mut<'a>(&'a mut self) -> NodeDataContainerRefMut<'a, StyledNode> {
350        NodeDataContainerRefMut {
351            internal: self.as_mut(),
352        }
353    }
354}
355
356#[test]
357fn test_css_styling_with_nested_divs() {
358    let s = "
359        html, body, p {
360            margin: 0;
361            padding: 0;
362        }
363        #div1 {
364            border: solid black;
365            height: 2in;
366            position: absolute;
367            top: 1in;
368            width: 3in;
369        }
370        div div {
371            background: blue;
372            height: 1in;
373            position: fixed;
374            width: 1in;
375        }
376    ";
377
378    let css = azul_css::parser2::new_from_str(s);
379    let _styled_dom = Dom::create_body()
380        .with_children(
381            vec![Dom::create_div()
382                .with_ids_and_classes(
383                    vec![crate::dom::IdOrClass::Id("div1".to_string().into())].into(),
384                )
385                .with_children(vec![Dom::create_div()].into())]
386            .into(),
387        )
388        .with_component_css(css.0);
389}
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]
399fn test_recompute_preserves_hot_flag_has_background() {
400    use azul_css::compact_cache::HOT_FLAG_HAS_BACKGROUND;
401
402    let css_str = "
403        body { margin: 0; padding: 0; }
404        .painted { background: red; width: 100px; height: 100px; }
405    ";
406    let css = azul_css::parser2::new_from_str(css_str).0;
407
408    let mut dom = Dom::create_body().with_children(
409        vec![Dom::create_div().with_class("painted".to_string().into())].into(),
410    );
411    let mut styled = StyledDom::create(&mut dom, css);
412
413    // Frame 1: find the painted node by walking its hot_flags.
414    let any_bg_frame1 = {
415        let cache = styled
416            .css_property_cache
417            .ptr
418            .compact_cache
419            .as_ref()
420            .expect("compact_cache populated by create_from_compact_dom");
421        (0..styled.node_hierarchy.as_ref().len())
422            .any(|i| cache.tier2_cold[i].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0)
423    };
424    assert!(
425        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    styled.recompute_inheritance_and_compact_cache();
433
434    let any_bg_frame2 = {
435        let cache = styled
436            .css_property_cache
437            .ptr
438            .compact_cache
439            .as_ref()
440            .expect("compact_cache rebuilt by recompute_inheritance_and_compact_cache");
441        (0..styled.node_hierarchy.as_ref().len())
442            .any(|i| cache.tier2_cold[i].hot_flags & HOT_FLAG_HAS_BACKGROUND != 0)
443    };
444    assert!(
445        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}
453
454/// Calculated hash of a font-family
455#[derive(Copy, Clone, Hash, PartialEq, Eq, Ord, PartialOrd)]
456pub struct StyleFontFamilyHash(pub u64);
457
458impl ::core::fmt::Debug for StyleFontFamilyHash {
459    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
460        write!(f, "StyleFontFamilyHash({})", self.0)
461    }
462}
463
464impl StyleFontFamilyHash {
465    /// Computes a 64-bit hash of a font family for cache lookups.
466    pub fn new(family: &StyleFontFamily) -> Self {
467        use std::hash::Hasher;
468        let mut hasher = std::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)]
476pub struct StyleFontFamiliesHash(pub u64);
477
478impl ::core::fmt::Debug for StyleFontFamiliesHash {
479    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
480        write!(f, "StyleFontFamiliesHash({})", self.0)
481    }
482}
483
484impl StyleFontFamiliesHash {
485    /// Computes a 64-bit hash of multiple font families for cache lookups.
486    pub fn new(families: &[StyleFontFamily]) -> Self {
487        use std::hash::Hasher;
488        let mut hasher = std::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)]
539pub 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
545impl fmt::Debug for NodeHierarchyItemId {
546    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
547        match self.into_crate_internal() {
548            Some(n) => write!(f, "Some(NodeId({}))", n),
549            None => write!(f, "None"),
550        }
551    }
552}
553
554impl fmt::Display for NodeHierarchyItemId {
555    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
556        write!(f, "{:?}", self)
557    }
558}
559
560impl 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
586impl_option!(
587    NodeHierarchyItemId,
588    OptionNodeHierarchyItemId,
589    [Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash]
590);
591
592impl_vec!(NodeHierarchyItemId, NodeHierarchyItemIdVec, NodeHierarchyItemIdVecDestructor, NodeHierarchyItemIdVecDestructorType, NodeHierarchyItemIdVecSlice, OptionNodeHierarchyItemId);
593impl_vec_mut!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
594impl_vec_debug!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
595impl_vec_ord!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
596impl_vec_eq!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
597impl_vec_hash!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
598impl_vec_partialord!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
599impl_vec_clone!(NodeHierarchyItemId, NodeHierarchyItemIdVec, NodeHierarchyItemIdVecDestructor);
600impl_vec_partialeq!(NodeHierarchyItemId, NodeHierarchyItemIdVec);
601
602impl NodeHierarchyItemId {
603    /// Decodes to `Option<NodeId>` (0 = None, n > 0 = Some(NodeId(n-1))).
604    #[inline]
605    pub const fn into_crate_internal(&self) -> Option<NodeId> {
606        NodeId::from_usize(self.inner)
607    }
608
609    /// Encodes from `Option<NodeId>` (None → 0, Some(NodeId(n)) → n+1).
610    #[inline]
611    pub const fn from_crate_internal(t: Option<NodeId>) -> Self {
612        Self {
613            inner: NodeId::into_raw(&t),
614        }
615    }
616}
617
618impl From<Option<NodeId>> for NodeHierarchyItemId {
619    #[inline]
620    fn from(opt: Option<NodeId>) -> Self {
621        NodeHierarchyItemId::from_crate_internal(opt)
622    }
623}
624
625impl 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)]
634pub struct NodeHierarchyItem {
635    pub parent: usize,
636    pub previous_sibling: usize,
637    pub next_sibling: usize,
638    pub last_child: usize,
639}
640
641impl_option!(
642    NodeHierarchyItem,
643    OptionNodeHierarchyItem,
644    [Debug, Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash]
645);
646
647impl 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
659impl From<Node> for NodeHierarchyItem {
660    fn from(node: Node) -> NodeHierarchyItem {
661        NodeHierarchyItem {
662            parent: NodeId::into_raw(&node.parent),
663            previous_sibling: NodeId::into_raw(&node.previous_sibling),
664            next_sibling: NodeId::into_raw(&node.next_sibling),
665            last_child: NodeId::into_raw(&node.last_child),
666        }
667    }
668}
669
670impl NodeHierarchyItem {
671    /// Returns the parent node ID, if any.
672    pub fn parent_id(&self) -> Option<NodeId> {
673        NodeId::from_usize(self.parent)
674    }
675    /// Returns the previous sibling node ID, if any.
676    pub fn previous_sibling_id(&self) -> Option<NodeId> {
677        NodeId::from_usize(self.previous_sibling)
678    }
679    /// Returns the next sibling node ID, if any.
680    pub fn next_sibling_id(&self) -> Option<NodeId> {
681        NodeId::from_usize(self.next_sibling)
682    }
683    /// Returns the first child node ID (current_node_id + 1 if has children).
684    pub fn first_child_id(&self, current_node_id: NodeId) -> Option<NodeId> {
685        self.last_child_id().map(|_| current_node_id + 1)
686    }
687    /// Returns the last child node ID, if any.
688    pub fn last_child_id(&self) -> Option<NodeId> {
689        NodeId::from_usize(self.last_child)
690    }
691}
692
693impl_vec!(NodeHierarchyItem, NodeHierarchyItemVec, NodeHierarchyItemVecDestructor, NodeHierarchyItemVecDestructorType, NodeHierarchyItemVecSlice, OptionNodeHierarchyItem);
694impl_vec_mut!(NodeHierarchyItem, NodeHierarchyItemVec);
695impl_vec_debug!(AzNode, NodeHierarchyItemVec);
696impl_vec_partialord!(AzNode, NodeHierarchyItemVec);
697impl_vec_clone!(
698    NodeHierarchyItem,
699    NodeHierarchyItemVec,
700    NodeHierarchyItemVecDestructor
701);
702impl_vec_partialeq!(AzNode, NodeHierarchyItemVec);
703
704impl NodeHierarchyItemVec {
705    /// Returns an immutable container reference for indexed access.
706    pub fn as_container<'a>(&'a self) -> NodeDataContainerRef<'a, NodeHierarchyItem> {
707        NodeDataContainerRef {
708            internal: self.as_ref(),
709        }
710    }
711    /// Returns a mutable container reference for indexed access.
712    pub fn as_container_mut<'a>(&'a mut self) -> NodeDataContainerRefMut<'a, NodeHierarchyItem> {
713        NodeDataContainerRefMut {
714            internal: self.as_mut(),
715        }
716    }
717}
718
719impl<'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)]
734pub struct ParentWithNodeDepth {
735    pub depth: usize,
736    pub node_id: NodeHierarchyItemId,
737}
738
739impl_option!(
740    ParentWithNodeDepth,
741    OptionParentWithNodeDepth,
742    [Copy, Clone, PartialEq, Eq, Ord, PartialOrd, Hash]
743);
744
745impl 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
756impl_vec!(ParentWithNodeDepth, ParentWithNodeDepthVec, ParentWithNodeDepthVecDestructor, ParentWithNodeDepthVecDestructorType, ParentWithNodeDepthVecSlice, OptionParentWithNodeDepth);
757impl_vec_mut!(ParentWithNodeDepth, ParentWithNodeDepthVec);
758impl_vec_debug!(ParentWithNodeDepth, ParentWithNodeDepthVec);
759impl_vec_partialord!(ParentWithNodeDepth, ParentWithNodeDepthVec);
760impl_vec_clone!(
761    ParentWithNodeDepth,
762    ParentWithNodeDepthVec,
763    ParentWithNodeDepthVecDestructor
764);
765impl_vec_partialeq!(ParentWithNodeDepth, ParentWithNodeDepthVec);
766
767#[derive(Debug, Clone, PartialEq, Eq, Ord, PartialOrd)]
768#[repr(C)]
769pub 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
778impl_option!(
779    TagIdToNodeIdMapping,
780    OptionTagIdToNodeIdMapping,
781    copy = false,
782    [Debug, Clone, PartialEq, Eq, Ord, PartialOrd]
783);
784
785impl_vec!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec, TagIdToNodeIdMappingVecDestructor, TagIdToNodeIdMappingVecDestructorType, TagIdToNodeIdMappingVecSlice, OptionTagIdToNodeIdMapping);
786impl_vec_mut!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
787impl_vec_debug!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
788impl_vec_partialord!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
789impl_vec_clone!(
790    TagIdToNodeIdMapping,
791    TagIdToNodeIdMappingVec,
792    TagIdToNodeIdMappingVecDestructor
793);
794impl_vec_partialeq!(TagIdToNodeIdMapping, TagIdToNodeIdMappingVec);
795
796#[derive(Debug, Clone, PartialEq, PartialOrd)]
797#[repr(C)]
798pub 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
806impl_option!(
807    ContentGroup,
808    OptionContentGroup,
809    copy = false,
810    [Debug, Clone, PartialEq, PartialOrd]
811);
812
813impl_vec!(ContentGroup, ContentGroupVec, ContentGroupVecDestructor, ContentGroupVecDestructorType, ContentGroupVecSlice, OptionContentGroup);
814impl_vec_mut!(ContentGroup, ContentGroupVec);
815impl_vec_debug!(ContentGroup, ContentGroupVec);
816impl_vec_partialord!(ContentGroup, ContentGroupVec);
817impl_vec_clone!(ContentGroup, ContentGroupVec, ContentGroupVecDestructor);
818impl_vec_partialeq!(ContentGroup, ContentGroupVec);
819
820#[derive(Debug, PartialEq, Clone)]
821#[repr(C)]
822pub 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}
836impl_option!(
837    StyledDom,
838    OptionStyledDom,
839    copy = false,
840    [Debug, Clone, PartialEq]
841);
842
843impl Default for StyledDom {
844    fn default() -> Self {
845        let root_node: NodeHierarchyItem = Node::ROOT.into();
846        let root_node_id: NodeHierarchyItemId =
847            NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO));
848        Self {
849            root: root_node_id,
850            node_hierarchy: vec![root_node].into(),
851            node_data: vec![NodeData::create_body()].into(),
852            styled_nodes: vec![StyledNode::default()].into(),
853            cascade_info: vec![CascadeInfo {
854                index_in_parent: 0,
855                is_last_child: true,
856            }]
857            .into(),
858            tag_ids_to_node_ids: Vec::new().into(),
859            non_leaf_nodes: vec![ParentWithNodeDepth {
860                depth: 0,
861                node_id: root_node_id,
862            }]
863            .into(),
864            nodes_with_window_callbacks: Vec::new().into(),
865            nodes_with_datasets: Vec::new().into(),
866            css_property_cache: CssPropertyCachePtr::new(CssPropertyCache::empty(1)),
867            dom_id: DomId::ROOT_ID,
868        }
869    }
870}
871
872/// Per-field heap-byte breakdown of a `StyledDom`.
873#[derive(Debug, Clone, Default)]
874pub 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
886impl 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
899impl 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    pub fn create(dom: &mut Dom, css: Css) -> Self {
940        use core::mem;
941
942        let mut swap_dom = Dom::create_body();
943        mem::swap(dom, &mut swap_dom);
944
945        let compact_dom: CompactDom = swap_dom.into();
946        let node_hierarchy: NodeHierarchyItemVec = compact_dom
947            .node_hierarchy
948            .as_ref()
949            .internal
950            .iter()
951            .map(|i| (*i).into())
952            .collect::<Vec<NodeHierarchyItem>>()
953            .into();
954
955        Self::create_from_compact_dom(compact_dom, css, node_hierarchy)
956    }
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    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        let mut combined_rules: Vec<azul_css::css::CssRuleBlock> = Vec::new();
969        for css_with_id in fast_dom.css.into_library_owned_vec() {
970            combined_rules.extend(css_with_id.css.rules.into_library_owned_vec());
971        }
972        let combined_css = if combined_rules.is_empty() {
973            Css::empty()
974        } else {
975            Css::new(combined_rules)
976        };
977
978        // 2. Convert NodeHierarchyItemVec → NodeHierarchy (Vec<Node>)
979        //    for cascade tree computation
980        let node_hierarchy_items = fast_dom.node_hierarchy;
981        let nodes: Vec<crate::id::Node> = node_hierarchy_items.as_ref()
982            .iter()
983            .map(|item| crate::id::Node {
984                parent: NodeId::from_usize(item.parent),
985                previous_sibling: NodeId::from_usize(item.previous_sibling),
986                next_sibling: NodeId::from_usize(item.next_sibling),
987                last_child: NodeId::from_usize(item.last_child),
988            })
989            .collect();
990        let node_hierarchy_internal = crate::id::NodeHierarchy { internal: nodes };
991
992        // 3. Build CompactDom from the flat arenas (no conversion needed)
993        let node_data_vec = fast_dom.node_data.into_library_owned_vec();
994        let compact_dom = CompactDom {
995            node_hierarchy: node_hierarchy_internal,
996            node_data: crate::id::NodeDataContainer { internal: node_data_vec },
997            root: NodeId::ZERO,
998        };
999
1000        // 4. Delegate to create() which handles cascade, UA CSS, etc.
1001        //    We need a mutable Dom to pass to create(), but we already have CompactDom.
1002        //    Instead, inline the cascade logic from create() with our CompactDom.
1003        Self::create_from_compact_dom(compact_dom, combined_css, node_hierarchy_items)
1004    }
1005
1006    /// Internal: creates StyledDom from a CompactDom + CSS + pre-built hierarchy items.
1007    /// Shared by both the Slow path (create → convert_dom_into_compact_dom → this)
1008    /// and the Fast path (create_from_fast_dom → this).
1009    fn create_from_compact_dom(
1010        compact_dom: CompactDom,
1011        mut css: Css,
1012        node_hierarchy: NodeHierarchyItemVec,
1013    ) -> Self {
1014        use crate::dom::EventFilter;
1015
1016        static CASCADE_BREAKDOWN: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1017        let cascade_dbg = *CASCADE_BREAKDOWN.get_or_init(crate::profile::memory_enabled);
1018
1019        let node_count = compact_dom.len();
1020
1021        let non_leaf_nodes = compact_dom
1022            .node_hierarchy
1023            .as_ref()
1024            .get_parents_sorted_by_depth();
1025
1026        let mut styled_nodes = vec![
1027            StyledNode {
1028                styled_node_state: StyledNodeState::new()
1029            };
1030            node_count
1031        ];
1032
1033        let mut css_property_cache = CssPropertyCache::empty(compact_dom.node_data.len());
1034
1035        let html_tree = construct_html_cascade_tree(
1036            &compact_dom.node_hierarchy.as_ref(),
1037            &non_leaf_nodes[..],
1038            &compact_dom.node_data.as_ref(),
1039        );
1040
1041        let non_leaf_nodes = non_leaf_nodes
1042            .iter()
1043            .map(|(depth, node_id)| ParentWithNodeDepth {
1044                depth: *depth,
1045                node_id: NodeHierarchyItemId::from_crate_internal(Some(*node_id)),
1046            })
1047            .collect::<Vec<_>>();
1048
1049        let non_leaf_nodes: ParentWithNodeDepthVec = non_leaf_nodes.into();
1050
1051        let _restyle_tag_ids = css_property_cache.restyle(
1052            &mut css,
1053            &compact_dom.node_data.as_ref(),
1054            &node_hierarchy,
1055            &non_leaf_nodes,
1056            &html_tree.as_ref(),
1057        );
1058
1059        // Drop the CSS object now — selectors/declarations are no longer needed
1060        // after restyle has populated css_props. This frees ~500 KiB of stylesheet
1061        // data structures (CssRuleBlock, CssPathSelector, CssDeclaration).
1062        drop(css);
1063
1064        // Apply UA defaults + compute inherited values so consumers that
1065        // read `css_property_cache.computed_values` (the web/HTML
1066        // renderer in `dll/src/web/html_render.rs`) see resolved
1067        // properties. The compact cache below stores the same info in
1068        // a different layout for the desktop renderer; computed_values
1069        // is the "tall" form that the web renderer's CSS emitter
1070        // (`emit_css_from_cache`) walks per node.
1071        css_property_cache.apply_ua_css(compact_dom.node_data.as_ref().internal);
1072        css_property_cache.compute_inherited_values(
1073            node_hierarchy.as_container().internal,
1074            compact_dom.node_data.as_ref().internal,
1075        );
1076
1077        let prev_font_hashes: Vec<u64> = css_property_cache.compact_cache
1078            .as_ref()
1079            .map(|c| c.prev_font_hashes.clone())
1080            .unwrap_or_default();
1081        let compact = css_property_cache.build_compact_cache_with_inheritance(
1082            compact_dom.node_data.as_ref().internal,
1083            node_hierarchy.as_container().internal,
1084            &prev_font_hashes,
1085        );
1086        css_property_cache.compact_cache = Some(compact);
1087        let pre_prune = if cascade_dbg {
1088            Some(css_property_cache.memory_breakdown())
1089        } else { None };
1090        css_property_cache.prune_compact_normal_props();
1091        if let Some(pre) = pre_prune {
1092            let post = css_property_cache.memory_breakdown();
1093            eprintln!("[PRUNE] css_props {} → {} KiB  cascaded {} → {} KiB  (saved {} KiB)",
1094                pre.css_props_bytes / 1024, post.css_props_bytes / 1024,
1095                pre.cascaded_props_bytes / 1024, post.cascaded_props_bytes / 1024,
1096                (pre.total_bytes().saturating_sub(post.total_bytes())) / 1024);
1097        }
1098
1099        let tag_ids = css_property_cache.generate_tag_ids(
1100            &compact_dom.node_data.as_ref(),
1101            &node_hierarchy,
1102        );
1103
1104        if cascade_dbg {
1105            let bd = css_property_cache.memory_breakdown();
1106            eprintln!("[CASCADE] {} nodes  cascaded_props={} KiB  css_props={} KiB  compact={} KiB  computed={} KiB  total={} KiB",
1107                node_count,
1108                bd.cascaded_props_bytes / 1024, bd.css_props_bytes / 1024,
1109                bd.compact_cache_bytes / 1024, bd.computed_values_bytes / 1024,
1110                bd.total_bytes() / 1024);
1111        }
1112
1113        // Collect callback/dataset nodes in a single pass (avoids 3 separate 50K scans).
1114        // For XHTML-parsed DOMs with no callbacks, this early-exits immediately.
1115        let has_any_callbacks = compact_dom.node_data.as_ref().internal.iter()
1116            .any(|c| !c.get_callbacks().is_empty() || c.get_dataset().is_some());
1117
1118        let (nodes_with_window_callbacks, nodes_with_datasets) = if has_any_callbacks {
1119            let mut win_cbs = Vec::new();
1120            let mut datasets = Vec::new();
1121            for (node_id, c) in compact_dom.node_data.as_ref().internal.iter().enumerate() {
1122                let cbs = c.get_callbacks();
1123                let has_dataset = c.get_dataset().is_some();
1124                if !cbs.is_empty() || has_dataset {
1125                    datasets.push(NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_id))));
1126                }
1127                for cb in cbs.iter() {
1128                    if let EventFilter::Window(_) = cb.event {
1129                        win_cbs.push(NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(node_id))));
1130                        break;
1131                    }
1132                }
1133            }
1134            (win_cbs, datasets)
1135        } else {
1136            (Vec::new(), Vec::new())
1137        };
1138        let mut styled_dom = StyledDom {
1139            root: NodeHierarchyItemId::from_crate_internal(Some(compact_dom.root)),
1140            node_hierarchy,
1141            node_data: compact_dom.node_data.internal.into(),
1142            cascade_info: html_tree.internal.into(),
1143            styled_nodes: styled_nodes.into(),
1144            tag_ids_to_node_ids: tag_ids.into(),
1145            nodes_with_window_callbacks: nodes_with_window_callbacks.into(),
1146            nodes_with_datasets: nodes_with_datasets.into(),
1147            non_leaf_nodes,
1148            css_property_cache: CssPropertyCachePtr::new(css_property_cache),
1149            dom_id: DomId::ROOT_ID,
1150        };
1151        #[cfg(feature = "table_layout")]
1152        if let Err(_e) = crate::dom_table::generate_anonymous_table_elements(&mut styled_dom) {
1153        }
1154
1155        styled_dom
1156    }
1157
1158    /// Creates a StyledDom from a recursive Dom tree with deferred CSS.
1159    ///
1160    /// This is the Phase 7.2 entry point: the layout callback returns a recursive
1161    /// `Dom` with `css: Vec<Css>` on each node. This function:
1162    ///
1163    /// 1. Collects all CSS objects from the recursive tree
1164    /// 2. Flattens the Dom into contiguous arrays (CompactDom)
1165    /// 3. Merges all CSS objects and runs a single cascade pass
1166    /// 4. Runs apply_ua_css → compute_inherited_values → build_compact_cache
1167    /// 5. Generates anonymous table elements
1168    pub fn create_from_dom(mut dom: Dom) -> Self {
1169        use azul_css::css::Css;
1170
1171        // 1. Collect all CSS objects from the recursive Dom tree
1172        let mut all_css = Vec::new();
1173        collect_css_from_dom(&dom, &mut all_css);
1174
1175        // 2. Merge all CSS objects into one combined Css
1176        let mut combined_css = if all_css.is_empty() {
1177            Css::empty()
1178        } else {
1179            let mut combined_rules: Vec<azul_css::css::CssRuleBlock> = Vec::new();
1180            for css in all_css {
1181                combined_rules.extend(css.rules.into_library_owned_vec());
1182            }
1183            Css::new(combined_rules)
1184        };
1185
1186        // 3. Strip CSS from all Dom nodes before flattening
1187        //    (CSS is already collected, don't need it in the flat tree)
1188        strip_css_from_dom(&mut dom);
1189
1190        // 4. Use existing StyledDom::create to flatten + cascade
1191        Self::create(&mut dom, combined_css)
1192    }
1193
1194    /// Appends another `StyledDom` as a child to the `self.root`
1195    /// without re-styling the DOM itself
1196    pub fn append_child(&mut self, other: Self) {
1197        let self_root_id = self.root.into_crate_internal().unwrap_or(NodeId::ZERO);
1198        let current_root_children_count = self_root_id
1199            .az_children(&self.node_hierarchy.as_container())
1200            .count();
1201        self.append_child_with_index(other, current_root_children_count);
1202        self.finalize_non_leaf_nodes();
1203    }
1204
1205    /// Optimized version of `append_child` that takes the child index directly
1206    /// instead of counting existing children (O(1) instead of O(n))
1207    pub fn append_child_with_index(&mut self, mut other: Self, child_index: usize) {
1208        // shift all the node ids in other by self.len()
1209        let self_len = self.node_hierarchy.as_ref().len();
1210        let other_len = other.node_hierarchy.as_ref().len();
1211        let self_root_id = self.root.into_crate_internal().unwrap_or(NodeId::ZERO);
1212        let other_root_id = other.root.into_crate_internal().unwrap_or(NodeId::ZERO);
1213
1214        // Use provided index instead of counting children
1215        other.cascade_info.as_mut()[other_root_id.index()].index_in_parent = child_index as u32;
1216        other.cascade_info.as_mut()[other_root_id.index()].is_last_child = true;
1217
1218        self.cascade_info.append(&mut other.cascade_info);
1219
1220        // adjust node hierarchy
1221        for other in other.node_hierarchy.as_mut().iter_mut() {
1222            if other.parent != 0 {
1223                other.parent += self_len;
1224            }
1225            if other.previous_sibling != 0 {
1226                other.previous_sibling += self_len;
1227            }
1228            if other.next_sibling != 0 {
1229                other.next_sibling += self_len;
1230            }
1231            if other.last_child != 0 {
1232                other.last_child += self_len;
1233            }
1234        }
1235
1236        other.node_hierarchy.as_container_mut()[other_root_id].parent =
1237            NodeId::into_raw(&Some(self_root_id));
1238        let current_last_child = self.node_hierarchy.as_container()[self_root_id].last_child_id();
1239        other.node_hierarchy.as_container_mut()[other_root_id].previous_sibling =
1240            NodeId::into_raw(&current_last_child);
1241        if let Some(current_last) = current_last_child {
1242            if self.node_hierarchy.as_container_mut()[current_last]
1243                .next_sibling_id()
1244                .is_some()
1245            {
1246                self.node_hierarchy.as_container_mut()[current_last].next_sibling +=
1247                    other_root_id.index() + other_len;
1248            } else {
1249                self.node_hierarchy.as_container_mut()[current_last].next_sibling =
1250                    NodeId::into_raw(&Some(NodeId::new(self_len + other_root_id.index())));
1251            }
1252        }
1253        self.node_hierarchy.as_container_mut()[self_root_id].last_child =
1254            NodeId::into_raw(&Some(NodeId::new(self_len + other_root_id.index())));
1255
1256        self.node_hierarchy.append(&mut other.node_hierarchy);
1257        self.node_data.append(&mut other.node_data);
1258        self.styled_nodes.append(&mut other.styled_nodes);
1259        self.get_css_property_cache_mut()
1260            .append(other.get_css_property_cache_mut());
1261
1262        // Tag IDs are globally unique (AtomicUsize counter) and never collide,
1263        // so we only shift node_id (which changes when DOMs are merged).
1264        for tag_id_node_id in other.tag_ids_to_node_ids.iter_mut() {
1265            tag_id_node_id.node_id.inner += self_len;
1266        }
1267
1268        self.tag_ids_to_node_ids
1269            .append(&mut other.tag_ids_to_node_ids);
1270
1271        for nid in other.nodes_with_window_callbacks.iter_mut() {
1272            nid.inner += self_len;
1273        }
1274        self.nodes_with_window_callbacks
1275            .append(&mut other.nodes_with_window_callbacks);
1276
1277        for nid in other.nodes_with_datasets.iter_mut() {
1278            nid.inner += self_len;
1279        }
1280        self.nodes_with_datasets
1281            .append(&mut other.nodes_with_datasets);
1282
1283        // edge case: if the other StyledDom consists of only one node
1284        // then it is not a parent itself
1285        if other_len != 1 {
1286            for other_non_leaf_node in other.non_leaf_nodes.iter_mut() {
1287                other_non_leaf_node.node_id.inner += self_len;
1288                other_non_leaf_node.depth += 1;
1289            }
1290            self.non_leaf_nodes.append(&mut other.non_leaf_nodes);
1291            // NOTE: Sorting deferred - call finalize_non_leaf_nodes() after all appends
1292        }
1293    }
1294
1295    /// Call this after all append_child_with_index operations are complete
1296    /// to sort non_leaf_nodes by depth (required for correct rendering)
1297    pub fn finalize_non_leaf_nodes(&mut self) {
1298        self.non_leaf_nodes.sort_by(|a, b| a.depth.cmp(&b.depth));
1299    }
1300
1301    /// Same as `append_child()`, but as a builder method
1302    pub fn with_child(mut self, other: Self) -> Self {
1303        self.append_child(other);
1304        self
1305    }
1306
1307    /// Sets the context menu for the root node
1308    pub fn set_context_menu(&mut self, context_menu: Menu) {
1309        if let Some(root_id) = self.root.into_crate_internal() {
1310            self.node_data.as_container_mut()[root_id].set_context_menu(context_menu);
1311        }
1312    }
1313
1314    /// Builder method for setting the context menu
1315    pub fn with_context_menu(mut self, context_menu: Menu) -> Self {
1316        self.set_context_menu(context_menu);
1317        self
1318    }
1319
1320    /// Sets the menu bar for the root node
1321    pub fn set_menu_bar(&mut self, menu_bar: Menu) {
1322        if let Some(root_id) = self.root.into_crate_internal() {
1323            self.node_data.as_container_mut()[root_id].set_menu_bar(menu_bar);
1324        }
1325    }
1326
1327    /// Builder method for setting the menu bar
1328    pub fn with_menu_bar(mut self, menu_bar: Menu) -> Self {
1329        self.set_menu_bar(menu_bar);
1330        self
1331    }
1332
1333    /// Re-compute inherited CSS values and rebuild the compact layout cache.
1334    ///
1335    /// This MUST be called after `append_child()` merges multiple `StyledDom`s.
1336    /// `append_child()` concatenates the CSS property caches but does NOT
1337    /// re-run inheritance or rebuild the compact cache. This means:
1338    ///
1339    /// 1. **Broken inheritance**: Inherited properties (`color`, `font-size`,
1340    ///    `direction`) from the parent DOM do not flow into appended subtrees.
1341    /// 2. **Stale compact cache**: The child's tier 1/2/2b entries still reflect
1342    ///    the child's isolated cascade, not the composed tree.
1343    ///
1344    /// Calling this method after all `append_child()` calls fixes both issues
1345    /// by re-running a full depth-first inheritance pass and rebuilding the
1346    /// compact cache from scratch on the composed tree.
1347    pub fn recompute_inheritance_and_compact_cache(&mut self) {
1348        // Use the _with_inheritance variant: it does inheritance inline (via
1349        // parent-compact-field copy) AND populates hot_flags via
1350        // apply_css_property_to_compact.  The plain build_compact_cache would
1351        // leave HOT_FLAG_HAS_BACKGROUND / HAS_CLIP_PATH / extra_flags at 0,
1352        // causing renderer negative fast-paths to skip paint (regression
1353        // introduced by ff059052b).  No SIGABRT risk — _with_inheritance
1354        // never pushes to the flat cascaded_props storage.
1355        let prev_font_hashes: Vec<u64> = self.css_property_cache
1356            .downcast_mut()
1357            .compact_cache
1358            .as_ref()
1359            .map(|c| c.prev_font_hashes.clone())
1360            .unwrap_or_default();
1361        let compact = self.css_property_cache
1362            .downcast_mut()
1363            .build_compact_cache_with_inheritance(
1364                self.node_data.as_container().internal,
1365                self.node_hierarchy.as_container().internal,
1366                &prev_font_hashes,
1367            );
1368        self.css_property_cache.downcast_mut().compact_cache = Some(compact);
1369    }
1370
1371    /// Re-applies CSS styles to the existing DOM structure.
1372    pub fn restyle(&mut self, mut css: Css) {
1373        let new_tag_ids = self.css_property_cache.downcast_mut().restyle(
1374            &mut css,
1375            &self.node_data.as_container(),
1376            &self.node_hierarchy,
1377            &self.non_leaf_nodes,
1378            &self.cascade_info.as_container(),
1379        );
1380
1381        // Apply UA CSS properties before computing inheritance
1382        self.css_property_cache
1383            .downcast_mut()
1384            .apply_ua_css(self.node_data.as_container().internal);
1385
1386        // Compute inherited values after restyle and apply_ua_css (resolves em, %, etc.)
1387        self.css_property_cache
1388            .downcast_mut()
1389            .compute_inherited_values(
1390                self.node_hierarchy.as_container().internal,
1391                self.node_data.as_container().internal,
1392            );
1393
1394        self.tag_ids_to_node_ids = new_tag_ids.into();
1395    }
1396
1397    /// Returns the total number of nodes in this StyledDom.
1398    #[inline]
1399    pub fn node_count(&self) -> usize {
1400        self.node_data.len()
1401    }
1402
1403    /// Returns an immutable reference to the CSS property cache.
1404    #[inline]
1405    pub fn get_css_property_cache<'a>(&'a self) -> &'a CssPropertyCache {
1406        &*self.css_property_cache.ptr
1407    }
1408
1409    /// Returns a mutable reference to the CSS property cache.
1410    #[inline]
1411    pub fn get_css_property_cache_mut<'a>(&'a mut self) -> &'a mut CssPropertyCache {
1412        &mut *self.css_property_cache.ptr
1413    }
1414
1415    /// Returns the current state (hover, active, focus) of a styled node.
1416    #[inline]
1417    pub fn get_styled_node_state(&self, node_id: &NodeId) -> StyledNodeState {
1418        self.styled_nodes.as_container()[*node_id]
1419            .styled_node_state
1420            .clone()
1421    }
1422
1423    /// Updates hover state for nodes and returns changed CSS properties.
1424    #[must_use]
1425    pub fn restyle_nodes_hover(
1426        &mut self,
1427        nodes: &[NodeId],
1428        new_hover_state: bool,
1429    ) -> RestyleNodes {
1430        self.restyle_nodes_state(
1431            nodes,
1432            new_hover_state,
1433            |state, val| state.hover = val,
1434            azul_css::dynamic_selector::PseudoStateType::Hover,
1435        )
1436    }
1437
1438    /// Updates active state for nodes and returns changed CSS properties.
1439    #[must_use]
1440    pub fn restyle_nodes_active(
1441        &mut self,
1442        nodes: &[NodeId],
1443        new_active_state: bool,
1444    ) -> RestyleNodes {
1445        self.restyle_nodes_state(
1446            nodes,
1447            new_active_state,
1448            |state, val| state.active = val,
1449            azul_css::dynamic_selector::PseudoStateType::Active,
1450        )
1451    }
1452
1453    /// Updates focus state for nodes and returns changed CSS properties.
1454    #[must_use]
1455    pub fn restyle_nodes_focus(
1456        &mut self,
1457        nodes: &[NodeId],
1458        new_focus_state: bool,
1459    ) -> RestyleNodes {
1460        self.restyle_nodes_state(
1461            nodes,
1462            new_focus_state,
1463            |state, val| state.focused = val,
1464            azul_css::dynamic_selector::PseudoStateType::Focus,
1465        )
1466    }
1467
1468    /// Generic restyle method parameterized by the state field and pseudo-state type.
1469    fn restyle_nodes_state(
1470        &mut self,
1471        nodes: &[NodeId],
1472        new_state_value: bool,
1473        set_state: impl Fn(&mut StyledNodeState, bool),
1474        pseudo_state_type: azul_css::dynamic_selector::PseudoStateType,
1475    ) -> RestyleNodes {
1476        // save the old node state
1477        let old_node_states = nodes
1478            .iter()
1479            .map(|nid| {
1480                self.styled_nodes.as_container()[*nid]
1481                    .styled_node_state
1482                    .clone()
1483            })
1484            .collect::<Vec<_>>();
1485
1486        for nid in nodes.iter() {
1487            set_state(
1488                &mut self.styled_nodes.as_container_mut()[*nid].styled_node_state,
1489                new_state_value,
1490            );
1491        }
1492
1493        let css_property_cache = self.get_css_property_cache();
1494        let styled_nodes = self.styled_nodes.as_container();
1495        let node_data = self.node_data.as_container();
1496
1497        // scan all properties that could have changed because of addition / removal
1498        let v = nodes
1499            .iter()
1500            .zip(old_node_states.iter())
1501            .filter_map(|(node_id, old_node_state)| {
1502                let mut keys_normal: Vec<_> = CssPropertyCache::prop_types_for_state(
1503                    css_property_cache.css_props.get_slice(node_id.index()),
1504                    pseudo_state_type,
1505                ).collect();
1506                let mut keys_inherited: Vec<_> = CssPropertyCache::prop_types_for_state(
1507                    css_property_cache.cascaded_props.get_slice(node_id.index()),
1508                    pseudo_state_type,
1509                ).collect();
1510                let keys_inline: Vec<CssPropertyType> = {
1511                    use azul_css::dynamic_selector::DynamicSelector;
1512                    node_data[*node_id]
1513                        .style
1514                        .iter_inline_properties()
1515                        .filter_map(|(prop, conds)| {
1516                            let matches = conds.as_slice().iter().any(|c| {
1517                                matches!(c, DynamicSelector::PseudoState(pst) if *pst == pseudo_state_type)
1518                            });
1519                            if matches {
1520                                Some(prop.get_type())
1521                            } else {
1522                                None
1523                            }
1524                        })
1525                        .collect()
1526                };
1527                let mut keys_inline_ref: Vec<_> = keys_inline.iter().collect();
1528
1529                keys_normal.append(&mut keys_inherited);
1530                keys_normal.append(&mut keys_inline_ref);
1531
1532                let node_properties_that_could_have_changed = keys_normal;
1533
1534                if node_properties_that_could_have_changed.is_empty() {
1535                    return None;
1536                }
1537
1538                let new_node_state = &styled_nodes[*node_id].styled_node_state;
1539                let node_data = &node_data[*node_id];
1540
1541                let changes = node_properties_that_could_have_changed
1542                    .into_iter()
1543                    .filter_map(|prop| {
1544                        // calculate both the old and the new state
1545                        let old = css_property_cache.get_property_slow(
1546                            node_data,
1547                            node_id,
1548                            old_node_state,
1549                            prop,
1550                        );
1551                        let new = css_property_cache.get_property_slow(
1552                            node_data,
1553                            node_id,
1554                            new_node_state,
1555                            prop,
1556                        );
1557                        if old == new {
1558                            None
1559                        } else {
1560                            Some(ChangedCssProperty {
1561                                previous_state: old_node_state.clone(),
1562                                previous_prop: match old {
1563                                    None => CssProperty::auto(*prop),
1564                                    Some(s) => s.clone(),
1565                                },
1566                                current_state: new_node_state.clone(),
1567                                current_prop: match new {
1568                                    None => CssProperty::auto(*prop),
1569                                    Some(s) => s.clone(),
1570                                },
1571                            })
1572                        }
1573                    })
1574                    .collect::<Vec<_>>();
1575
1576                if changes.is_empty() {
1577                    None
1578                } else {
1579                    Some((*node_id, changes))
1580                }
1581            })
1582            .collect::<Vec<_>>();
1583
1584        v.into_iter().collect()
1585    }
1586
1587    /// Unified entry point for all CSS restyle operations.
1588    ///
1589    /// This function synchronizes the StyledNodeState with runtime state
1590    /// and computes which CSS properties have changed. It determines whether
1591    /// layout, display list, or GPU-only updates are needed.
1592    ///
1593    /// # Arguments
1594    /// * `focus_changes` - Nodes gaining/losing focus
1595    /// * `hover_changes` - Nodes gaining/losing hover
1596    /// * `active_changes` - Nodes gaining/losing active (mouse down)
1597    ///
1598    /// # Returns
1599    /// * `RestyleResult` containing changed nodes and what needs updating
1600    #[must_use]
1601    pub fn restyle_on_state_change(
1602        &mut self,
1603        focus_changes: Option<FocusChange>,
1604        hover_changes: Option<HoverChange>,
1605        active_changes: Option<ActiveChange>,
1606    ) -> RestyleResult {
1607        
1608        let mut result = RestyleResult::default();
1609        result.gpu_only_changes = true; // Start with GPU-only assumption
1610
1611        // Helper closure to merge changes and analyze property categories
1612        let mut process_changes = |changes: RestyleNodes| {
1613            for (node_id, props) in changes {
1614                for change in &props {
1615                    let prop_type = change.current_prop.get_type();
1616
1617                    // Use the granular RelayoutScope instead of the binary
1618                    // can_trigger_relayout(). We pass node_is_ifc_member = true
1619                    // conservatively: this means font/text property changes will
1620                    // produce IfcOnly (rather than None). Phase 2c can refine
1621                    // this by checking whether the node actually participates
1622                    // in an IFC.
1623                    let scope = prop_type.relayout_scope(/* node_is_ifc_member */ true);
1624
1625                    // Track the highest scope seen
1626                    if scope > result.max_relayout_scope {
1627                        result.max_relayout_scope = scope;
1628                    }
1629
1630                    // Any scope above None triggers layout
1631                    if scope != RelayoutScope::None {
1632                        result.needs_layout = true;
1633                        result.gpu_only_changes = false;
1634                    }
1635                    
1636                    // Check if this is a GPU-only property
1637                    if !prop_type.is_gpu_only_property() {
1638                        result.gpu_only_changes = false;
1639                    }
1640                    
1641                    // Any visual change needs display list update (unless GPU-only)
1642                    result.needs_display_list = true;
1643                }
1644                
1645                result.changed_nodes.entry(node_id).or_default().extend(props);
1646            }
1647        };
1648
1649        // 1. Process focus changes
1650        if let Some(focus) = focus_changes {
1651            if let Some(old) = focus.lost_focus {
1652                let changes = self.restyle_nodes_focus(&[old], false);
1653                process_changes(changes);
1654            }
1655            if let Some(new) = focus.gained_focus {
1656                let changes = self.restyle_nodes_focus(&[new], true);
1657                process_changes(changes);
1658            }
1659        }
1660
1661        // 2. Process hover changes
1662        if let Some(hover) = hover_changes {
1663            if !hover.left_nodes.is_empty() {
1664                let changes = self.restyle_nodes_hover(&hover.left_nodes, false);
1665                process_changes(changes);
1666            }
1667            if !hover.entered_nodes.is_empty() {
1668                let changes = self.restyle_nodes_hover(&hover.entered_nodes, true);
1669                process_changes(changes);
1670            }
1671        }
1672
1673        // 3. Process active changes
1674        if let Some(active) = active_changes {
1675            if !active.deactivated.is_empty() {
1676                let changes = self.restyle_nodes_active(&active.deactivated, false);
1677                process_changes(changes);
1678            }
1679            if !active.activated.is_empty() {
1680                let changes = self.restyle_nodes_active(&active.activated, true);
1681                process_changes(changes);
1682            }
1683        }
1684
1685        // If no changes, reset display_list flag
1686        if result.changed_nodes.is_empty() {
1687            result.needs_display_list = false;
1688            result.gpu_only_changes = false;
1689        }
1690        
1691        // If layout is needed, display list is also needed
1692        if result.needs_layout {
1693            result.needs_display_list = true;
1694            result.gpu_only_changes = false;
1695        }
1696
1697        result
1698    }
1699
1700    /// Overrides CSS properties for a single node from user code (typically a
1701    /// callback). Writes into `CssPropertyCache::user_overridden_properties`,
1702    /// which `get_property_slow` / `get_property_fast` / `get_computed_value`
1703    /// consult at higher priority than the static CSS cascade — making this
1704    /// the fast path for animating a handful of properties per frame.
1705    ///
1706    /// Passing `CssProperty::Initial` for a property removes any override for
1707    /// that type, restoring the cascaded value. Returns the set of
1708    /// `ChangedCssProperty` entries the caller can feed into the incremental
1709    /// restyle pipeline.
1710    #[must_use]
1711    pub fn restyle_user_property(
1712        &mut self,
1713        node_id: &NodeId,
1714        new_properties: &[CssProperty],
1715    ) -> RestyleNodes {
1716        let mut map = BTreeMap::default();
1717
1718        if new_properties.is_empty() {
1719            return map;
1720        }
1721
1722        let node_count = self.node_data.as_ref().len();
1723        if node_id.index() >= node_count {
1724            return map;
1725        }
1726
1727        let node_data = self.node_data.as_container();
1728        let node_data = &node_data[*node_id];
1729
1730        let node_states = &self.styled_nodes.as_container();
1731        let old_node_state = &node_states[*node_id].styled_node_state;
1732
1733        let changes: Vec<ChangedCssProperty> = {
1734            let css_property_cache = self.get_css_property_cache();
1735
1736            new_properties
1737                .iter()
1738                .filter_map(|new_prop| {
1739                    let old_prop = css_property_cache.get_property_slow(
1740                        node_data,
1741                        node_id,
1742                        old_node_state,
1743                        &new_prop.get_type(),
1744                    );
1745
1746                    let old_prop = match old_prop {
1747                        None => CssProperty::auto(new_prop.get_type()),
1748                        Some(s) => s.clone(),
1749                    };
1750
1751                    if old_prop == *new_prop {
1752                        None
1753                    } else {
1754                        Some(ChangedCssProperty {
1755                            previous_state: old_node_state.clone(),
1756                            previous_prop: old_prop,
1757                            // overriding a user property does not change the state
1758                            current_state: old_node_state.clone(),
1759                            current_prop: new_prop.clone(),
1760                        })
1761                    }
1762                })
1763                .collect()
1764        };
1765
1766        let css_property_cache_mut = self.get_css_property_cache_mut();
1767
1768        // user_overridden_properties is built lazily (empty after StyledDom
1769        // construction). Grow to cover this node_id before indexing so the
1770        // override path works on any DOM, not just ones that already have
1771        // overrides from a prior mutation.
1772        if css_property_cache_mut.user_overridden_properties.len() < node_count {
1773            css_property_cache_mut
1774                .user_overridden_properties
1775                .resize(node_count, Vec::new());
1776        }
1777
1778        for new_prop in new_properties.iter() {
1779            let prop_type = new_prop.get_type();
1780            let vec = &mut css_property_cache_mut
1781                .user_overridden_properties[node_id.index()];
1782            if new_prop.is_initial() {
1783                // CssProperty::Initial = remove overridden property
1784                if let Ok(idx) = vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
1785                    vec.remove(idx);
1786                }
1787            } else {
1788                match vec.binary_search_by_key(&prop_type, |(k, _)| *k) {
1789                    Ok(idx) => vec[idx].1 = new_prop.clone(),
1790                    Err(idx) => vec.insert(idx, (prop_type, new_prop.clone())),
1791                }
1792            }
1793        }
1794
1795        if !changes.is_empty() {
1796            map.insert(*node_id, changes);
1797        }
1798
1799        map
1800    }
1801
1802    /// Returns a HTML-formatted version of the DOM for easier debugging.
1803    ///
1804    /// For example, a DOM with a parent div containing a child div would return:
1805    ///
1806    /// ```xml,no_run,ignore
1807    /// <div id="hello">
1808    ///      <div id="test" />
1809    /// </div>
1810    /// ```
1811    pub fn get_html_string(&self, custom_head: &str, custom_body: &str, test_mode: bool) -> String {
1812        let css_property_cache = self.get_css_property_cache();
1813
1814        let mut output = String::new();
1815
1816        // After which nodes should a close tag be printed?
1817        let mut should_print_close_tag_after_node: BTreeMap<NodeId, Vec<(NodeId, usize)>> = BTreeMap::new();
1818
1819        let should_print_close_tag_debug = self
1820            .non_leaf_nodes
1821            .iter()
1822            .filter_map(|p| {
1823                let parent_node_id = p.node_id.into_crate_internal()?;
1824                let mut total_last_child = None;
1825                recursive_get_last_child(
1826                    parent_node_id,
1827                    &self.node_hierarchy.as_ref(),
1828                    &mut total_last_child,
1829                );
1830                let total_last_child = total_last_child?;
1831                Some((parent_node_id, (total_last_child, p.depth)))
1832            })
1833            .collect::<BTreeMap<_, _>>();
1834
1835        for (parent_id, (last_child, parent_depth)) in should_print_close_tag_debug {
1836            should_print_close_tag_after_node
1837                .entry(last_child)
1838                .or_default()
1839                .push((parent_id, parent_depth));
1840        }
1841
1842        let mut all_node_depths = self
1843            .non_leaf_nodes
1844            .iter()
1845            .filter_map(|p| {
1846                let parent_node_id = p.node_id.into_crate_internal()?;
1847                Some((parent_node_id, p.depth))
1848            })
1849            .collect::<BTreeMap<_, _>>();
1850
1851        for (parent_node_id, parent_depth) in self
1852            .non_leaf_nodes
1853            .iter()
1854            .filter_map(|p| Some((p.node_id.into_crate_internal()?, p.depth)))
1855        {
1856            for child_id in parent_node_id.az_children(&self.node_hierarchy.as_container()) {
1857                all_node_depths.insert(child_id, parent_depth + 1);
1858            }
1859        }
1860
1861        for node_id in self.node_hierarchy.as_container().linear_iter() {
1862            let depth = all_node_depths[&node_id];
1863
1864            let node_data = &self.node_data.as_container()[node_id];
1865            let node_state = &self.styled_nodes.as_container()[node_id].styled_node_state;
1866            let tabs = String::from("    ").repeat(depth);
1867
1868            output.push_str("\r\n");
1869            output.push_str(&tabs);
1870            output.push_str(&node_data.debug_print_start(css_property_cache, &node_id, node_state));
1871
1872            if let Some(content) = node_data.get_node_type().format().as_ref() {
1873                output.push_str(content);
1874            }
1875
1876            let node_has_children = self.node_hierarchy.as_container()[node_id]
1877                .first_child_id(node_id)
1878                .is_some();
1879            if !node_has_children {
1880                let node_data = &self.node_data.as_container()[node_id];
1881                output.push_str(&node_data.debug_print_end());
1882            }
1883
1884            if let Some(close_tag_vec) = should_print_close_tag_after_node.get(&node_id) {
1885                let mut close_tag_vec = close_tag_vec.clone();
1886                close_tag_vec.sort_by(|a, b| b.1.cmp(&a.1)); // sort by depth descending
1887                for (close_tag_parent_id, close_tag_depth) in close_tag_vec {
1888                    let node_data = &self.node_data.as_container()[close_tag_parent_id];
1889                    let tabs = String::from("    ").repeat(close_tag_depth);
1890                    output.push_str("\r\n");
1891                    output.push_str(&tabs);
1892                    output.push_str(&node_data.debug_print_end());
1893                }
1894            }
1895        }
1896
1897        if !test_mode {
1898            format!(
1899                "
1900                <html>
1901                    <head>
1902                    <style>* {{ margin:0px; padding:0px; }}</style>
1903                    {custom_head}
1904                    </head>
1905                {output}
1906                {custom_body}
1907                </html>
1908            "
1909            )
1910        } else {
1911            output
1912        }
1913    }
1914
1915    /// Returns nodes grouped by their rendering order (respects z-index and position).
1916    pub fn get_rects_in_rendering_order(&self) -> ContentGroup {
1917        Self::determine_rendering_order(
1918            &self.non_leaf_nodes.as_ref(),
1919            &self.node_hierarchy.as_container(),
1920            &self.styled_nodes.as_container(),
1921            &self.node_data.as_container(),
1922            &self.get_css_property_cache(),
1923        )
1924    }
1925
1926    /// Returns the rendering order of the items (the rendering
1927    /// order doesn't have to be the original order)
1928    fn determine_rendering_order<'a>(
1929        non_leaf_nodes: &[ParentWithNodeDepth],
1930        node_hierarchy: &NodeDataContainerRef<'a, NodeHierarchyItem>,
1931        styled_nodes: &NodeDataContainerRef<StyledNode>,
1932        node_data_container: &NodeDataContainerRef<NodeData>,
1933        css_property_cache: &CssPropertyCache,
1934    ) -> ContentGroup {
1935        let children_sorted = non_leaf_nodes
1936            .iter()
1937            .filter_map(|parent| {
1938                Some((
1939                    parent.node_id,
1940                    sort_children_by_position(
1941                        parent.node_id.into_crate_internal()?,
1942                        node_hierarchy,
1943                        styled_nodes,
1944                        node_data_container,
1945                        css_property_cache,
1946                    ),
1947                ))
1948            })
1949            .collect::<Vec<_>>();
1950
1951        let children_sorted: BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>> =
1952            children_sorted.into_iter().collect();
1953
1954        let mut root_content_group = ContentGroup {
1955            root: NodeHierarchyItemId::from_crate_internal(Some(NodeId::ZERO)),
1956            children: Vec::new().into(),
1957        };
1958
1959        fill_content_group_children(&mut root_content_group, &children_sorted);
1960
1961        root_content_group
1962    }
1963
1964    /// Replaces this StyledDom with default and returns the old value.
1965    pub fn swap_with_default(&mut self) -> Self {
1966        let mut new = Self::default();
1967        core::mem::swap(self, &mut new);
1968        new
1969    }
1970
1971}
1972
1973/// Same as `Dom`, but arena-based for more efficient memory layout and faster traversal.
1974#[derive(Debug, PartialEq, PartialOrd, Eq)]
1975pub struct CompactDom {
1976    /// The arena containing the hierarchical relationships (parent, child, sibling) of all nodes.
1977    pub node_hierarchy: NodeHierarchy,
1978    /// The arena containing the actual data (`NodeData`) for each node.
1979    pub node_data: NodeDataContainer<NodeData>,
1980    /// The ID of the root node of the DOM tree.
1981    pub root: NodeId,
1982}
1983
1984impl CompactDom {
1985    /// Returns the number of nodes in this DOM.
1986    #[inline(always)]
1987    pub fn len(&self) -> usize {
1988        self.node_hierarchy.as_ref().len()
1989    }
1990}
1991
1992impl From<Dom> for CompactDom {
1993    fn from(dom: Dom) -> Self {
1994        convert_dom_into_compact_dom(dom)
1995    }
1996}
1997
1998/// Converts a tree-based Dom into an arena-based CompactDom for efficient traversal.
1999pub fn convert_dom_into_compact_dom(mut dom: Dom) -> CompactDom {
2000    // note: somehow convert this into a non-recursive form later on!
2001    fn convert_dom_into_compact_dom_internal(
2002        dom: &mut Dom,
2003        node_hierarchy: &mut [Node],
2004        node_data: &mut Vec<NodeData>,
2005        parent_node_id: NodeId,
2006        node: Node,
2007        cur_node_id: &mut usize,
2008    ) {
2009        // - parent [0]
2010        //    - child [1]
2011        //    - child [2]
2012        //        - child of child 2 [2]
2013        //        - child of child 2 [4]
2014        //    - child [5]
2015        //    - child [6]
2016        //        - child of child 4 [7]
2017
2018        // Write node into the arena here!
2019        node_hierarchy[parent_node_id.index()] = node.clone();
2020
2021        let copy = dom.root.copy_special();
2022
2023        node_data[parent_node_id.index()] = copy;
2024
2025        *cur_node_id += 1;
2026
2027        let mut previous_sibling_id = None;
2028        let children_len = dom.children.len();
2029        for (child_index, child_dom) in dom.children.as_mut().iter_mut().enumerate() {
2030            let child_node_id = NodeId::new(*cur_node_id);
2031            let is_last_child = (child_index + 1) == children_len;
2032            let child_dom_is_empty = child_dom.children.is_empty();
2033            let child_node = Node {
2034                parent: Some(parent_node_id),
2035                previous_sibling: previous_sibling_id,
2036                next_sibling: if is_last_child {
2037                    None
2038                } else {
2039                    Some(child_node_id + child_dom.estimated_total_children + 1)
2040                },
2041                last_child: if child_dom_is_empty {
2042                    None
2043                } else {
2044                    Some(child_node_id + child_dom.estimated_total_children)
2045                },
2046            };
2047            previous_sibling_id = Some(child_node_id);
2048            // recurse BEFORE adding the next child
2049            convert_dom_into_compact_dom_internal(
2050                child_dom,
2051                node_hierarchy,
2052                node_data,
2053                child_node_id,
2054                child_node,
2055                cur_node_id,
2056            );
2057        }
2058    }
2059
2060    // Pre-allocate all nodes (+ 1 root node)
2061    let sum_nodes = dom.fixup_children_estimated();
2062
2063    let mut node_hierarchy = vec![Node::ROOT; sum_nodes + 1];
2064    let mut node_data = vec![NodeData::create_div(); sum_nodes + 1];
2065    let mut cur_node_id = 0;
2066
2067    let root_node_id = NodeId::ZERO;
2068    let root_node = Node {
2069        parent: None,
2070        previous_sibling: None,
2071        next_sibling: None,
2072        last_child: if dom.children.is_empty() {
2073            None
2074        } else {
2075            Some(root_node_id + dom.estimated_total_children)
2076        },
2077    };
2078
2079    convert_dom_into_compact_dom_internal(
2080        &mut dom,
2081        &mut node_hierarchy,
2082        &mut node_data,
2083        root_node_id,
2084        root_node,
2085        &mut cur_node_id,
2086    );
2087
2088    CompactDom {
2089        node_hierarchy: NodeHierarchy {
2090            internal: node_hierarchy,
2091        },
2092        node_data: NodeDataContainer {
2093            internal: node_data,
2094        },
2095        root: root_node_id,
2096    }
2097}
2098
2099/// Recursively collect all CSS objects from a Dom tree (depth-first).
2100/// Inner (deeper) CSS objects come first, outer (shallower) CSS objects come last.
2101/// This means outer CSS has higher cascade priority when applied in order.
2102fn collect_css_from_dom(dom: &Dom, out: &mut Vec<azul_css::css::Css>) {
2103    // First, recurse into children (inner CSS = lower priority)
2104    for child in dom.children.iter() {
2105        collect_css_from_dom(child, out);
2106    }
2107    // Then, add this node's CSS objects (outer CSS = higher priority)
2108    for css in dom.css.iter() {
2109        out.push(css.clone());
2110    }
2111}
2112
2113/// Recursively strip CSS from all Dom nodes (sets css to empty vec).
2114/// Called after collecting CSS so the CompactDom doesn't carry CSS data.
2115fn strip_css_from_dom(dom: &mut Dom) {
2116    dom.css = Vec::new().into();
2117    for child in dom.children.as_mut().iter_mut() {
2118        strip_css_from_dom(child);
2119    }
2120}
2121
2122fn fill_content_group_children(
2123    group: &mut ContentGroup,
2124    children_sorted: &BTreeMap<NodeHierarchyItemId, Vec<NodeHierarchyItemId>>,
2125) {
2126    if let Some(c) = children_sorted.get(&group.root) {
2127        // returns None for leaf nodes
2128        group.children = c
2129            .iter()
2130            .map(|child| ContentGroup {
2131                root: *child,
2132                children: Vec::new().into(),
2133            })
2134            .collect::<Vec<ContentGroup>>()
2135            .into();
2136
2137        for c in group.children.as_mut() {
2138            fill_content_group_children(c, children_sorted);
2139        }
2140    }
2141}
2142
2143fn sort_children_by_position<'a>(
2144    parent: NodeId,
2145    node_hierarchy: &NodeDataContainerRef<'a, NodeHierarchyItem>,
2146    rectangles: &NodeDataContainerRef<StyledNode>,
2147    node_data_container: &NodeDataContainerRef<NodeData>,
2148    css_property_cache: &CssPropertyCache,
2149) -> Vec<NodeHierarchyItemId> {
2150    use azul_css::props::layout::LayoutPosition::*;
2151
2152    let children_positions = parent
2153        .az_children(node_hierarchy)
2154        .map(|nid| {
2155            let position = css_property_cache
2156                .get_position(
2157                    &node_data_container[nid],
2158                    &nid,
2159                    &rectangles[nid].styled_node_state,
2160                )
2161                .and_then(|p| p.clone().get_property_or_default())
2162                .unwrap_or_default();
2163            let id = NodeHierarchyItemId::from_crate_internal(Some(nid));
2164            (id, position)
2165        })
2166        .collect::<Vec<_>>();
2167
2168    let mut not_absolute_children = children_positions
2169        .iter()
2170        .filter_map(|(node_id, position)| {
2171            if *position != Absolute {
2172                Some(*node_id)
2173            } else {
2174                None
2175            }
2176        })
2177        .collect::<Vec<_>>();
2178
2179    let mut absolute_children = children_positions
2180        .iter()
2181        .filter_map(|(node_id, position)| {
2182            if *position == Absolute {
2183                Some(*node_id)
2184            } else {
2185                None
2186            }
2187        })
2188        .collect::<Vec<_>>();
2189
2190    // Append the position:absolute children after the regular children
2191    not_absolute_children.append(&mut absolute_children);
2192    not_absolute_children
2193}
2194
2195// calls get_last_child() recursively until the last child of the last child of the ... has been
2196// found
2197fn recursive_get_last_child(
2198    node_id: NodeId,
2199    node_hierarchy: &[NodeHierarchyItem],
2200    target: &mut Option<NodeId>,
2201) {
2202    match node_hierarchy[node_id.index()].last_child_id() {
2203        None => return,
2204        Some(s) => {
2205            *target = Some(s);
2206            recursive_get_last_child(s, node_hierarchy, target);
2207        }
2208    }
2209}
2210
2211// ============================================================================
2212// DOM TRAVERSAL FOR MULTI-NODE SELECTION
2213// ============================================================================
2214
2215/// Determine if node_a comes before node_b in document order.
2216///
2217/// Document order is defined as pre-order depth-first traversal order.
2218/// This is equivalent to the order nodes appear in HTML source.
2219///
2220/// ## Algorithm
2221/// 1. Find the path from root to each node
2222/// 2. Find the Lowest Common Ancestor (LCA)
2223/// 3. At the divergence point, the child that appears first in sibling order comes first
2224pub fn is_before_in_document_order(
2225    hierarchy: &NodeHierarchyItemVec,
2226    node_a: NodeId,
2227    node_b: NodeId,
2228) -> bool {
2229    if node_a == node_b {
2230        return false;
2231    }
2232    
2233    let hierarchy = hierarchy.as_container();
2234    
2235    // Get paths from root to each node (stored as root-first order)
2236    let path_a = get_path_to_root(&hierarchy, node_a);
2237    let path_b = get_path_to_root(&hierarchy, node_b);
2238    
2239    // Find divergence point (last common ancestor)
2240    let min_len = path_a.len().min(path_b.len());
2241    
2242    for i in 0..min_len {
2243        if path_a[i] != path_b[i] {
2244            // Found divergence - check which sibling comes first
2245            let child_towards_a = path_a[i];
2246            let child_towards_b = path_b[i];
2247            
2248            // A smaller NodeId index means it was created earlier in DOM construction,
2249            // which means it comes first in document order for siblings
2250            return child_towards_a.index() < child_towards_b.index();
2251        }
2252    }
2253    
2254    // One path is a prefix of the other - the shorter path (ancestor) comes first
2255    path_a.len() < path_b.len()
2256}
2257
2258/// Get the path from root to a node, returned in root-first order.
2259fn get_path_to_root(
2260    hierarchy: &NodeDataContainerRef<'_, NodeHierarchyItem>,
2261    node: NodeId,
2262) -> Vec<NodeId> {
2263    let mut path = Vec::new();
2264    let mut current = Some(node);
2265    
2266    while let Some(node_id) = current {
2267        path.push(node_id);
2268        current = hierarchy.get(node_id).and_then(|h| h.parent_id());
2269    }
2270    
2271    // Reverse to get root-first order
2272    path.reverse();
2273    path
2274}
2275
2276/// Collect all nodes between start and end (inclusive) in document order.
2277///
2278/// This performs a pre-order depth-first traversal starting from the root,
2279/// collecting nodes once we've seen `start` and stopping at `end`.
2280///
2281/// ## Parameters
2282/// * `hierarchy` - The node hierarchy
2283/// * `start_node` - First node in document order
2284/// * `end_node` - Last node in document order
2285///
2286/// ## Returns
2287/// Vector of NodeIds in document order, from start to end (inclusive)
2288pub fn collect_nodes_in_document_order(
2289    hierarchy: &NodeHierarchyItemVec,
2290    start_node: NodeId,
2291    end_node: NodeId,
2292) -> Vec<NodeId> {
2293    if start_node == end_node {
2294        return vec![start_node];
2295    }
2296    
2297    let hierarchy_container = hierarchy.as_container();
2298    let hierarchy_slice = hierarchy.as_ref();
2299    
2300    let mut result = Vec::new();
2301    let mut in_range = false;
2302    
2303    // Pre-order DFS using a stack
2304    // We need to traverse in document order, which is pre-order DFS
2305    let mut stack: Vec<NodeId> = vec![NodeId::ZERO]; // Start from root
2306    
2307    while let Some(current) = stack.pop() {
2308        // Check if we've entered the range
2309        if current == start_node {
2310            in_range = true;
2311        }
2312        
2313        // Collect if in range
2314        if in_range {
2315            result.push(current);
2316        }
2317        
2318        // Check if we've exited the range
2319        if current == end_node {
2320            break;
2321        }
2322        
2323        // Push children in reverse order so they pop in correct order
2324        // (first child should be processed first)
2325        if let Some(item) = hierarchy_container.get(current) {
2326            // Get first child
2327            if let Some(first_child) = item.first_child_id(current) {
2328                // Collect all children by following next_sibling
2329                let mut children = Vec::new();
2330                let mut child = Some(first_child);
2331                while let Some(child_id) = child {
2332                    children.push(child_id);
2333                    child = hierarchy_container.get(child_id).and_then(|h| h.next_sibling_id());
2334                }
2335                // Push in reverse order for correct DFS order
2336                for child_id in children.into_iter().rev() {
2337                    stack.push(child_id);
2338                }
2339            }
2340        }
2341    }
2342    
2343    result
2344}
2345
2346/// Check if two `StyledDom`s are structurally equivalent for layout purposes.
2347///
2348/// Returns `true` if the DOMs have the same structure, node types, classes,
2349/// IDs, inline styles, and callback event registrations — meaning the
2350/// layout output would be identical.
2351///
2352/// Image callback nodes are compared by function pointer and `RefAny` type ID
2353/// rather than heap pointer, since each `layout()` call creates new `ImageRef`
2354/// allocations even when the callback is the same.
2355///
2356/// This is used to short-circuit the expensive layout pipeline when the DOM
2357/// hasn't actually changed (e.g., an animation timer fires but only the GL
2358/// texture content changed, not the DOM structure).
2359pub fn is_layout_equivalent(old: &StyledDom, new: &StyledDom) -> bool {
2360    use crate::dom::NodeType;
2361    use crate::resources::DecodedImage;
2362
2363    // Quick check: node count must match
2364    let old_nodes = old.node_data.as_ref();
2365    let new_nodes = new.node_data.as_ref();
2366    if old_nodes.len() != new_nodes.len() {
2367        return false;
2368    }
2369
2370    // Check hierarchy (parent/child/sibling structure)
2371    let old_hier = old.node_hierarchy.as_ref();
2372    let new_hier = new.node_hierarchy.as_ref();
2373    if old_hier.len() != new_hier.len() {
2374        return false;
2375    }
2376    if old_hier != new_hier {
2377        return false;
2378    }
2379
2380    // Per-node comparison
2381    for (old_node, new_node) in old_nodes.iter().zip(new_nodes.iter()) {
2382
2383        // Compare node type discriminant
2384        if core::mem::discriminant(&old_node.node_type)
2385            != core::mem::discriminant(&new_node.node_type)
2386        {
2387            return false;
2388        }
2389
2390        // Compare node type content (with special handling for image callbacks)
2391        match (&old_node.node_type, &new_node.node_type) {
2392            (NodeType::Image(old_img), NodeType::Image(new_img)) => {
2393                match (old_img.get_data(), new_img.get_data()) {
2394                    (DecodedImage::Callback(old_cb), DecodedImage::Callback(new_cb)) => {
2395                        // Compare callback function pointer (stable across frames)
2396                        if old_cb.callback.cb != new_cb.callback.cb {
2397                            return false;
2398                        }
2399                        // Compare RefAny type ID (not instance pointer)
2400                        if old_cb.refany.get_type_id() != new_cb.refany.get_type_id() {
2401                            return false;
2402                        }
2403                    }
2404                    _ => {
2405                        // Raw images / GL textures: compare by pointer identity
2406                        if old_img != new_img {
2407                            return false;
2408                        }
2409                    }
2410                }
2411            }
2412            _ => {
2413                if old_node.node_type != new_node.node_type {
2414                    return false;
2415                }
2416            }
2417        }
2418
2419        // Compare IDs and classes (now stored in attributes as AttributeType::Id/Class)
2420        {
2421            use crate::dom::AttributeType;
2422            let old_ids_classes: Vec<_> = old_node.attributes().as_ref().iter()
2423                .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
2424                .collect();
2425            let new_ids_classes: Vec<_> = new_node.attributes().as_ref().iter()
2426                .filter(|a| matches!(a, AttributeType::Id(_) | AttributeType::Class(_)))
2427                .collect();
2428            if old_ids_classes != new_ids_classes {
2429                return false;
2430            }
2431        }
2432
2433        // Compare inline CSS (direct layout input)
2434        if old_node.style != new_node.style {
2435            return false;
2436        }
2437
2438        // Compare callback event types (affects hit-test tags)
2439        // We compare only event types, not function pointers or data
2440        let old_cbs = old_node.callbacks.as_ref();
2441        let new_cbs = new_node.callbacks.as_ref();
2442        if old_cbs.len() != new_cbs.len() {
2443            return false;
2444        }
2445        for (old_cb, new_cb) in old_cbs.iter().zip(new_cbs.iter()) {
2446            if old_cb.event != new_cb.event {
2447                return false;
2448            }
2449        }
2450
2451        // Compare attributes (some affect layout, e.g. colspan)
2452        if old_node.attributes().as_ref() != new_node.attributes().as_ref() {
2453            return false;
2454        }
2455    }
2456
2457    // Compare styled node states (hover/focus/active flags affect CSS resolution)
2458    let old_styled = old.styled_nodes.as_ref();
2459    let new_styled = new.styled_nodes.as_ref();
2460    if old_styled.len() != new_styled.len() {
2461        return false;
2462    }
2463    if old_styled != new_styled {
2464        return false;
2465    }
2466
2467    true
2468}