Skip to main content

azul_core/
prop_cache.rs

1//! CSS property cache for efficient style resolution and animation.
2//!
3//! This module implements a cache layer between the raw CSS stylesheet and the rendered DOM.
4//! It resolves CSS properties for each node, handling:
5//!
6//! - **Cascade resolution**: Computes final values from CSS rules, inline styles, and inheritance
7//! - **Pseudo-class states**: Caches styles for `:hover`, `:active`, `:focus`, etc.
8//! - **Animation support**: Tracks animating properties for smooth interpolation
9//! - **Performance**: Avoids re-parsing and re-resolving unchanged properties
10//!
11//! # Architecture
12//!
13//! The cache is organized per-node and per-property-type. Each property has a dedicated
14//! getter method that:
15//!
16//! 1. Checks if the property is cached
17//! 2. If not, resolves it from CSS rules + inline styles
18//! 3. Caches the result for subsequent frames
19//!
20//! # Thread Safety
21//!
22//! Not thread-safe. Each window has its own cache instance.
23
24extern crate alloc;
25
26use alloc::{boxed::Box, string::String, vec::Vec};
27use core::fmt::Write;
28use core::mem::ManuallyDrop;
29
30use crate::dom::NodeType;
31
32/// Tracks the origin of a CSS property value.
33/// Used to correctly implement the CSS cascade and inheritance rules.
34#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
35pub enum CssPropertyOrigin {
36    /// Property was inherited from parent node (only for inheritable properties)
37    Inherited,
38    /// Property is the node's own value (from UA CSS, CSS file, inline style, or user override)
39    Own,
40}
41
42/// A CSS property with its origin tracking.
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct CssPropertyWithOrigin {
45    pub property: CssProperty,
46    pub origin: CssPropertyOrigin,
47}
48
49use azul_css::{
50    css::{Css, CssPath},
51    props::{
52        basic::{StyleFontFamily, StyleFontFamilyVec, StyleFontSize},
53        layout::{LayoutDisplay, LayoutHeight, LayoutWidth},
54        property::{
55            BoxDecorationBreakValue, BreakInsideValue, CaretAnimationDurationValue,
56            CaretColorValue, CaretWidthValue, ClipPathValue, ColumnCountValue, ColumnFillValue,
57            ColumnRuleColorValue, ColumnRuleStyleValue, ColumnRuleWidthValue, ColumnSpanValue,
58            ColumnWidthValue, ContentValue, CounterIncrementValue, CounterResetValue, CssProperty,
59            CssPropertyType, FlowFromValue, FlowIntoValue, LayoutAlignContentValue,
60            LayoutAlignItemsValue, LayoutAlignSelfValue, LayoutBorderBottomWidthValue,
61            LayoutBorderLeftWidthValue, LayoutBorderRightWidthValue, LayoutBorderSpacingValue,
62            LayoutBorderTopWidthValue, LayoutBoxSizingValue, LayoutClearValue,
63            LayoutColumnGapValue, LayoutDisplayValue, LayoutFlexBasisValue,
64            LayoutFlexDirectionValue, LayoutFlexGrowValue, LayoutFlexShrinkValue,
65            LayoutFlexWrapValue, LayoutFloatValue, LayoutGapValue, LayoutGridAutoColumnsValue,
66            LayoutGridAutoFlowValue, LayoutGridAutoRowsValue, LayoutGridColumnValue,
67            LayoutGridRowValue, LayoutGridTemplateColumnsValue, LayoutGridTemplateRowsValue,
68            LayoutHeightValue, LayoutInsetBottomValue, LayoutJustifyContentValue,
69            LayoutJustifyItemsValue, LayoutJustifySelfValue, LayoutLeftValue,
70            LayoutMarginBottomValue, LayoutMarginLeftValue, LayoutMarginRightValue,
71            LayoutMarginTopValue, LayoutMaxHeightValue, LayoutMaxWidthValue, LayoutMinHeightValue,
72            LayoutMinWidthValue, LayoutOverflowValue, LayoutPaddingBottomValue,
73            LayoutPaddingLeftValue, LayoutPaddingRightValue, LayoutPaddingTopValue,
74            LayoutPositionValue, LayoutRightValue, LayoutRowGapValue, LayoutScrollbarWidthValue,
75            LayoutTableLayoutValue, LayoutTextJustifyValue, LayoutTopValue, LayoutWidthValue,
76            LayoutWritingModeValue, LayoutZIndexValue, OrphansValue, PageBreakValue,
77            StyleBackgroundContentValue, ScrollbarFadeDelayValue, ScrollbarFadeDurationValue,
78            ScrollbarVisibilityModeValue, SelectionBackgroundColorValue, SelectionColorValue,
79            SelectionRadiusValue, ShapeImageThresholdValue, ShapeInsideValue, ShapeMarginValue,
80            ShapeOutsideValue, StringSetValue, StyleBackfaceVisibilityValue,
81            StyleBackgroundContentVecValue, StyleBackgroundPositionVecValue,
82            StyleBackgroundRepeatVecValue, StyleBackgroundSizeVecValue,
83            StyleBorderBottomColorValue, StyleBorderBottomLeftRadiusValue,
84            StyleBorderBottomRightRadiusValue, StyleBorderBottomStyleValue,
85            StyleBorderCollapseValue, StyleBorderLeftColorValue, StyleBorderLeftStyleValue,
86            StyleBorderRightColorValue, StyleBorderRightStyleValue, StyleBorderTopColorValue,
87            StyleBorderTopLeftRadiusValue, StyleBorderTopRightRadiusValue,
88            StyleBorderTopStyleValue, StyleBoxShadowValue, StyleCaptionSideValue, StyleCursorValue,
89            StyleDirectionValue, StyleEmptyCellsValue, StyleExclusionMarginValue,
90            StyleFilterVecValue, StyleFontFamilyVecValue, StyleFontSizeValue, StyleFontStyleValue,
91            StyleFontValue, StyleFontWeightValue, StyleHangingPunctuationValue,
92            StyleHyphenationLanguageValue, StyleHyphensValue, StyleInitialLetterValue,
93            StyleLetterSpacingValue, StyleLineBreakValue, StyleLineClampValue, StyleLineHeightValue,
94            StyleListStylePositionValue, StyleListStyleTypeValue, StyleMixBlendModeValue,
95            StyleAspectRatioValue, StyleObjectFitValue, StyleObjectPositionValue, StyleTextOverflowValue,
96            StyleOpacityValue, StylePerspectiveOriginValue,
97            StyleScrollbarColorValue, StyleOverflowWrapValue, StyleTabSizeValue,
98            StyleTextAlignLastValue, StyleTextOrientationValue, StyleTextTransformValue,
99            StyleTextAlignValue, StyleTextColorValue,
100            StyleTextCombineUprightValue, StyleUnicodeBidiValue,
101            StyleTextBoxTrimValue, StyleTextBoxEdgeValue,
102            StyleDominantBaselineValue, StyleAlignmentBaselineValue, StyleBaselineSourceValue,
103            StyleLineFitEdgeValue,
104            StyleInitialLetterAlignValue, StyleInitialLetterWrapValue,
105            StyleScrollbarGutterValue, StyleOverflowClipMarginValue, StyleClipRectValue,
106            StyleTextDecorationValue, StyleTextIndentValue,
107            StyleTransformOriginValue, StyleTransformVecValue, StyleUserSelectValue,
108            StyleVerticalAlignValue, StyleVisibilityValue, StyleWhiteSpaceValue,
109            StyleWordBreakValue, StyleWordSpacingValue, WidowsValue,
110        },
111        style::{StyleCursor, StyleTextColor, StyleTransformOrigin},
112    },
113    AzString,
114};
115
116use crate::{
117    dom::{NodeData, NodeId, TabIndex, TagId},
118    id::{NodeDataContainer, NodeDataContainerRef},
119    style::CascadeInfo,
120    styled_dom::{
121        NodeHierarchyItem, NodeHierarchyItemId, NodeHierarchyItemVec, ParentWithNodeDepth,
122        ParentWithNodeDepthVec, StyledNodeState, TagIdToNodeIdMapping,
123    },
124};
125
126use azul_css::dynamic_selector::{
127    CssPropertyWithConditions, CssPropertyWithConditionsVec, DynamicSelectorContext,
128};
129
130#[cfg(feature = "std")]
131std::thread_local! {
132    static PROP_COUNTS: core::cell::RefCell<
133        std::collections::HashMap<&'static str, usize>
134    > = core::cell::RefCell::new(std::collections::HashMap::new());
135}
136
137/// Drain the per-thread CSS cascade-walk counter populated by
138/// [`CssPropertyCache::get_property`] when `AZ_PROP_COUNT=1` is set
139/// in the environment.
140///
141/// Returns `(property_label, count)` pairs
142/// sorted by count descending. Layout-side instrumentation calls
143/// this after each `layout_document` to print which properties
144/// drove the most cascade walks.
145#[cfg(feature = "std")]
146#[must_use] pub fn drain_css_prop_counts() -> Vec<(&'static str, usize)> {
147    // try_with: no real TLS in the lifted-to-wasm web backend (see the
148    // get_property recording site) — return empty rather than panic.
149    PROP_COUNTS
150        .try_with(|c| {
151            let map = core::mem::take(&mut *c.borrow_mut());
152            let mut v: Vec<_> = map.into_iter().collect();
153            v.sort_by(|a, b| b.1.cmp(&a.1));
154            v
155        })
156        .unwrap_or_default()
157}
158
159// Unit conversion constants (CSS absolute units → pixels)
160const PT_TO_PX: f32 = 1.333_333;
161const IN_TO_PX: f32 = 96.0;
162const CM_TO_PX: f32 = 37.795_277;
163const MM_TO_PX: f32 = 3.779_527_7;
164
165/// Match on any `CssProperty` variant and access the inner `CssPropertyValue`<T>.
166#[allow(unused_macros)]
167macro_rules! match_property_value {
168    ($property:expr, $value:ident, $expr:expr) => {
169        match $property {
170            CssProperty::CaretColor($value) => $expr,
171            CssProperty::CaretAnimationDuration($value) => $expr,
172            CssProperty::SelectionBackgroundColor($value) => $expr,
173            CssProperty::SelectionColor($value) => $expr,
174            CssProperty::SelectionRadius($value) => $expr,
175            CssProperty::TextColor($value) => $expr,
176            CssProperty::FontSize($value) => $expr,
177            CssProperty::FontFamily($value) => $expr,
178            CssProperty::FontWeight($value) => $expr,
179            CssProperty::FontStyle($value) => $expr,
180            CssProperty::TextAlign($value) => $expr,
181            CssProperty::TextJustify($value) => $expr,
182            CssProperty::VerticalAlign($value) => $expr,
183            CssProperty::LetterSpacing($value) => $expr,
184            CssProperty::TextIndent($value) => $expr,
185            CssProperty::InitialLetter($value) => $expr,
186            CssProperty::LineClamp($value) => $expr,
187            CssProperty::HangingPunctuation($value) => $expr,
188            CssProperty::TextCombineUpright($value) => $expr,
189            CssProperty::UnicodeBidi($value) => $expr,
190            CssProperty::TextBoxTrim($value) => $expr,
191            CssProperty::TextBoxEdge($value) => $expr,
192            CssProperty::DominantBaseline($value) => $expr,
193            CssProperty::AlignmentBaseline($value) => $expr,
194            CssProperty::BaselineSource($value) => $expr,
195            CssProperty::LineFitEdge($value) => $expr,
196            CssProperty::InitialLetterAlign($value) => $expr,
197            CssProperty::InitialLetterWrap($value) => $expr,
198            CssProperty::ScrollbarGutter($value) => $expr,
199            CssProperty::OverflowClipMargin($value) => $expr,
200            CssProperty::Clip($value) => $expr,
201            CssProperty::ExclusionMargin($value) => $expr,
202            CssProperty::HyphenationLanguage($value) => $expr,
203            CssProperty::LineHeight($value) => $expr,
204            CssProperty::WordSpacing($value) => $expr,
205            CssProperty::TabSize($value) => $expr,
206            CssProperty::WhiteSpace($value) => $expr,
207            CssProperty::Hyphens($value) => $expr,
208            CssProperty::Direction($value) => $expr,
209            CssProperty::UserSelect($value) => $expr,
210            CssProperty::TextDecoration($value) => $expr,
211            CssProperty::Cursor($value) => $expr,
212            CssProperty::Display($value) => $expr,
213            CssProperty::Float($value) => $expr,
214            CssProperty::BoxSizing($value) => $expr,
215            CssProperty::Width($value) => $expr,
216            CssProperty::Height($value) => $expr,
217            CssProperty::MinWidth($value) => $expr,
218            CssProperty::MinHeight($value) => $expr,
219            CssProperty::MaxWidth($value) => $expr,
220            CssProperty::MaxHeight($value) => $expr,
221            CssProperty::Position($value) => $expr,
222            CssProperty::Top($value) => $expr,
223            CssProperty::Right($value) => $expr,
224            CssProperty::Left($value) => $expr,
225            CssProperty::Bottom($value) => $expr,
226            CssProperty::ZIndex($value) => $expr,
227            CssProperty::FlexWrap($value) => $expr,
228            CssProperty::FlexDirection($value) => $expr,
229            CssProperty::FlexGrow($value) => $expr,
230            CssProperty::FlexShrink($value) => $expr,
231            CssProperty::FlexBasis($value) => $expr,
232            CssProperty::JustifyContent($value) => $expr,
233            CssProperty::AlignItems($value) => $expr,
234            CssProperty::AlignContent($value) => $expr,
235            CssProperty::AlignSelf($value) => $expr,
236            CssProperty::JustifyItems($value) => $expr,
237            CssProperty::JustifySelf($value) => $expr,
238            CssProperty::BackgroundContent($value) => $expr,
239            CssProperty::BackgroundPosition($value) => $expr,
240            CssProperty::BackgroundSize($value) => $expr,
241            CssProperty::BackgroundRepeat($value) => $expr,
242            CssProperty::OverflowX($value) => $expr,
243            CssProperty::OverflowY($value) => $expr,
244            CssProperty::OverflowBlock($value) => $expr,
245            CssProperty::OverflowInline($value) => $expr,
246            CssProperty::PaddingTop($value) => $expr,
247            CssProperty::PaddingLeft($value) => $expr,
248            CssProperty::PaddingRight($value) => $expr,
249            CssProperty::PaddingBottom($value) => $expr,
250            CssProperty::MarginTop($value) => $expr,
251            CssProperty::MarginLeft($value) => $expr,
252            CssProperty::MarginRight($value) => $expr,
253            CssProperty::MarginBottom($value) => $expr,
254            CssProperty::BorderTopLeftRadius($value) => $expr,
255            CssProperty::BorderTopRightRadius($value) => $expr,
256            CssProperty::BorderBottomLeftRadius($value) => $expr,
257            CssProperty::BorderBottomRightRadius($value) => $expr,
258            CssProperty::BorderTopColor($value) => $expr,
259            CssProperty::BorderRightColor($value) => $expr,
260            CssProperty::BorderLeftColor($value) => $expr,
261            CssProperty::BorderBottomColor($value) => $expr,
262            CssProperty::BorderTopStyle($value) => $expr,
263            CssProperty::BorderRightStyle($value) => $expr,
264            CssProperty::BorderLeftStyle($value) => $expr,
265            CssProperty::BorderBottomStyle($value) => $expr,
266            CssProperty::BorderTopWidth($value) => $expr,
267            CssProperty::BorderRightWidth($value) => $expr,
268            CssProperty::BorderLeftWidth($value) => $expr,
269            CssProperty::BorderBottomWidth($value) => $expr,
270            CssProperty::BoxShadow($value) => $expr,
271            CssProperty::Opacity($value) => $expr,
272            CssProperty::Transform($value) => $expr,
273            CssProperty::TransformOrigin($value) => $expr,
274            CssProperty::PerspectiveOrigin($value) => $expr,
275            CssProperty::BackfaceVisibility($value) => $expr,
276            CssProperty::MixBlendMode($value) => $expr,
277            CssProperty::Filter($value) => $expr,
278            CssProperty::Visibility($value) => $expr,
279            CssProperty::WritingMode($value) => $expr,
280            CssProperty::GridTemplateColumns($value) => $expr,
281            CssProperty::GridTemplateRows($value) => $expr,
282            CssProperty::GridAutoColumns($value) => $expr,
283            CssProperty::GridAutoRows($value) => $expr,
284            CssProperty::GridAutoFlow($value) => $expr,
285            CssProperty::GridColumn($value) => $expr,
286            CssProperty::GridRow($value) => $expr,
287            CssProperty::GridTemplateAreas($value) => $expr,
288            CssProperty::Gap($value) => $expr,
289            CssProperty::ColumnGap($value) => $expr,
290            CssProperty::RowGap($value) => $expr,
291            CssProperty::Clear($value) => $expr,
292            CssProperty::ScrollbarTrack($value) => $expr,
293            CssProperty::ScrollbarThumb($value) => $expr,
294            CssProperty::ScrollbarButton($value) => $expr,
295            CssProperty::ScrollbarCorner($value) => $expr,
296            CssProperty::ScrollbarResizer($value) => $expr,
297            CssProperty::ScrollbarWidth($value) => $expr,
298            CssProperty::ScrollbarColor($value) => $expr,
299            CssProperty::ListStyleType($value) => $expr,
300            CssProperty::ListStylePosition($value) => $expr,
301            CssProperty::Font($value) => $expr,
302            CssProperty::ColumnCount($value) => $expr,
303            CssProperty::ColumnWidth($value) => $expr,
304            CssProperty::ColumnSpan($value) => $expr,
305            CssProperty::ColumnFill($value) => $expr,
306            CssProperty::ColumnRuleStyle($value) => $expr,
307            CssProperty::ColumnRuleWidth($value) => $expr,
308            CssProperty::ColumnRuleColor($value) => $expr,
309            CssProperty::FlowInto($value) => $expr,
310            CssProperty::FlowFrom($value) => $expr,
311            CssProperty::ShapeOutside($value) => $expr,
312            CssProperty::ShapeInside($value) => $expr,
313            CssProperty::ShapeImageThreshold($value) => $expr,
314            CssProperty::ShapeMargin($value) => $expr,
315            CssProperty::ClipPath($value) => $expr,
316            CssProperty::Content($value) => $expr,
317            CssProperty::CounterIncrement($value) => $expr,
318            CssProperty::CounterReset($value) => $expr,
319            CssProperty::StringSet($value) => $expr,
320            CssProperty::Orphans($value) => $expr,
321            CssProperty::Widows($value) => $expr,
322            CssProperty::PageBreakBefore($value) => $expr,
323            CssProperty::PageBreakAfter($value) => $expr,
324            CssProperty::PageBreakInside($value) => $expr,
325            CssProperty::BreakInside($value) => $expr,
326            CssProperty::BoxDecorationBreak($value) => $expr,
327            CssProperty::TableLayout($value) => $expr,
328            CssProperty::BorderCollapse($value) => $expr,
329            CssProperty::BorderSpacing($value) => $expr,
330            CssProperty::CaptionSide($value) => $expr,
331            CssProperty::EmptyCells($value) => $expr,
332        }
333    };
334}
335
336/// A CSS property tagged with its pseudo-state and property type.
337///
338/// Replaces the per-pseudo-state `BTreeMap` approach: instead of 6 `BTreeMaps`
339/// per node (Normal/Hover/Active/Focus/Dragging/DragOver), we store one Vec
340/// per node and tag each property with its state. Lookups use `.iter().find()`.
341#[derive(Debug, Clone, PartialEq, Eq)]
342pub struct StatefulCssProperty {
343    pub state: azul_css::dynamic_selector::PseudoStateType,
344    pub prop_type: CssPropertyType,
345    pub property: CssProperty,
346}
347
348// =============================================================================
349// FlatVecVec: Cache-friendly replacement for Vec<Vec<T>>
350// =============================================================================
351
352/// A flat, cache-friendly replacement for `Vec<Vec<T>>`.
353///
354/// During the **build phase**, items are pushed into per-node inner Vecs
355/// (same as before). After building is complete, `flatten()` compacts all
356/// inner Vecs into a single contiguous `Vec<T>` with a `(start, len)` offset
357/// table per node. All subsequent reads use the flat layout, eliminating
358/// N heap allocations and pointer chasing.
359///
360/// ## Lifecycle
361///
362/// ```text
363/// new(n) → push_to(idx, item)* → sort_each_and_flatten(key_fn) → get_slice(idx)*
364///          ── build phase ──       ── transition ──                ── read phase ──
365/// ```
366#[derive(Debug, Clone)]
367pub struct FlatVecVec<T> {
368    /// Per-node inner Vecs (used during build phase, empty after flatten).
369    build: Vec<Vec<T>>,
370    /// Flat contiguous storage (populated after flatten).
371    data: Vec<T>,
372    /// `(start, len)` offsets into `data` for each node (populated after flatten).
373    offsets: Vec<(u32, u32)>,
374}
375
376impl<T: PartialEq> PartialEq for FlatVecVec<T> {
377    fn eq(&self, other: &Self) -> bool {
378        let self_in_build = !self.build.is_empty() && self.offsets.is_empty();
379        let other_in_build = !other.build.is_empty() && other.offsets.is_empty();
380        debug_assert!(
381            self_in_build == other_in_build,
382            "FlatVecVec::eq called across phases (one build, one flattened)"
383        );
384        if self_in_build || other_in_build {
385            self.build == other.build
386        } else {
387            self.data == other.data && self.offsets == other.offsets
388        }
389    }
390}
391
392impl<T> Default for FlatVecVec<T> {
393    fn default() -> Self {
394        Self {
395            build: Vec::new(),
396            data: Vec::new(),
397            offsets: Vec::new(),
398        }
399    }
400}
401
402impl<T> FlatVecVec<T> {
403    /// Approximate heap bytes retained. Sums capacity of the
404    /// flattened `data` + `offsets` tables and the per-node build
405    /// Vecs (in case `sort_each_and_flatten` hasn't been called
406    /// yet). `per_element_size` should be `size_of::<T>()`.
407    #[must_use] pub fn heap_bytes(&self, per_element_size: usize) -> usize {
408        let data_bytes = self.data.capacity() * per_element_size;
409        let offsets_bytes =
410            self.offsets.capacity() * size_of::<(u32, u32)>();
411        let mut build_bytes = self.build.capacity() * size_of::<Vec<T>>();
412        for v in &self.build {
413            build_bytes += v.capacity() * per_element_size;
414        }
415        data_bytes + offsets_bytes + build_bytes
416    }
417
418    /// Create a new `FlatVecVec` with `node_count` empty slots (build phase).
419    #[must_use] pub fn new(node_count: usize) -> Self {
420        let mut build = Vec::with_capacity(node_count);
421        for _ in 0..node_count {
422            build.push(Vec::new());
423        }
424        Self {
425            build,
426            data: Vec::new(),
427            offsets: Vec::new(),
428        }
429    }
430
431    /// Push an item to the inner Vec at `node_index` (build phase).
432    ///
433    /// # Panics
434    /// Panics if already flattened or if `node_index >= len()`.
435    #[inline]
436    pub fn push_to(&mut self, node_index: usize, item: T) {
437        self.build[node_index].push(item);
438    }
439
440    /// Get a mutable reference to the inner Vec at `node_index` (build phase).
441    #[inline]
442    pub fn build_mut(&mut self, node_index: usize) -> &mut Vec<T> {
443        &mut self.build[node_index]
444    }
445
446    /// Iterate mutably over all inner Vecs (build phase, e.g. for clearing).
447    #[inline]
448    pub fn build_iter_mut(&mut self) -> core::slice::IterMut<'_, Vec<T>> {
449        self.build.iter_mut()
450    }
451
452    /// Get a reference to the inner Vec at `node_index` during build phase.
453    /// During read phase, returns None (use `get_slice` instead).
454    #[inline]
455    #[must_use] pub fn build_get(&self, node_index: usize) -> Option<&Vec<T>> {
456        self.build.get(node_index)
457    }
458
459    /// Number of node slots.
460    #[inline]
461    #[must_use] pub const fn len(&self) -> usize {
462        if self.offsets.is_empty() {
463            self.build.len()
464        } else {
465            self.offsets.len()
466        }
467    }
468
469    /// Returns `true` if there are no node slots.
470    #[inline]
471    #[must_use] pub const fn is_empty(&self) -> bool {
472        self.len() == 0
473    }
474
475    /// Returns true if this is in read (flattened) mode.
476    #[inline]
477    #[must_use] pub const fn is_flattened(&self) -> bool {
478        !self.offsets.is_empty() || self.build.is_empty()
479    }
480
481    /// Get a slice for the node at `node_index` (read phase).
482    /// Returns empty slice if index is out of bounds or not yet flattened
483    /// (falls back to build-phase data if not yet flattened).
484    #[inline]
485    #[must_use] pub fn get_slice(&self, node_index: usize) -> &[T] {
486        if self.offsets.is_empty() {
487            // Build phase fallback: use inner Vecs
488            self.build.get(node_index).map_or(&[], alloc::vec::Vec::as_slice)
489        } else {
490            // Read phase: use flat data
491            if let Some(&(start, len)) = self.offsets.get(node_index) {
492                let s = start as usize;
493                let l = len as usize;
494                &self.data[s..s + l]
495            } else {
496                &[]
497            }
498        }
499    }
500
501    /// Flatten: sort each inner Vec by key, deduplicate by keeping the last
502    /// occurrence of each key (CSS cascade: later source order wins among
503    /// equal specificity), then compact into flat storage.
504    /// Drains all build-phase Vecs. After this call, only `get_slice()` works.
505    pub fn sort_each_and_flatten<K: Ord + Eq>(&mut self, key_fn: impl Fn(&T) -> K) {
506        let node_count = self.build.len();
507        let total: usize = self.build.iter().map(alloc::vec::Vec::len).sum();
508
509        let mut flat_data = Vec::with_capacity(total);
510        let mut offsets = Vec::with_capacity(node_count);
511
512        for inner in &mut self.build {
513            inner.sort_by_key(|a| key_fn(a));
514
515            // Deduplicate: keep last of each consecutive-key group (CSS cascade).
516            let n = inner.len();
517            let mut keep = vec![false; n];
518            for i in 0..n {
519                if i + 1 >= n || key_fn(&inner[i]) != key_fn(&inner[i + 1]) {
520                    keep[i] = true;
521                }
522            }
523
524            let start = u32::try_from(flat_data.len()).unwrap_or(u32::MAX);
525            // Drain inner and push only kept items
526            for (i, item) in inner.drain(..).enumerate() {
527                if keep[i] {
528                    flat_data.push(item);
529                }
530            }
531
532            let len = u32::try_from(flat_data.len()).unwrap_or(u32::MAX) - start;
533            offsets.push((start, len));
534        }
535
536        flat_data.shrink_to_fit();
537        self.data = flat_data;
538        self.offsets = offsets;
539        self.build = Vec::new();
540    }
541
542    /// Flatten without sorting (for data that's already sorted).
543    pub fn flatten(&mut self) {
544        let node_count = self.build.len();
545        let total: usize = self.build.iter().map(alloc::vec::Vec::len).sum();
546
547        let mut flat_data = Vec::with_capacity(total);
548        let mut offsets = Vec::with_capacity(node_count);
549
550        for inner in &mut self.build {
551            let start = u32::try_from(flat_data.len()).unwrap_or(u32::MAX);
552            let len = u32::try_from(inner.len()).unwrap_or(u32::MAX);
553            offsets.push((start, len));
554            flat_data.append(inner);
555        }
556
557        self.data = flat_data;
558        self.offsets = offsets;
559        self.build = Vec::new();
560    }
561
562    /// Rebuild flat storage, keeping only items matching `predicate`.
563    /// Must be called after flatten. Preserves per-node ordering.
564    pub fn retain(&mut self, predicate: impl Fn(&T) -> bool) where T: Clone {
565        if self.offsets.is_empty() { return; }
566        let node_count = self.offsets.len();
567        let mut new_data = Vec::new();
568        let mut new_offsets = Vec::with_capacity(node_count);
569        for &(start, len) in &self.offsets {
570            let s = start as usize;
571            let l = len as usize;
572            let new_start = u32::try_from(new_data.len()).unwrap_or(u32::MAX);
573            let slice = &self.data[s..s + l];
574            let mut kept = 0u32;
575            for item in slice {
576                if predicate(item) {
577                    new_data.push((*item).clone());
578                    kept += 1;
579                }
580            }
581            new_offsets.push((new_start, kept));
582        }
583        new_data.shrink_to_fit();
584        self.data = new_data;
585        self.offsets = new_offsets;
586    }
587
588    /// Return to build phase from read (flattened) phase, preserving all data, so
589    /// `push_to` / `build_mut` work again. No-op if already in build phase.
590    ///
591    /// The build → flatten → read progression is otherwise one-way, but `restyle()`
592    /// legitimately runs more than once on the same cache (`StyledDom::create` does one
593    /// internal pass, then the public API may do more), and building the compact cache
594    /// flattens these vecs in between. Without re-entering build phase, the next
595    /// restyle's `push_to` / `build_mut` would index an emptied `build` and panic.
596    pub fn ensure_build_phase(&mut self) where T: Clone {
597        if self.offsets.is_empty() {
598            return; // already in build phase (or wholly empty)
599        }
600        let mut build = Vec::with_capacity(self.offsets.len());
601        for &(start, len) in &self.offsets {
602            let s = start as usize;
603            let l = len as usize;
604            build.push(self.data[s..s + l].to_vec());
605        }
606        self.build = build;
607        self.data = Vec::new();
608        self.offsets = Vec::new();
609    }
610
611    /// Like `retain`, but passes each item's owning node index to the predicate.
612    /// Must be called after flatten. Preserves per-node ordering.
613    pub fn retain_with_node_index(
614        &mut self,
615        predicate: impl Fn(usize, &T) -> bool,
616    ) where T: Clone {
617        if self.offsets.is_empty() { return; }
618        let node_count = self.offsets.len();
619        let mut new_data = Vec::new();
620        let mut new_offsets = Vec::with_capacity(node_count);
621        for (node_idx, &(start, len)) in self.offsets.iter().enumerate() {
622            let s = start as usize;
623            let l = len as usize;
624            let new_start = u32::try_from(new_data.len()).unwrap_or(u32::MAX);
625            let slice = &self.data[s..s + l];
626            let mut kept = 0u32;
627            for item in slice {
628                if predicate(node_idx, item) {
629                    new_data.push((*item).clone());
630                    kept += 1;
631                }
632            }
633            new_offsets.push((new_start, kept));
634        }
635        new_data.shrink_to_fit();
636        self.data = new_data;
637        self.offsets = new_offsets;
638    }
639
640    /// Iterate over all nodes, yielding (`node_index`, &[T]) for each.
641    /// Works in both build and flattened phases.
642    pub(crate) const fn iter_node_slices(&self) -> FlatVecVecIter<'_, T> {
643        FlatVecVecIter {
644            fvv: self,
645            idx: 0,
646            count: self.len(),
647        }
648    }
649
650    /// Extend this `FlatVecVec` with all nodes from `other` (append for DOM merge).
651    /// Both must be in build phase, or both must be flattened.
652    pub fn extend_from(&mut self, other: &mut Self) {
653        if !self.offsets.is_empty() && !other.offsets.is_empty() {
654            // Both flattened: extend flat data with offset adjustment
655            let base = u32::try_from(self.data.len()).unwrap_or(u32::MAX);
656            self.data.append(&mut other.data);
657            self.offsets.extend(other.offsets.drain(..).map(|(s, l)| (s + base, l)));
658        } else {
659            // At least one in build phase: extend build vecs
660            self.build.append(&mut other.build);
661            // Invalidate flat data if it existed
662            self.data.clear();
663            self.offsets.clear();
664        }
665    }
666}
667
668/// Iterator over (`node_index`, &[T]) pairs from a `FlatVecVec`.
669pub(crate) struct FlatVecVecIter<'a, T> {
670    fvv: &'a FlatVecVec<T>,
671    idx: usize,
672    count: usize,
673}
674
675impl<'a, T> Iterator for FlatVecVecIter<'a, T> {
676    type Item = (usize, &'a [T]);
677
678    #[inline]
679    fn next(&mut self) -> Option<Self::Item> {
680        if self.idx >= self.count {
681            return None;
682        }
683        let i = self.idx;
684        self.idx += 1;
685        Some((i, self.fvv.get_slice(i)))
686    }
687
688    fn size_hint(&self) -> (usize, Option<usize>) {
689        let rem = self.count - self.idx;
690        (rem, Some(rem))
691    }
692}
693
694impl<T> ExactSizeIterator for FlatVecVecIter<'_, T> {}
695
696// NOTE: To avoid large memory allocations, this is a "cache" that stores all the CSS properties
697// found in the DOM. This cache exists on a per-DOM basis, so it scales independent of how many
698// nodes are in the DOM.
699//
700// If each node would carry its own CSS properties, that would unnecessarily consume memory
701// because most nodes use the default properties or override only one or two properties.
702//
703// The cache can compute the property of any node at any given time, given the current node
704// state (hover, active, focused, normal). This way we don't have to duplicate the CSS properties
705// onto every single node and exchange them when the style changes. Two caches can be appended
706// to each other by simply merging their NodeIds.
707#[derive(Debug, Default, Clone, PartialEq)]
708pub struct CssPropertyCache {
709    // number of nodes in the current DOM
710    pub node_count: usize,
711
712    // The author stylesheet this cache was last cascaded with. Retained so nodes
713    // inserted at runtime can be re-styled (`StyledDom::restyle_retained`) — the
714    // cascade runs once at creation, and without the rules an inserted node could
715    // only ever receive UA defaults + inheritance, never its author CSS.
716    // (This struct lives behind `CssPropertyCachePtr`, so the field is invisible
717    // to the C ABI.)
718    pub retained_author_css: Css,
719
720    // properties that were overridden in callbacks (not specific to any node state)
721    pub user_overridden_properties: Vec<Vec<(CssPropertyType, CssProperty)>>,
722    /// The window's dynamic-selector context (viewport size, theme, OS,
723    /// media type...), provided by the layout funnel before the first
724    /// layout. `None` = context UNKNOWN (a freshly created `StyledDom` that no
725    /// window has adopted yet): non-pseudo-state conditions then evaluate to
726    /// "does not apply", which is the same behaviour they always had before
727    /// contexts were wired through. Pseudo-state conditions never depend on
728    /// this field.
729    pub dynamic_context: Option<Box<DynamicSelectorContext>>,
730
731    // non-default CSS properties that were cascaded from the parent,
732    // unified across all pseudo-states (Normal, Hover, Active, Focus, Dragging, DragOver).
733    // Stored in a flat cache-friendly layout after sort_and_flatten().
734    pub cascaded_props: FlatVecVec<StatefulCssProperty>,
735
736    // non-default CSS properties that were set via a CSS file,
737    // unified across all pseudo-states.
738    pub css_props: FlatVecVec<StatefulCssProperty>,
739
740    // Pre-resolved inherited properties (sorted Vec per node, keyed by CssPropertyType)
741    pub computed_values: Vec<Vec<(CssPropertyType, CssPropertyWithOrigin)>>,
742
743    // Compact layout cache: three-tier numeric encoding for O(1) layout lookups.
744    // Built once after restyle + apply_ua_css + compute_inherited_values.
745    // Non-compact properties (background, shadow, transform) use get_property_slow().
746    pub compact_cache: Option<azul_css::compact_cache::CompactLayoutCache>,
747
748    // Global CSS properties from `*` rules — shared across all nodes.
749    // Applied during build_compact_cache_with_inheritance instead of being
750    // cloned into each node's css_props (saves 50K×N clones).
751    pub global_css_props: Vec<CssProperty>,
752
753    /// Per-node resolved font-size, in pixels, for the `Normal`
754    /// pseudo-state. Populated lazily on first call to
755    /// [`crate::styled_dom::StyledDom::resolved_font_size_px`] via a
756    /// single bottom-up DOM walk; subsequent reads are O(1) Vec
757    /// index by `NodeId::index()`.
758    ///
759    /// Motivation: `get_font_size` is called ~730× per node per
760    /// layout pass (see `AZ_PROP_COUNT=1` report — 329 629
761    /// cascade walks on excel.html alone). Each resolution
762    /// recursively reads the parent's font-size (for `em`) plus
763    /// the root's font-size (for `rem`), multiplying the walk
764    /// count. Caching the pre-resolved pixel value collapses that
765    /// to a single `Vec<f32>` indexed lookup.
766    pub resolved_font_sizes_px: crate::sync::OnceLock<Vec<f32>>,
767}
768
769/// Heap-size breakdown of a `CssPropertyCache`, produced by
770/// [`CssPropertyCache::memory_breakdown`]. All values in bytes.
771///
772/// Primarily a diagnostic — the numbers are capacity-based and
773/// don't chase into property-variant payloads (e.g. the `Vec`
774/// inside a `FontFamily(...)`). Intended for "which subfield is
775/// eating RSS" triage, not for precise accounting.
776#[derive(Debug, Clone, Copy, Default)]
777pub struct CssPropertyCacheBreakdown {
778    pub node_count: usize,
779    pub cascaded_props_bytes: usize,
780    pub css_props_bytes: usize,
781    pub computed_values_bytes: usize,
782    pub user_overridden_bytes: usize,
783    pub global_css_props_bytes: usize,
784    pub compact_cache_bytes: usize,
785    pub resolved_font_sizes_bytes: usize,
786}
787
788impl CssPropertyCacheBreakdown {
789    /// Sum of all subfields.
790    #[must_use] pub const fn total_bytes(&self) -> usize {
791        self.cascaded_props_bytes
792            + self.css_props_bytes
793            + self.computed_values_bytes
794            + self.user_overridden_bytes
795            + self.global_css_props_bytes
796            + self.compact_cache_bytes
797            + self.resolved_font_sizes_bytes
798    }
799}
800
801impl CssPropertyCache {
802    /// Approximate heap bytes retained by this cache, broken out by
803    /// subfield. Used by `StyledDom::memory_breakdown` + the
804    /// `AZ_PROFILE=memory` reporter. Sums capacity × element size
805    /// for each Vec and adds a coarse allowance for the inner Vec
806    /// headers inside `computed_values`.
807    ///
808    /// This is a measurement helper, not a tight bound — it doesn't
809    /// chase into the `CssProperty` enum variants that carry their
810    /// own `Vec`/`String` allocations (notably `FontFamily` →
811    /// `StyleFontFamilyVec` → `Vec<StyleFontFamily>`), so the real
812    /// heap footprint for a property-rich DOM can be 2-3× these
813    /// numbers. Still useful for spotting gross duplication between
814    /// the pre-compact and compact caches.
815    pub fn memory_breakdown(&self) -> CssPropertyCacheBreakdown {
816        let stateful_sz = size_of::<StatefulCssProperty>();
817        let computed_entry_sz =
818            size_of::<(CssPropertyType, CssPropertyWithOrigin)>();
819        let outer_vec_sz = size_of::<Vec<(CssPropertyType, CssPropertyWithOrigin)>>();
820
821        let cascaded_bytes = self.cascaded_props.heap_bytes(stateful_sz);
822        let css_bytes = self.css_props.heap_bytes(stateful_sz);
823
824        let mut computed_bytes = self.computed_values.capacity() * outer_vec_sz;
825        for v in &self.computed_values {
826            computed_bytes += v.capacity() * computed_entry_sz;
827        }
828
829        let user_overridden_bytes = {
830            let mut b = self.user_overridden_properties.capacity() * outer_vec_sz;
831            for v in &self.user_overridden_properties {
832                b += v.capacity()
833                    * size_of::<(CssPropertyType, CssProperty)>();
834            }
835            b
836        };
837
838        let global_bytes = self.global_css_props.capacity()
839            * size_of::<CssProperty>();
840
841        let compact_bytes = self
842            .compact_cache
843            .as_ref()
844            .map_or(0, |c| {
845                c.tier1_enums.capacity() * 8
846                    + c.tier2_dims.capacity() * 68
847                    + c.tier2_cold.capacity() * 28
848                    + c.tier2b_text.capacity() * 24
849                    + c.prev_font_hashes.capacity() * 8
850                    + c.font_dirty_nodes.capacity() * 8
851            });
852
853        let resolved_font_sizes_bytes = self
854            .resolved_font_sizes_px
855            .get()
856            .map_or(0, |v| v.capacity() * size_of::<f32>());
857
858        CssPropertyCacheBreakdown {
859            node_count: self.node_count,
860            cascaded_props_bytes: cascaded_bytes,
861            css_props_bytes: css_bytes,
862            computed_values_bytes: computed_bytes,
863            user_overridden_bytes,
864            global_css_props_bytes: global_bytes,
865            compact_cache_bytes: compact_bytes,
866            resolved_font_sizes_bytes,
867        }
868    }
869
870    /// Drop Normal-state properties that have compact encodings from
871    /// `css_props` and `cascaded_props`. After `build_compact_cache_with_inheritance`,
872    /// these are redundant — the compact cache is the source of truth for layout.
873    /// Non-Normal entries (hover/active/focus) and non-compact properties
874    /// (background, box-shadow, transform, etc.) are kept for `get_property_slow`.
875    pub fn prune_compact_normal_props(&mut self) {
876        use azul_css::dynamic_selector::PseudoStateType;
877
878        #[cfg(feature = "std")]
879        {
880        static PRUNE_DBG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
881        let dbg = *PRUNE_DBG.get_or_init(crate::profile::memory_enabled);
882        if dbg {
883            let mut normal_compact = 0usize;
884            let mut normal_noncompact = 0usize;
885            let mut nonnormal = 0usize;
886            for i in 0..self.css_props.len() {
887                for p in self.css_props.get_slice(i) {
888                    if p.state != PseudoStateType::Normal {
889                        nonnormal += 1;
890                    } else if p.prop_type.has_compact_encoding() {
891                        normal_compact += 1;
892                    } else {
893                        normal_noncompact += 1;
894                    }
895                }
896            }
897            let ssp_sz = size_of::<StatefulCssProperty>();
898            let mut casc_normal_compact = 0usize;
899            let mut casc_total = 0usize;
900            for i in 0..self.cascaded_props.len() {
901                for p in self.cascaded_props.get_slice(i) {
902                    casc_total += 1;
903                    if p.state == PseudoStateType::Normal && p.prop_type.has_compact_encoding() {
904                        casc_normal_compact += 1;
905                    }
906                }
907            }
908            eprintln!("[PRUNE] css_props: norm+compact={normal_compact} norm+other={normal_noncompact} nonnorm={nonnormal} SSP={ssp_sz}B | cascaded: total={casc_total} norm+compact={casc_normal_compact}");
909        }
910        }
911
912        // The compact cache stores SENTINEL for pixel-valued properties whose inner
913        // value is Exact with a non-px metric (vh, vw, %, em, rem, calc(), ...).
914        // Those need the slow `css_props` walk at layout time because the compact
915        // cache has nothing usable. We must keep them here or the slow path falls
916        // back to UA CSS and silently clobbers the author's rule.
917        let keep = |p: &StatefulCssProperty| -> bool {
918            if p.state != PseudoStateType::Normal {
919                return true;
920            }
921            if !p.prop_type.has_compact_encoding() {
922                return true;
923            }
924            // Compact-encoded AND Normal: drop only if the compact cache fully
925            // captured the value (px metric, or Auto/Initial/Inherit/None).
926            if property_needs_slow_path_after_compact(&p.property) {
927                return true;
928            }
929            false
930        };
931        // DO NOT prune css_props: regenerate_layout calls
932        // recompute_inheritance_and_compact_cache() every frame, which REBUILDS the
933        // compact cache from css_props (build_compact_cache_with_inheritance reads
934        // css_props in its per-node Step 3). If we drop compact-encoded Normal props
935        // here, that rebuild reads pruned css_props and resets those props to their
936        // CSS-initial value — e.g. white-space:pre-wrap on a node regressed to Normal
937        // on the 2nd (recompute) build, collapsing \n in pre-wrap text into one line
938        // (#8, intermittently — depends on whether the recompute ran). The doc's
939        // premise ("the compact cache is the source of truth", implying permanence)
940        // is false given that per-frame recompute. cascaded_props is NOT read by the
941        // rebuild (Step 1 inherits from the parent's COMPACT value, not cascaded_props),
942        // so pruning it remains safe. TODO: re-enable css_props pruning once recompute
943        // becomes incremental (preserve directly-set compact values instead of rebuilding).
944        if !self.cascaded_props.is_flattened() {
945            self.cascaded_props.sort_each_and_flatten(|p| (p.state, p.prop_type));
946        }
947        self.cascaded_props.retain(keep);
948    }
949
950    /// Look up a CSS property for a specific pseudo-state in a stateful property vec.
951    /// Requires the vec to be sorted by (state, `prop_type`).
952    #[inline]
953    // prop_cache threads &NodeId/&CssPropertyType uniformly through its hot cascade
954    // lookup API (40+ such params); flipping only clippy's few flags to by-value
955    // would force ref/deref juggling at every boundary with the by-ref majority,
956    // for no measurable hot-path gain — keep the uniform by-ref convention.
957    #[allow(clippy::trivially_copy_pass_by_ref)]
958    fn find_in_stateful<'a>(
959        props: &'a [StatefulCssProperty],
960        state: azul_css::dynamic_selector::PseudoStateType,
961        prop_type: &CssPropertyType,
962    ) -> Option<&'a CssProperty> {
963        let key = (state, *prop_type);
964        props.binary_search_by_key(&key, |p| (p.state, p.prop_type))
965            .ok()
966            .map(|idx| &props[idx].property)
967    }
968
969    /// Check if any properties exist for a specific pseudo-state in a stateful property vec.
970    /// Requires the vec to be sorted by (state, `prop_type`).
971    #[inline]
972    fn has_state_props(
973        props: &[StatefulCssProperty],
974        state: azul_css::dynamic_selector::PseudoStateType,
975    ) -> bool {
976        // All entries with the same state are contiguous. Use partition_point
977        // to find the first entry >= state, then check if it matches.
978        let i = props.partition_point(|p| p.state < state);
979        i < props.len() && props[i].state == state
980    }
981
982    /// Collect all property types for a specific pseudo-state.
983    pub(crate) fn prop_types_for_state(
984        props: &[StatefulCssProperty],
985        state: azul_css::dynamic_selector::PseudoStateType,
986    ) -> impl Iterator<Item = &CssPropertyType> + '_ {
987        props.iter().filter(move |p| p.state == state).map(|p| &p.prop_type)
988    }
989}
990
991/// Returns true if `prop`'s value cannot be fully represented in the compact
992/// cache and therefore needs to survive `prune_compact_normal_props` so the
993/// slow `css_props` walk can still find it at layout time.
994///
995/// Pixel-valued properties (margin, padding, width, height, ...) are the only
996/// case: `Exact(pv)` with `pv.metric != Px` (vh, vw, %, em, rem, ...) encodes
997/// to the compact cache's SENTINEL slot, which loses the value. All other
998/// compact-encoded types (tier1 enums, colors, hashes, etc.) always round-trip
999/// through the compact encoding.
1000fn property_needs_slow_path_after_compact(prop: &CssProperty) -> bool {
1001    use azul_css::css::CssPropertyValue;
1002    use azul_css::props::{
1003        basic::length::SizeMetric,
1004        layout::{
1005            dimensions::{LayoutHeight, LayoutWidth},
1006            flex::LayoutFlexBasis,
1007        },
1008    };
1009
1010    // `inner: PixelValue` wrapper types — check metric directly.
1011    macro_rules! check_plain {
1012        ($v:expr) => {{
1013            if let CssPropertyValue::Exact(ref inner) = $v {
1014                return inner.inner.metric != SizeMetric::Px;
1015            }
1016            false
1017        }};
1018    }
1019
1020    match prop {
1021        // LayoutWidth / LayoutHeight: enum with `Px(PixelValue)` variant.
1022        // Non-pixel variants (Auto / MinContent / MaxContent / FitContent / Calc)
1023        // are already handled by the tier1 fast path or don't exist as i16 dims.
1024        CssProperty::Width(v) => {
1025            if let CssPropertyValue::Exact(LayoutWidth::Px(pv)) = v {
1026                return pv.metric != SizeMetric::Px;
1027            }
1028            false
1029        }
1030        CssProperty::Height(v) => {
1031            if let CssPropertyValue::Exact(LayoutHeight::Px(pv)) = v {
1032                return pv.metric != SizeMetric::Px;
1033            }
1034            false
1035        }
1036
1037        // LayoutFlexBasis: enum with `Exact(PixelValue)` variant.
1038        CssProperty::FlexBasis(v) => {
1039            if let CssPropertyValue::Exact(LayoutFlexBasis::Exact(pv)) = v {
1040                return pv.metric != SizeMetric::Px;
1041            }
1042            false
1043        }
1044
1045        // `inner: PixelValue` wrappers
1046        CssProperty::MinWidth(v) => check_plain!(v),
1047        CssProperty::MaxWidth(v) => check_plain!(v),
1048        CssProperty::MinHeight(v) => check_plain!(v),
1049        CssProperty::MaxHeight(v) => check_plain!(v),
1050        CssProperty::FontSize(v) => check_plain!(v),
1051        CssProperty::PaddingTop(v) => check_plain!(v),
1052        CssProperty::PaddingRight(v) => check_plain!(v),
1053        CssProperty::PaddingBottom(v) => check_plain!(v),
1054        CssProperty::PaddingLeft(v) => check_plain!(v),
1055        CssProperty::MarginTop(v) => check_plain!(v),
1056        CssProperty::MarginRight(v) => check_plain!(v),
1057        CssProperty::MarginBottom(v) => check_plain!(v),
1058        CssProperty::MarginLeft(v) => check_plain!(v),
1059        CssProperty::BorderTopWidth(v) => check_plain!(v),
1060        CssProperty::BorderRightWidth(v) => check_plain!(v),
1061        CssProperty::BorderBottomWidth(v) => check_plain!(v),
1062        CssProperty::BorderLeftWidth(v) => check_plain!(v),
1063        CssProperty::Top(v) => check_plain!(v),
1064        CssProperty::Right(v) => check_plain!(v),
1065        CssProperty::Bottom(v) => check_plain!(v),
1066        CssProperty::Left(v) => check_plain!(v),
1067        CssProperty::ColumnGap(v) => check_plain!(v),
1068        CssProperty::RowGap(v) => check_plain!(v),
1069        CssProperty::LetterSpacing(v) => check_plain!(v),
1070        CssProperty::WordSpacing(v) => check_plain!(v),
1071        CssProperty::TextIndent(v) => check_plain!(v),
1072        CssProperty::TabSize(v) => check_plain!(v),
1073
1074        // All other compact-encoded types round-trip through the compact cache.
1075        _ => false,
1076    }
1077}
1078
1079/// Clone a `CssProperty` WITHOUT going through its derived `Clone`. The derived clone
1080/// is a ~179-arm `match self { V(x) => V(x.clone()) }` that LLVM lowers to an indirect
1081/// HALFWORD jump table (`ldrh`-indexed). The web (remill→wasm) backend mis-lifts that
1082/// table, so for HEAP/Vec-bearing variants (gradients, font-family, shadows, filters,
1083/// transforms) the mis-dispatched clone reads wrong-sized data and the cascade traps
1084/// with "memory access out of bounds" (restyle → inherit → clone). Here every
1085/// heap-bearing variant is dispatched via single-variant `if let` — a direct
1086/// discriminant compare, NO jump table — and each inner `v.clone()` is the value
1087/// type's own clone, which lifts correctly. POD variants fall through to the derived
1088/// clone: correct on native, and harmless on web (a mis-dispatched discriminant 0 is
1089/// `CaretColor`, a `Copy` value with no heap pointer to deref). On native this function
1090/// is byte-for-byte equivalent to `p.clone()`.
1091/// Inheritable properties whose value must be inherited as the parent's already
1092/// *resolved* value, NOT propagated as a raw declaration through `cascaded_props`.
1093///
1094/// `font-size` is the case: a relative parent value (`1.5em`) propagated raw
1095/// would be re-resolved against the already-resolved parent at every descendant
1096/// (multiplicative error: 30px -> 45px -> 67.5px down a chain), and a parent
1097/// whose own `cascaded` font-size is the *grandparent's* absolute value would
1098/// skip the parent's own size entirely. Both consuming paths already inherit
1099/// font-size correctly from the parent's resolved value — `inherit_from_parent`
1100/// for `computed_values`, and the parent's resolved compact slot in
1101/// `build_compact_cache_with_inheritance` — so font-size must not ride the raw
1102/// propagation at all.
1103fn is_resolved_parent_inherited(prop_type: CssPropertyType) -> bool {
1104    prop_type == CssPropertyType::FontSize
1105}
1106
1107fn clone_inheritable_property(
1108    p: &CssProperty,
1109) -> CssProperty {
1110    use azul_css::props::property::CssProperty;
1111    if let CssProperty::FontFamily(v) = p { return CssProperty::FontFamily(v.clone()); }
1112    if let CssProperty::BackgroundContent(v) = p { return CssProperty::BackgroundContent(v.clone()); }
1113    if let CssProperty::BackgroundPosition(v) = p { return CssProperty::BackgroundPosition(v.clone()); }
1114    if let CssProperty::BackgroundSize(v) = p { return CssProperty::BackgroundSize(v.clone()); }
1115    if let CssProperty::BackgroundRepeat(v) = p { return CssProperty::BackgroundRepeat(v.clone()); }
1116    if let CssProperty::BoxShadowLeft(v) = p { return CssProperty::BoxShadowLeft(v.clone()); }
1117    if let CssProperty::BoxShadowRight(v) = p { return CssProperty::BoxShadowRight(v.clone()); }
1118    if let CssProperty::BoxShadowTop(v) = p { return CssProperty::BoxShadowTop(v.clone()); }
1119    if let CssProperty::BoxShadowBottom(v) = p { return CssProperty::BoxShadowBottom(v.clone()); }
1120    if let CssProperty::TextShadow(v) = p { return CssProperty::TextShadow(v.clone()); }
1121    if let CssProperty::ScrollbarTrack(v) = p { return CssProperty::ScrollbarTrack(v.clone()); }
1122    if let CssProperty::ScrollbarThumb(v) = p { return CssProperty::ScrollbarThumb(v.clone()); }
1123    if let CssProperty::ScrollbarButton(v) = p { return CssProperty::ScrollbarButton(v.clone()); }
1124    if let CssProperty::ScrollbarCorner(v) = p { return CssProperty::ScrollbarCorner(v.clone()); }
1125    if let CssProperty::ScrollbarResizer(v) = p { return CssProperty::ScrollbarResizer(v.clone()); }
1126    if let CssProperty::Transform(v) = p { return CssProperty::Transform(v.clone()); }
1127    if let CssProperty::Filter(v) = p { return CssProperty::Filter(v.clone()); }
1128    if let CssProperty::BackdropFilter(v) = p { return CssProperty::BackdropFilter(v.clone()); }
1129    if let CssProperty::Content(v) = p { return CssProperty::Content(v.clone()); }
1130    if let CssProperty::HyphenationLanguage(v) = p { return CssProperty::HyphenationLanguage(v.clone()); }
1131    if let CssProperty::Cursor(v) = p { return CssProperty::Cursor(*v); }
1132    p.clone()
1133}
1134
1135impl CssPropertyCache {
1136    /// Match CSS selectors to nodes and populate `css_props`.
1137    /// Returns tag IDs for hit-testing. If `compact_cache` is available,
1138    /// uses it for fast display/overflow checks; otherwise falls back to slow path.
1139    #[must_use]
1140    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
1141    pub fn restyle(
1142        &mut self,
1143        css: &mut Css,
1144        node_data: &NodeDataContainerRef<'_, NodeData>,
1145        node_hierarchy: &NodeHierarchyItemVec,
1146        non_leaf_nodes: &ParentWithNodeDepthVec,
1147        html_tree: &NodeDataContainerRef<'_, CascadeInfo>,
1148    ) -> Vec<TagIdToNodeIdMapping> {
1149        use azul_css::{
1150            css::{CssDeclaration, CssPathPseudoSelector::{Hover, Active, Focus, Dragging, DragOver}, CssPathSelector, CssRuleBlock},
1151            dynamic_selector::{DynamicSelector, PseudoStateType},
1152            props::layout::LayoutDisplay,
1153        };
1154
1155        let css_is_empty = css.is_empty();
1156
1157        // @-rule conditions (@media width/height, theme, OS...) gate whole
1158        // rule BLOCKS. Evaluated here against the window's dynamic context —
1159        // rules whose conditions do not hold are dropped from this cascade
1160        // exactly as if absent, and `StyledDom::set_dynamic_selector_context`
1161        // re-runs the cascade when the context changes and the author css
1162        // has conditional rules. With NO context yet (a StyledDom no window
1163        // has adopted), conditional rules do not apply — the same behaviour
1164        // inline conditional properties have always had. (Until 2026-08-10
1165        // these conditions were silently IGNORED: an author
1166        // `@media (max-width: 720px)` block applied at every viewport.)
1167        let dyn_ctx = self.dynamic_context.clone();
1168        let rule_applies = |conds: &azul_css::dynamic_selector::DynamicSelectorVec| -> bool {
1169            let cs = conds.as_slice();
1170            cs.is_empty()
1171                || dyn_ctx
1172                    .as_deref()
1173                    .is_some_and(|c| cs.iter().all(|sel| sel.matches(c)))
1174        };
1175
1176        if !css_is_empty {
1177            css.sort_by_specificity();
1178
1179            // Separate CSS rules into "global only" (just `*`) vs "has specific selector".
1180            // Global-only rules apply to ALL nodes — push directly into css_props
1181            // without per-node selector matching (avoids m×n for these rules).
1182            // Specific rules still go through matches_html_element per-node.
1183            let mut global_only_rules: Vec<&CssRuleBlock> = Vec::new();
1184            let mut specific_rules: Vec<&CssRuleBlock> = Vec::new();
1185
1186            for rule in css.rules() {
1187                let selectors = rule.path.selectors.as_ref();
1188                let is_global_only = selectors.len() == 1
1189                    && matches!(selectors.first(), Some(CssPathSelector::Global));
1190                if is_global_only {
1191                    global_only_rules.push(rule);
1192                } else {
1193                    specific_rules.push(rule);
1194                }
1195            }
1196
1197            // Re-enter build phase before repopulating. restyle() is not
1198            // single-shot: StyledDom::create runs one restyle internally, and building
1199            // the compact cache flattens these vecs — so on a later restyle both are in
1200            // read phase, where the old reset `build_iter_mut().clear()` silently
1201            // iterated ZERO entries (flatten empties `build`). The push_to / build_mut
1202            // below then indexed an emptied Vec and panicked.
1203            //
1204            // css_props is rebuilt from scratch each restyle (repopulated below,
1205            // flattened at the end), so replace it with a fresh build-phase vec.
1206            //
1207            // cascaded_props is rebuilt from scratch TOO (2026-08-12): the old
1208            // preserve-and-or_insert approach LEAKED properties of rules whose
1209            // @-condition turned OFF — a color inherited under a min-width
1210            // block survived in every descendant after crossing below it, so
1211            // wide and narrow styling applied SIMULTANEOUSLY (the
1212            // media_restyle_cost law pin caught it). Preservation is
1213            // unnecessary: the inheritance walk is top-down (parents' fresh
1214            // slices are written before children read them — the same
1215            // ordering css_props relies on), so a fresh build-phase vec
1216            // repopulates completely. The historical reason for preserving
1217            // was a phase-bug in the old clear, not a data dependency.
1218            let node_count = self.css_props.len();
1219            self.css_props = FlatVecVec::new(node_count);
1220            self.cascaded_props = FlatVecVec::new(node_count);
1221
1222            // Collect global-only rule declarations ONCE (not per-node).
1223            // These are stored in self.global_css_props and applied during
1224            // build_compact_cache_with_inheritance for each node, avoiding
1225            // 50K × N clones into per-node css_props Vecs.
1226            self.global_css_props.clear();
1227            for rule in &global_only_rules {
1228                if !rule_applies(&rule.conditions) {
1229                    continue;
1230                }
1231                if crate::style::rule_ends_with(&rule.path, None) {
1232                    for d in &rule.declarations {
1233                        if let CssDeclaration::Static(s) = d {
1234                            self.global_css_props.push(s.clone());
1235                        }
1236                    }
1237                }
1238            }
1239
1240            // Phase 2: Match specific rules per-node (only non-global rules)
1241            if !specific_rules.is_empty() {
1242
1243            // Per-node "which declarations match" lists are built as
1244            // `(rule_idx, decl_idx)` pairs — 4 bytes per entry instead of
1245            // cloning a 140-byte `CssProperty`. The clone only happens at
1246            // the final push_to step, so the transient peak is ~35× smaller.
1247            //
1248            // rule_idx indexes into `specific_rules` (Vec<&CssRuleBlock>),
1249            // decl_idx indexes into `rule.declarations.as_slice()`. Both
1250            // fit in u16 since real stylesheets have far fewer than 65k
1251            // rules and declarations per rule.
1252            macro_rules! filter_rules {($expected_pseudo_selector:expr, $node_id:expr) => {{
1253                let mut out: Vec<(u16, u16)> = Vec::new();
1254                for (rule_idx, rule_block) in specific_rules.iter().enumerate() {
1255                    if !rule_applies(&rule_block.conditions) {
1256                        continue;
1257                    }
1258                    if !crate::style::rule_ends_with(&rule_block.path, $expected_pseudo_selector) {
1259                        continue;
1260                    }
1261                    if !crate::style::matches_html_element(
1262                        &rule_block.path,
1263                        $node_id,
1264                        &node_hierarchy.as_container(),
1265                        &node_data,
1266                        &html_tree,
1267                        $expected_pseudo_selector,
1268                    ) {
1269                        continue;
1270                    }
1271                    for (decl_idx, decl) in rule_block.declarations.as_slice().iter().enumerate() {
1272                        if matches!(decl, CssDeclaration::Static(_)) {
1273                            out.push((u16::try_from(rule_idx).unwrap_or(u16::MAX), u16::try_from(decl_idx).unwrap_or(u16::MAX)));
1274                        }
1275                    }
1276                }
1277                out
1278            }};}
1279
1280            // Pre-check which pseudo-states have any matching rules at all.
1281            // This avoids iterating 50K nodes for pseudo-states with zero rules
1282            // (common: most stylesheets have no :hover/:focus/:active rules).
1283            let has_normal = specific_rules.iter().any(|r| crate::style::rule_ends_with(&r.path, None));
1284            let has_hover = specific_rules.iter().any(|r| crate::style::rule_ends_with(&r.path, Some(Hover)));
1285            let has_active = specific_rules.iter().any(|r| crate::style::rule_ends_with(&r.path, Some(Active)));
1286            let has_focus = specific_rules.iter().any(|r| crate::style::rule_ends_with(&r.path, Some(Focus)));
1287            let has_dragging = specific_rules.iter().any(|r| crate::style::rule_ends_with(&r.path, Some(Dragging)));
1288            let has_drag_over = specific_rules.iter().any(|r| crate::style::rule_ends_with(&r.path, Some(DragOver)));
1289
1290            macro_rules! collect_and_assign {
1291                ($pseudo:expr, $state:expr, $has_any:expr) => {
1292                    if $has_any {
1293                        let indices: NodeDataContainer<(NodeId, Vec<(u16, u16)>)> = node_data
1294                            .transform_nodeid_optional(|node_id| {
1295                                let r = filter_rules!($pseudo, node_id);
1296                                if r.is_empty() { None } else { Some((node_id, r)) }
1297                            });
1298                        for (n, pairs) in indices.internal.into_iter() {
1299                            for (rule_idx, decl_idx) in pairs {
1300                                let decl = &specific_rules[rule_idx as usize]
1301                                    .declarations
1302                                    .as_slice()[decl_idx as usize];
1303                                if let CssDeclaration::Static(prop) = decl {
1304                                    self.css_props.push_to(n.index(), StatefulCssProperty {
1305                                        state: $state,
1306                                        prop_type: prop.get_type(),
1307                                        property: prop.clone(),
1308                                    });
1309                                }
1310                            }
1311                        }
1312                    }
1313                };
1314            }
1315
1316            collect_and_assign!(None, PseudoStateType::Normal, has_normal);
1317            collect_and_assign!(Some(Hover), PseudoStateType::Hover, has_hover);
1318            collect_and_assign!(Some(Active), PseudoStateType::Active, has_active);
1319            collect_and_assign!(Some(Focus), PseudoStateType::Focus, has_focus);
1320            collect_and_assign!(Some(Dragging), PseudoStateType::Dragging, has_dragging);
1321            collect_and_assign!(Some(DragOver), PseudoStateType::DragOver, has_drag_over);
1322
1323            } // end if !specific_rules.is_empty()
1324        }
1325
1326        // Inheritance: Inherit all values of the parent to the children, but
1327        // only if the property is inheritable and isn't yet set
1328        for ParentWithNodeDepth { depth: _, node_id } in non_leaf_nodes {
1329            let Some(parent_id) = node_id.into_crate_internal() else {
1330                continue;
1331            };
1332
1333            let all_states = [
1334                PseudoStateType::Normal,
1335                PseudoStateType::Hover,
1336                PseudoStateType::Active,
1337                PseudoStateType::Focus,
1338                PseudoStateType::Dragging,
1339                PseudoStateType::DragOver,
1340            ];
1341
1342            for &state in &all_states {
1343                // 1. Inherit inline CSS properties from parent for this pseudo-state
1344                let parent_inheritable_inline: Vec<(CssPropertyType, CssProperty)> = node_data[parent_id]
1345                    .style
1346                    .iter_inline_properties()
1347                    .filter(|(_prop, conds)| {
1348                        let conditions = conds.as_slice();
1349                        if conditions.is_empty() {
1350                            state == PseudoStateType::Normal
1351                        } else {
1352                            conditions.iter().all(|c| {
1353                                matches!(c, DynamicSelector::PseudoState(s) if *s == state)
1354                            })
1355                        }
1356                    })
1357                    .map(|(prop, _)| prop)
1358                    .filter(|prop| prop.get_type().is_inheritable() && !is_resolved_parent_inherited(prop.get_type()))
1359                    .map(|p| (p.get_type(), clone_inheritable_property(p)))
1360                    .collect();
1361
1362                // 2. Inherit CSS stylesheet properties from parent for this pseudo-state
1363                let parent_inheritable_css: Vec<(CssPropertyType, CssProperty)> = if css_is_empty {
1364                    Vec::new()
1365                } else {
1366                    self.css_props.get_slice(parent_id.index())
1367                        .iter()
1368                        .filter(|p| p.state == state && p.prop_type.is_inheritable() && !is_resolved_parent_inherited(p.prop_type))
1369                        .map(|p| (p.prop_type, clone_inheritable_property(&p.property)))
1370                        .collect()
1371                };
1372
1373                // 3. Inherit cascaded properties from parent for this pseudo-state
1374                let parent_inheritable_cascaded: Vec<(CssPropertyType, CssProperty)> =
1375                    self.cascaded_props.get_slice(parent_id.index())
1376                        .iter()
1377                        .filter(|p| p.state == state && p.prop_type.is_inheritable() && !is_resolved_parent_inherited(p.prop_type))
1378                        .map(|p| (p.prop_type, clone_inheritable_property(&p.property)))
1379                        .collect();
1380
1381                // Combine all inheritable props (inline first = strongest, cascaded last)
1382                // Only insert if child doesn't already have that (state, prop_type) combo
1383                if parent_inheritable_inline.is_empty()
1384                    && parent_inheritable_css.is_empty()
1385                    && parent_inheritable_cascaded.is_empty()
1386                {
1387                    continue;
1388                }
1389
1390                for child_id in parent_id.az_children(&node_hierarchy.as_container()) {
1391                    let child_vec = self.cascaded_props.build_mut(child_id.index());
1392                    for (prop_type, prop_value) in parent_inheritable_inline
1393                        .iter()
1394                        .chain(parent_inheritable_css.iter())
1395                        .chain(parent_inheritable_cascaded.iter())
1396                    {
1397                        // or_insert: only insert if child doesn't already have this (state, prop_type)
1398                        if !child_vec.iter().any(|p| p.state == state && p.prop_type == *prop_type) {
1399                            child_vec.push(StatefulCssProperty {
1400                                state,
1401                                prop_type: *prop_type,
1402                                property: prop_value.clone(),
1403                            });
1404                        }
1405                    }
1406                }
1407            }
1408        }
1409
1410        // Sort css_props by (state, prop_type) for binary search lookups,
1411        // then flatten into contiguous memory for cache-friendly reads.
1412        self.css_props.sort_each_and_flatten(|p| (p.state, p.prop_type));
1413
1414        // Restyling can change font-size properties; the memoized resolved font
1415        // sizes are now stale and must be recomputed on next access.
1416        self.invalidate_resolved_font_sizes();
1417
1418        self.generate_tag_ids(node_data, node_hierarchy)
1419    }
1420
1421    /// Generate hit-test tag IDs for nodes that need event handling.
1422    /// Uses compact cache (if available) for fast display/overflow reads.
1423    /// Can be called separately after `build_compact_cache_with_inheritance`.
1424    pub fn generate_tag_ids(
1425        &self,
1426        node_data: &NodeDataContainerRef<'_, NodeData>,
1427        node_hierarchy: &NodeHierarchyItemVec,
1428    ) -> Vec<TagIdToNodeIdMapping> {
1429
1430        // Tag ID generation: determine which nodes need hit-test tags for
1431        // hover/click/scroll events. Uses compact cache for display/overflow
1432        // checks instead of get_property_slow (which searches 6 data structures).
1433        use azul_css::compact_cache::{
1434            DISPLAY_SHIFT, DISPLAY_MASK,
1435            OVERFLOW_X_SHIFT, OVERFLOW_Y_SHIFT, OVERFLOW_MASK,
1436        };
1437
1438        let compact_cache = self.compact_cache.as_ref();
1439        let node_data_container = &node_data.internal;
1440
1441        let tag_ids = node_data
1442            .internal
1443            .iter()
1444            .enumerate()
1445            .filter_map(|(node_idx, node_data)| {
1446                let node_id = NodeId::new(node_idx);
1447
1448                let should_auto_insert_tabindex = node_data
1449                    .get_callbacks()
1450                    .iter()
1451                    .any(|cb| cb.event.is_focus_callback());
1452
1453                let tab_index = node_data.get_tab_index().map_or(if should_auto_insert_tabindex {
1454                            Some(TabIndex::Auto)
1455                        } else {
1456                            None
1457                        }, Some);
1458
1459                let mut need_tag = false;
1460
1461                // Single-pass guard block: each check `break`s out early once it
1462                // decides `need_tag`. Labeled block (not `loop`) makes the
1463                // never-iterating control flow explicit (clippy::never_loop).
1464                'compute_need_tag: {
1465                    // display:none check — read directly from compact tier1 (fast u64 read)
1466                    if let Some(cc) = compact_cache.as_ref() {
1467                        let t1 = cc.tier1_enums[node_idx];
1468                        let display_val = ((t1 >> DISPLAY_SHIFT) & DISPLAY_MASK) as u8;
1469                        if display_val == 4 { break 'compute_need_tag; } // 4 = LayoutDisplay::None (new encoding)
1470                    }
1471
1472                    if node_data.has_context_menu() || node_data.get_context_menu().is_some() {
1473                        need_tag = true; break 'compute_need_tag;
1474                    }
1475                    if tab_index.is_some() { need_tag = true; break 'compute_need_tag; }
1476
1477                    // Pseudo-state property checks (hover/active/focus/dragging/drag-over)
1478                    {
1479                        use azul_css::dynamic_selector::{DynamicSelector, PseudoStateType};
1480                        let has_pseudo = |state: PseudoStateType| -> bool {
1481                            node_data.style.iter_inline_properties().any(|(_p, conds)| {
1482                                conds.as_slice().iter().any(|c|
1483                                    matches!(c, DynamicSelector::PseudoState(s) if *s == state)
1484                                )
1485                            }) || Self::has_state_props(self.css_props.get_slice(node_idx), state)
1486                        };
1487
1488                        if has_pseudo(PseudoStateType::Hover)
1489                            || has_pseudo(PseudoStateType::Active)
1490                            || has_pseudo(PseudoStateType::Focus)
1491                            || has_pseudo(PseudoStateType::Dragging)
1492                            || has_pseudo(PseudoStateType::DragOver)
1493                        {
1494                            need_tag = true; break 'compute_need_tag;
1495                        }
1496                    }
1497
1498                    // Non-window callbacks
1499                    let has_non_window_cb = !node_data.get_callbacks().is_empty()
1500                        && !node_data.get_callbacks().iter().all(|cb| cb.event.is_window_callback());
1501                    if has_non_window_cb { need_tag = true; break 'compute_need_tag; }
1502
1503                    // Cursor check — read from cached css_props or inline style.
1504                    if self.css_props.get_slice(node_idx).iter().any(|p|
1505                        p.state == azul_css::dynamic_selector::PseudoStateType::Normal
1506                        && p.prop_type == CssPropertyType::Cursor
1507                    ) || node_data.style.iter_inline_properties().any(|(p, _)|
1508                        p.get_type() == CssPropertyType::Cursor
1509                    ) {
1510                        need_tag = true; break 'compute_need_tag;
1511                    }
1512
1513                    // Overflow scroll check — read from compact tier1
1514                    if let Some(cc) = compact_cache.as_ref() {
1515                        let t1 = cc.tier1_enums[node_idx];
1516                        let ox = ((t1 >> OVERFLOW_X_SHIFT) & OVERFLOW_MASK) as u8;
1517                        let oy = ((t1 >> OVERFLOW_Y_SHIFT) & OVERFLOW_MASK) as u8;
1518                        // 2 = Scroll, 3 = Auto in layout_overflow_to_u8 (new encoding)
1519                        if ox == 2 || ox == 3 || oy == 2 || oy == 3 {
1520                            need_tag = true; break 'compute_need_tag;
1521                        }
1522                    }
1523
1524                    // Selectable text check
1525                    {
1526                        use crate::dom::NodeType;
1527                        let hier = node_hierarchy.as_container()[node_id];
1528                        let mut has_text = false;
1529                        if let Some(first_child) = hier.first_child_id(node_id) {
1530                            let mut child_id = Some(first_child);
1531                            while let Some(cid) = child_id {
1532                                if matches!(node_data_container[cid.index()].get_node_type(), NodeType::Text(_)) {
1533                                    has_text = true; break;
1534                                }
1535                                child_id = node_hierarchy.as_container()[cid].next_sibling_id();
1536                            }
1537                        }
1538                        if has_text { need_tag = true; break 'compute_need_tag; }
1539                    }
1540
1541                    break 'compute_need_tag;
1542                }
1543
1544                if need_tag {
1545                    // DETERMINISTIC tag: a pure function of node identity
1546                    // (node index + 1; 0 stays "no tag"), NOT a global
1547                    // counter. Tag values are namespaced by tag TYPE
1548                    // (`TAG_TYPE_DOM_NODE` vs cursor/scrollbar/... — every
1549                    // consumer matches `tag.1`) and resolved per-DOM, so
1550                    // per-node determinism is all that is required.
1551                    //
1552                    // The old `TagId::unique()` counter made tag numbers an
1553                    // ALLOCATION ORDER artifact: rebuilding the SAME UI (any
1554                    // callback returning RefreshDom) produced a fresh tag
1555                    // map, while the structural-identity display-list cache
1556                    // (solver3 Step 1.1 — root subtree hash + viewport)
1557                    // correctly reused the old display list. Map and display
1558                    // list then disagreed about every tag, and each lookup
1559                    // that crosses the two — `get_node_hit_test_bounds`, the
1560                    // WebRender hit-test translation — silently resolved to
1561                    // nothing: after a hash-identical rebuild, clicks aimed
1562                    // by node stopped landing (the E2E `double_click` on the
1563                    // ribbon tab was the visible case). With tags derived
1564                    // from node identity, a structurally identical tree gets
1565                    // identical tags, which is exactly the invariant the
1566                    // display-list cache assumes.
1567                    Some(TagIdToNodeIdMapping {
1568                        tag_id: TagId::from_crate_internal(TagId {
1569                            inner: (node_idx as u64) + 1,
1570                        }),
1571                        node_id: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
1572                        tab_index: tab_index.into(),
1573                    })
1574                } else {
1575                    None
1576                }
1577            })
1578            .collect::<Vec<_>>();
1579
1580        tag_ids
1581    }
1582
1583    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
1584    pub fn get_computed_css_style_string(
1585        &self,
1586        node_data: &NodeData,
1587        node_id: &NodeId,
1588        node_state: &StyledNodeState,
1589    ) -> String {
1590        let mut s = String::new();
1591        if let Some(p) = self.get_background_content(node_data, node_id, node_state) {
1592            let _ = write!(s,"background: {};", p.get_css_value_fmt());
1593        }
1594        if let Some(p) = self.get_background_position(node_data, node_id, node_state) {
1595            let _ = write!(s,"background-position: {};", p.get_css_value_fmt());
1596        }
1597        if let Some(p) = self.get_background_size(node_data, node_id, node_state) {
1598            let _ = write!(s,"background-size: {};", p.get_css_value_fmt());
1599        }
1600        if let Some(p) = self.get_background_repeat(node_data, node_id, node_state) {
1601            let _ = write!(s,"background-repeat: {};", p.get_css_value_fmt());
1602        }
1603        if let Some(p) = self.get_font_size(node_data, node_id, node_state) {
1604            let _ = write!(s,"font-size: {};", p.get_css_value_fmt());
1605        }
1606        if let Some(p) = self.get_font_family(node_data, node_id, node_state) {
1607            let _ = write!(s,"font-family: {};", p.get_css_value_fmt());
1608        }
1609        if let Some(p) = self.get_text_color(node_data, node_id, node_state) {
1610            let _ = write!(s,"color: {};", p.get_css_value_fmt());
1611        }
1612        if let Some(p) = self.get_text_align(node_data, node_id, node_state) {
1613            let _ = write!(s,"text-align: {};", p.get_css_value_fmt());
1614        }
1615        if let Some(p) = self.get_line_height(node_data, node_id, node_state) {
1616            let _ = write!(s,"line-height: {};", p.get_css_value_fmt());
1617        }
1618        if let Some(p) = self.get_letter_spacing(node_data, node_id, node_state) {
1619            let _ = write!(s,"letter-spacing: {};", p.get_css_value_fmt());
1620        }
1621        if let Some(p) = self.get_word_spacing(node_data, node_id, node_state) {
1622            let _ = write!(s,"word-spacing: {};", p.get_css_value_fmt());
1623        }
1624        if let Some(p) = self.get_tab_size(node_data, node_id, node_state) {
1625            let _ = write!(s,"tab-size: {};", p.get_css_value_fmt());
1626        }
1627        if let Some(p) = self.get_cursor(node_data, node_id, node_state) {
1628            let _ = write!(s,"cursor: {};", p.get_css_value_fmt());
1629        }
1630        if let Some(p) = self.get_box_shadow_left(node_data, node_id, node_state) {
1631            let _ = write!(s,
1632                "-azul-box-shadow-left: {};",
1633                p.get_css_value_fmt()
1634            );
1635        }
1636        if let Some(p) = self.get_box_shadow_right(node_data, node_id, node_state) {
1637            let _ = write!(s,
1638                "-azul-box-shadow-right: {};",
1639                p.get_css_value_fmt()
1640            );
1641        }
1642        if let Some(p) = self.get_box_shadow_top(node_data, node_id, node_state) {
1643            let _ = write!(s,"-azul-box-shadow-top: {};", p.get_css_value_fmt());
1644        }
1645        if let Some(p) = self.get_box_shadow_bottom(node_data, node_id, node_state) {
1646            let _ = write!(s,
1647                "-azul-box-shadow-bottom: {};",
1648                p.get_css_value_fmt()
1649            );
1650        }
1651        if let Some(p) = self.get_border_top_color(node_data, node_id, node_state) {
1652            let _ = write!(s,"border-top-color: {};", p.get_css_value_fmt());
1653        }
1654        if let Some(p) = self.get_border_left_color(node_data, node_id, node_state) {
1655            let _ = write!(s,"border-left-color: {};", p.get_css_value_fmt());
1656        }
1657        if let Some(p) = self.get_border_right_color(node_data, node_id, node_state) {
1658            let _ = write!(s,"border-right-color: {};", p.get_css_value_fmt());
1659        }
1660        if let Some(p) = self.get_border_bottom_color(node_data, node_id, node_state) {
1661            let _ = write!(s,"border-bottom-color: {};", p.get_css_value_fmt());
1662        }
1663        if let Some(p) = self.get_border_top_style(node_data, node_id, node_state) {
1664            let _ = write!(s,"border-top-style: {};", p.get_css_value_fmt());
1665        }
1666        if let Some(p) = self.get_border_left_style(node_data, node_id, node_state) {
1667            let _ = write!(s,"border-left-style: {};", p.get_css_value_fmt());
1668        }
1669        if let Some(p) = self.get_border_right_style(node_data, node_id, node_state) {
1670            let _ = write!(s,"border-right-style: {};", p.get_css_value_fmt());
1671        }
1672        if let Some(p) = self.get_border_bottom_style(node_data, node_id, node_state) {
1673            let _ = write!(s,"border-bottom-style: {};", p.get_css_value_fmt());
1674        }
1675        if let Some(p) = self.get_border_top_left_radius(node_data, node_id, node_state) {
1676            let _ = write!(s,
1677                "border-top-left-radius: {};",
1678                p.get_css_value_fmt()
1679            );
1680        }
1681        if let Some(p) = self.get_border_top_right_radius(node_data, node_id, node_state) {
1682            let _ = write!(s,
1683                "border-top-right-radius: {};",
1684                p.get_css_value_fmt()
1685            );
1686        }
1687        if let Some(p) = self.get_border_bottom_left_radius(node_data, node_id, node_state) {
1688            let _ = write!(s,
1689                "border-bottom-left-radius: {};",
1690                p.get_css_value_fmt()
1691            );
1692        }
1693        if let Some(p) = self.get_border_bottom_right_radius(node_data, node_id, node_state) {
1694            let _ = write!(s,
1695                "border-bottom-right-radius: {};",
1696                p.get_css_value_fmt()
1697            );
1698        }
1699        if let Some(p) = self.get_opacity(node_data, node_id, node_state) {
1700            let _ = write!(s,"opacity: {};", p.get_css_value_fmt());
1701        }
1702        if let Some(p) = self.get_transform(node_data, node_id, node_state) {
1703            let _ = write!(s,"transform: {};", p.get_css_value_fmt());
1704        }
1705        if let Some(p) = self.get_transform_origin(node_data, node_id, node_state) {
1706            let _ = write!(s,"transform-origin: {};", p.get_css_value_fmt());
1707        }
1708        if let Some(p) = self.get_perspective_origin(node_data, node_id, node_state) {
1709            let _ = write!(s,"perspective-origin: {};", p.get_css_value_fmt());
1710        }
1711        if let Some(p) = self.get_backface_visibility(node_data, node_id, node_state) {
1712            let _ = write!(s,"backface-visibility: {};", p.get_css_value_fmt());
1713        }
1714        if let Some(p) = self.get_hyphens(node_data, node_id, node_state) {
1715            let _ = write!(s,"hyphens: {};", p.get_css_value_fmt());
1716        }
1717        if let Some(p) = self.get_direction(node_data, node_id, node_state) {
1718            let _ = write!(s,"direction: {};", p.get_css_value_fmt());
1719        }
1720        if let Some(p) = self.get_unicode_bidi(node_data, node_id, node_state) {
1721            let _ = write!(s,"unicode-bidi: {};", p.get_css_value_fmt());
1722        }
1723        if let Some(p) = self.get_text_box_trim(node_data, node_id, node_state) {
1724            let _ = write!(s,"text-box-trim: {};", p.get_css_value_fmt());
1725        }
1726        if let Some(p) = self.get_text_box_edge(node_data, node_id, node_state) {
1727            let _ = write!(s,"text-box-edge: {};", p.get_css_value_fmt());
1728        }
1729        if let Some(p) = self.get_dominant_baseline(node_data, node_id, node_state) {
1730            let _ = write!(s,"dominant-baseline: {};", p.get_css_value_fmt());
1731        }
1732        if let Some(p) = self.get_alignment_baseline(node_data, node_id, node_state) {
1733            let _ = write!(s,"alignment-baseline: {};", p.get_css_value_fmt());
1734        }
1735        if let Some(p) = self.get_baseline_source(node_data, node_id, node_state) {
1736            let _ = write!(s,"baseline-source: {};", p.get_css_value_fmt());
1737        }
1738        if let Some(p) = self.get_line_fit_edge(node_data, node_id, node_state) {
1739            let _ = write!(s,"line-fit-edge: {};", p.get_css_value_fmt());
1740        }
1741        if let Some(p) = self.get_initial_letter_align(node_data, node_id, node_state) {
1742            let _ = write!(s,"initial-letter-align: {};", p.get_css_value_fmt());
1743        }
1744        if let Some(p) = self.get_initial_letter_wrap(node_data, node_id, node_state) {
1745            let _ = write!(s,"initial-letter-wrap: {};", p.get_css_value_fmt());
1746        }
1747        if let Some(p) = self.get_scrollbar_gutter(node_data, node_id, node_state) {
1748            let _ = write!(s,"scrollbar-gutter: {};", p.get_css_value_fmt());
1749        }
1750        if let Some(p) = self.get_overflow_clip_margin(node_data, node_id, node_state) {
1751            let _ = write!(s,"overflow-clip-margin: {};", p.get_css_value_fmt());
1752        }
1753        if let Some(p) = self.get_clip(node_data, node_id, node_state) {
1754            let _ = write!(s,"clip: {};", p.get_css_value_fmt());
1755        }
1756        if let Some(p) = self.get_white_space(node_data, node_id, node_state) {
1757            let _ = write!(s,"white-space: {};", p.get_css_value_fmt());
1758        }
1759        if let Some(p) = self.get_display(node_data, node_id, node_state) {
1760            let _ = write!(s,"display: {};", p.get_css_value_fmt());
1761        }
1762        if let Some(p) = self.get_float(node_data, node_id, node_state) {
1763            let _ = write!(s,"float: {};", p.get_css_value_fmt());
1764        }
1765        if let Some(p) = self.get_box_sizing(node_data, node_id, node_state) {
1766            let _ = write!(s,"box-sizing: {};", p.get_css_value_fmt());
1767        }
1768        if let Some(p) = self.get_width(node_data, node_id, node_state) {
1769            let _ = write!(s,"width: {};", p.get_css_value_fmt());
1770        }
1771        if let Some(p) = self.get_height(node_data, node_id, node_state) {
1772            let _ = write!(s,"height: {};", p.get_css_value_fmt());
1773        }
1774        if let Some(p) = self.get_min_width(node_data, node_id, node_state) {
1775            let _ = write!(s,"min-width: {};", p.get_css_value_fmt());
1776        }
1777        if let Some(p) = self.get_min_height(node_data, node_id, node_state) {
1778            let _ = write!(s,"min-height: {};", p.get_css_value_fmt());
1779        }
1780        if let Some(p) = self.get_max_width(node_data, node_id, node_state) {
1781            let _ = write!(s,"max-width: {};", p.get_css_value_fmt());
1782        }
1783        if let Some(p) = self.get_max_height(node_data, node_id, node_state) {
1784            let _ = write!(s,"max-height: {};", p.get_css_value_fmt());
1785        }
1786        if let Some(p) = self.get_position(node_data, node_id, node_state) {
1787            let _ = write!(s,"position: {};", p.get_css_value_fmt());
1788        }
1789        if let Some(p) = self.get_top(node_data, node_id, node_state) {
1790            let _ = write!(s,"top: {};", p.get_css_value_fmt());
1791        }
1792        if let Some(p) = self.get_bottom(node_data, node_id, node_state) {
1793            let _ = write!(s,"bottom: {};", p.get_css_value_fmt());
1794        }
1795        if let Some(p) = self.get_right(node_data, node_id, node_state) {
1796            let _ = write!(s,"right: {};", p.get_css_value_fmt());
1797        }
1798        if let Some(p) = self.get_left(node_data, node_id, node_state) {
1799            let _ = write!(s,"left: {};", p.get_css_value_fmt());
1800        }
1801        if let Some(p) = self.get_padding_top(node_data, node_id, node_state) {
1802            let _ = write!(s,"padding-top: {};", p.get_css_value_fmt());
1803        }
1804        if let Some(p) = self.get_padding_bottom(node_data, node_id, node_state) {
1805            let _ = write!(s,"padding-bottom: {};", p.get_css_value_fmt());
1806        }
1807        if let Some(p) = self.get_padding_left(node_data, node_id, node_state) {
1808            let _ = write!(s,"padding-left: {};", p.get_css_value_fmt());
1809        }
1810        if let Some(p) = self.get_padding_right(node_data, node_id, node_state) {
1811            let _ = write!(s,"padding-right: {};", p.get_css_value_fmt());
1812        }
1813        if let Some(p) = self.get_margin_top(node_data, node_id, node_state) {
1814            let _ = write!(s,"margin-top: {};", p.get_css_value_fmt());
1815        }
1816        if let Some(p) = self.get_margin_bottom(node_data, node_id, node_state) {
1817            let _ = write!(s,"margin-bottom: {};", p.get_css_value_fmt());
1818        }
1819        if let Some(p) = self.get_margin_left(node_data, node_id, node_state) {
1820            let _ = write!(s,"margin-left: {};", p.get_css_value_fmt());
1821        }
1822        if let Some(p) = self.get_margin_right(node_data, node_id, node_state) {
1823            let _ = write!(s,"margin-right: {};", p.get_css_value_fmt());
1824        }
1825        if let Some(p) = self.get_border_top_width(node_data, node_id, node_state) {
1826            let _ = write!(s,"border-top-width: {};", p.get_css_value_fmt());
1827        }
1828        if let Some(p) = self.get_border_left_width(node_data, node_id, node_state) {
1829            let _ = write!(s,"border-left-width: {};", p.get_css_value_fmt());
1830        }
1831        if let Some(p) = self.get_border_right_width(node_data, node_id, node_state) {
1832            let _ = write!(s,"border-right-width: {};", p.get_css_value_fmt());
1833        }
1834        if let Some(p) = self.get_border_bottom_width(node_data, node_id, node_state) {
1835            let _ = write!(s,"border-bottom-width: {};", p.get_css_value_fmt());
1836        }
1837        if let Some(p) = self.get_overflow_x(node_data, node_id, node_state) {
1838            let _ = write!(s,"overflow-x: {};", p.get_css_value_fmt());
1839        }
1840        if let Some(p) = self.get_overflow_y(node_data, node_id, node_state) {
1841            let _ = write!(s,"overflow-y: {};", p.get_css_value_fmt());
1842        }
1843        if let Some(p) = self.get_flex_direction(node_data, node_id, node_state) {
1844            let _ = write!(s,"flex-direction: {};", p.get_css_value_fmt());
1845        }
1846        if let Some(p) = self.get_flex_wrap(node_data, node_id, node_state) {
1847            let _ = write!(s,"flex-wrap: {};", p.get_css_value_fmt());
1848        }
1849        if let Some(p) = self.get_flex_grow(node_data, node_id, node_state) {
1850            let _ = write!(s,"flex-grow: {};", p.get_css_value_fmt());
1851        }
1852        if let Some(p) = self.get_flex_shrink(node_data, node_id, node_state) {
1853            let _ = write!(s,"flex-shrink: {};", p.get_css_value_fmt());
1854        }
1855        if let Some(p) = self.get_justify_content(node_data, node_id, node_state) {
1856            let _ = write!(s,"justify-content: {};", p.get_css_value_fmt());
1857        }
1858        if let Some(p) = self.get_align_items(node_data, node_id, node_state) {
1859            let _ = write!(s,"align-items: {};", p.get_css_value_fmt());
1860        }
1861        if let Some(p) = self.get_align_content(node_data, node_id, node_state) {
1862            let _ = write!(s,"align-content: {};", p.get_css_value_fmt());
1863        }
1864        s
1865    }
1866}
1867
1868#[repr(C)]
1869#[derive(Debug, PartialEq, Clone)]
1870pub struct CssPropertyCachePtr {
1871    // `ManuallyDrop` so the owned `Box` is freed ONLY by our `Drop` (gated on
1872    // `run_destructor`), never by drop-glue. The codegen Az wrapper (AzStyledDom)
1873    // nests an AzCssPropertyCachePtr field whose own `Drop` re-runs
1874    // `_delete` -> `drop_in_place::<CssPropertyCachePtr>` on the SAME bytes; with a
1875    // bare `Box` the glue freed it a second time -> double free. Layout is
1876    // unchanged (one pointer), so the AzCssPropertyCachePtr<->CssPropertyCachePtr
1877    // FFI transmute stays valid. Matches the GlContextPtr / InstantPtr convention.
1878    pub ptr: ManuallyDrop<Box<CssPropertyCache>>,
1879    pub run_destructor: bool,
1880}
1881
1882impl CssPropertyCachePtr {
1883    pub fn new(cache: CssPropertyCache) -> Self {
1884        Self {
1885            ptr: ManuallyDrop::new(Box::new(cache)),
1886            run_destructor: true,
1887        }
1888    }
1889    pub fn downcast_mut(&mut self) -> &mut CssPropertyCache {
1890        &mut self.ptr
1891    }
1892}
1893
1894impl Drop for CssPropertyCachePtr {
1895    fn drop(&mut self) {
1896        // First drop (run_destructor still true) frees the Box and clears the flag in
1897        // the shared bytes; the codegen's redundant second drop sees false -> no-op.
1898        if self.run_destructor {
1899            self.run_destructor = false;
1900            unsafe {
1901                ManuallyDrop::drop(&mut self.ptr);
1902            }
1903        }
1904    }
1905}
1906
1907/// Generates a mechanical `get_<name>` CSS-property accessor: resolve the property
1908/// for `(node_data, node_id, node_state)` via `get_property`, then downcast it with
1909/// the given `as_*` method. Covers the long run of one-line accessors below.
1910macro_rules! impl_get_prop {
1911    ($name:ident, $value_ty:ty, $variant:ident, $as_method:ident) => {
1912        pub fn $name<'a>(
1913            &'a self,
1914            node_data: &'a NodeData,
1915            node_id: &NodeId,
1916            node_state: &StyledNodeState,
1917        ) -> Option<&'a $value_ty> {
1918            self.get_property(node_data, node_id, node_state, &CssPropertyType::$variant)
1919                .and_then(|p| p.$as_method())
1920        }
1921    };
1922}
1923
1924impl CssPropertyCache {
1925    #[must_use] pub fn empty(node_count: usize) -> Self {
1926        Self {
1927            node_count,
1928            retained_author_css: Css::default(),
1929            user_overridden_properties: Vec::new(),
1930            dynamic_context: None,
1931
1932            cascaded_props: FlatVecVec::new(node_count),
1933            css_props: FlatVecVec::new(node_count),
1934
1935            computed_values: Vec::new(),
1936            compact_cache: None,
1937            global_css_props: Vec::new(),
1938            resolved_font_sizes_px: crate::sync::OnceLock::new(),
1939        }
1940    }
1941
1942    /// Clear the lazily-populated font-size cache. Call after any
1943    /// mutation that could change resolved font-sizes (restyle,
1944    /// DOM mutation, `append`, etc.). The next
1945    /// [`crate::styled_dom::StyledDom::resolved_font_size_px`] call
1946    /// repopulates via a single bottom-up tree walk.
1947    pub fn invalidate_resolved_font_sizes(&mut self) {
1948        self.resolved_font_sizes_px = crate::sync::OnceLock::new();
1949    }
1950
1951    pub fn append(&mut self, other: &mut Self) {
1952        self.user_overridden_properties.append(&mut other.user_overridden_properties);
1953        // The parent's dynamic context wins; a child subtree styled before
1954        // composition has no window context of its own worth keeping.
1955        if self.dynamic_context.is_none() {
1956            self.dynamic_context = other.dynamic_context.take();
1957        }
1958        self.cascaded_props.extend_from(&mut other.cascaded_props);
1959        self.css_props.extend_from(&mut other.css_props);
1960        self.computed_values.append(&mut other.computed_values);
1961
1962        self.node_count += other.node_count;
1963        // Indices shifted — invalidate the font-size cache too.
1964        self.resolved_font_sizes_px = crate::sync::OnceLock::new();
1965
1966        // Invalidate compact cache since node IDs shifted
1967        self.compact_cache = None;
1968    }
1969
1970    pub fn is_horizontal_overflow_visible(
1971        &self,
1972        node_data: &NodeData,
1973        node_id: &NodeId,
1974        node_state: &StyledNodeState,
1975    ) -> bool {
1976        self.get_overflow_x(node_data, node_id, node_state)
1977            .and_then(|p| p.get_property_or_default())
1978            .unwrap_or_default()
1979            .is_overflow_visible()
1980    }
1981
1982    pub fn is_vertical_overflow_visible(
1983        &self,
1984        node_data: &NodeData,
1985        node_id: &NodeId,
1986        node_state: &StyledNodeState,
1987    ) -> bool {
1988        self.get_overflow_y(node_data, node_id, node_state)
1989            .and_then(|p| p.get_property_or_default())
1990            .unwrap_or_default()
1991            .is_overflow_visible()
1992    }
1993
1994    pub fn is_horizontal_overflow_hidden(
1995        &self,
1996        node_data: &NodeData,
1997        node_id: &NodeId,
1998        node_state: &StyledNodeState,
1999    ) -> bool {
2000        self.get_overflow_x(node_data, node_id, node_state)
2001            .and_then(|p| p.get_property_or_default())
2002            .unwrap_or_default()
2003            .is_overflow_hidden()
2004    }
2005
2006    pub fn is_vertical_overflow_hidden(
2007        &self,
2008        node_data: &NodeData,
2009        node_id: &NodeId,
2010        node_state: &StyledNodeState,
2011    ) -> bool {
2012        self.get_overflow_y(node_data, node_id, node_state)
2013            .and_then(|p| p.get_property_or_default())
2014            .unwrap_or_default()
2015            .is_overflow_hidden()
2016    }
2017
2018    pub fn get_text_color_or_default(
2019        &self,
2020        node_data: &NodeData,
2021        node_id: &NodeId,
2022        node_state: &StyledNodeState,
2023    ) -> StyleTextColor {
2024        use azul_css::defaults::DEFAULT_TEXT_COLOR;
2025        self.get_text_color(node_data, node_id, node_state)
2026            .and_then(|fs| fs.get_property().copied())
2027            .unwrap_or(DEFAULT_TEXT_COLOR)
2028    }
2029
2030    /// Returns the font family of the node, or the default font family if none is set.
2031    pub fn get_font_id_or_default(
2032        &self,
2033        node_data: &NodeData,
2034        node_id: &NodeId,
2035        node_state: &StyledNodeState,
2036    ) -> StyleFontFamilyVec {
2037        use azul_css::defaults::DEFAULT_FONT_ID;
2038        let default_font_id = vec![StyleFontFamily::System(AzString::from_const_str(
2039            DEFAULT_FONT_ID,
2040        ))]
2041        .into();
2042        let font_family_opt = self.get_font_family(node_data, node_id, node_state);
2043
2044        font_family_opt
2045            .as_ref()
2046            .and_then(|family| Some(family.get_property()?.clone()))
2047            .unwrap_or(default_font_id)
2048    }
2049
2050    pub fn get_font_size_or_default(
2051        &self,
2052        node_data: &NodeData,
2053        node_id: &NodeId,
2054        node_state: &StyledNodeState,
2055    ) -> StyleFontSize {
2056        use azul_css::defaults::DEFAULT_FONT_SIZE;
2057        self.get_font_size(node_data, node_id, node_state)
2058            .and_then(|fs| fs.get_property().copied())
2059            .unwrap_or(DEFAULT_FONT_SIZE)
2060    }
2061
2062    pub fn has_border(
2063        &self,
2064        node_data: &NodeData,
2065        node_id: &NodeId,
2066        node_state: &StyledNodeState,
2067    ) -> bool {
2068        self.get_border_left_width(node_data, node_id, node_state)
2069            .is_some()
2070            || self
2071                .get_border_right_width(node_data, node_id, node_state)
2072                .is_some()
2073            || self
2074                .get_border_top_width(node_data, node_id, node_state)
2075                .is_some()
2076            || self
2077                .get_border_bottom_width(node_data, node_id, node_state)
2078                .is_some()
2079    }
2080
2081    pub fn has_box_shadow(
2082        &self,
2083        node_data: &NodeData,
2084        node_id: &NodeId,
2085        node_state: &StyledNodeState,
2086    ) -> bool {
2087        self.get_box_shadow_left(node_data, node_id, node_state)
2088            .is_some()
2089            || self
2090                .get_box_shadow_right(node_data, node_id, node_state)
2091                .is_some()
2092            || self
2093                .get_box_shadow_top(node_data, node_id, node_state)
2094                .is_some()
2095            || self
2096                .get_box_shadow_bottom(node_data, node_id, node_state)
2097                .is_some()
2098    }
2099
2100    pub fn get_property<'a>(
2101        &'a self,
2102        node_data: &'a NodeData,
2103        node_id: &NodeId,
2104        node_state: &StyledNodeState,
2105        css_property_type: &CssPropertyType,
2106    ) -> Option<&'a CssProperty> {
2107        // Thread-local counter of cascade walks, broken down by
2108        // property type. Drain with `drain_css_prop_counts` (free
2109        // fn below) when `AZ_PROP_COUNT=1` is set to see which
2110        // properties dominate the cold layout path.
2111        //
2112        // Env check is read ONCE at process start and cached in a
2113        // `OnceLock<bool>`. Before this, the env check ran per
2114        // `get_property` call — and the function fires 710k+ times
2115        // per cold layout on excel.html. `std::env::var_os` takes
2116        // ~100 ns per call on macOS (env lock + hashmap lookup), so
2117        // the naive check added ~70 ms of pure noise to every
2118        // single layout, regardless of whether the env var was set.
2119        // Using a one-time cached bool removes that overhead.
2120        //
2121        // `no_std` builds have no thread-locals / env, so the profiling
2122        // counter is compiled out entirely.
2123        #[cfg(feature = "std")]
2124        {
2125            static PROP_COUNT_ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
2126            let enabled = *PROP_COUNT_ENABLED.get_or_init(crate::profile::cascade_enabled);
2127            if enabled {
2128                // `try_with` (not `with`): the lifted-to-wasm web backend has no
2129                // real TLS, so `with` would hit `panic_access_error` (the layout
2130                // path reads CSS props via these getters → would trap). `try_with`
2131                // returns Err and we skip the profiling-only increment (and its
2132                // inner Mutex-guarded label table). Desktop behaviour unchanged —
2133                // when the env var is unset the whole block is gated off anyway.
2134                let _ = PROP_COUNTS.try_with(|c| {
2135                    *c.borrow_mut()
2136                        .entry(Self::css_prop_type_label(css_property_type))
2137                        .or_insert(0) += 1;
2138                });
2139            }
2140        }
2141
2142        // Always use full cascade resolution.
2143        // Tier 1/2/2b handle layout-hot properties via direct typed getters.
2144        // This path is only used for paint-time reads (background, shadow, etc.)
2145        self.get_property_slow(node_data, node_id, node_state, css_property_type)
2146    }
2147
2148    #[cfg(feature = "std")]
2149    #[allow(clippy::trivially_copy_pass_by_ref)] // uniform by-ref cascade-API convention (see find_in_stateful)
2150    fn css_prop_type_label(t: &CssPropertyType) -> &'static str {
2151        // Intern Debug-format labels under a mutex-guarded map so
2152        // we leak at most one `&'static str` per distinct
2153        // `CssPropertyType` variant (bounded at ≤ 178 total). Only
2154        // triggered when `AZ_PROP_COUNT=1`, so zero cost normally.
2155        use std::sync::{Mutex, OnceLock};
2156        static TABLE: OnceLock<Mutex<std::collections::HashMap<CssPropertyType, &'static str>>> =
2157            OnceLock::new();
2158        let m = TABLE.get_or_init(|| Mutex::new(std::collections::HashMap::new()));
2159        let mut g = m.lock().expect("AZ_PROP_COUNT label table poisoned");
2160        if let Some(s) = g.get(t) {
2161            return s;
2162        }
2163        let s: String = std::format!("{t:?}");
2164        let leaked: &'static str = std::boxed::Box::leak(s.into_boxed_str());
2165        g.insert(*t, leaked);
2166        leaked
2167    }
2168
2169    /// Full cascade resolution for any CSS property type.
2170    /// Walks all cascade layers: user overrides → inline → stylesheet → cascaded → computed → UA.
2171    /// Also used by restyle functions that need state-aware lookups.
2172    #[allow(clippy::trivially_copy_pass_by_ref)] // uniform by-ref cascade-API convention (see find_in_stateful)
2173    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
2174    pub(crate) fn get_property_slow<'a>(
2175        &'a self,
2176        node_data: &'a NodeData,
2177        node_id: &NodeId,
2178        node_state: &StyledNodeState,
2179        css_property_type: &CssPropertyType,
2180    ) -> Option<&'a CssProperty> {
2181
2182        use azul_css::dynamic_selector::{DynamicSelector, PseudoStateType};
2183
2184        // Helper: do these conditions identify a rule that applies in `state`
2185        // under the window's dynamic context? Empty conditions = Normal-only.
2186        // Otherwise EVERY condition must hold: a pseudo-state condition must
2187        // equal `state`, and every other condition (viewport/@media, theme,
2188        // OS, container...) is evaluated against the window-provided
2189        // `dynamic_context`. With no context yet (a StyledDom no window has
2190        // adopted), non-pseudo conditions do not apply - the exact behaviour
2191        // they had before contexts were wired through, so creation-time
2192        // styling is unchanged.
2193        let ctx = self.dynamic_context.as_deref();
2194        let matches_pseudo_state = |conds: &azul_css::dynamic_selector::DynamicSelectorVec,
2195                                    state: PseudoStateType|
2196         -> bool {
2197            let conditions = conds.as_slice();
2198            if conditions.is_empty() {
2199                state == PseudoStateType::Normal
2200            } else {
2201                conditions.iter().all(|c| match c {
2202                    DynamicSelector::PseudoState(s) => *s == state,
2203                    non_pseudo => ctx.is_some_and(|ctx| non_pseudo.matches(ctx)),
2204                })
2205            }
2206        };
2207
2208        // First test if there is some user-defined override for the property
2209        if let Some(v) = self.user_overridden_properties.get(node_id.index()) {
2210            if let Ok(idx) = v.binary_search_by_key(css_property_type, |(k, _)| *k) {
2211                return Some(&v[idx].1);
2212            }
2213        }
2214
2215        // If that fails, see if there is an inline CSS property that matches
2216        // :focus > :active > :hover > normal (fallback)
2217        if node_state.focused {
2218            // PRIORITY 1: Inline CSS properties (highest priority per CSS spec)
2219            if let Some(p) = node_data.style.iter_inline_properties().fold(None, |acc, (prop, conds)| {
2220                if matches_pseudo_state(conds, PseudoStateType::Focus)
2221                    && prop.get_type() == *css_property_type
2222                {
2223                    // LAST matching inline declaration wins (CSS source order),
2224                    // same as the compact builder's later-overwrites-earlier and
2225                    // get_property_with_context - a widget's merged_style()
2226                    // appends overrides and relies on exactly this.
2227                    Some(prop)
2228                } else {
2229                    acc
2230                }
2231            }) {
2232                return Some(p);
2233            }
2234
2235            // PRIORITY 2: CSS stylesheet properties
2236            if let Some(p) = Self::find_in_stateful(
2237                self.css_props.get_slice(node_id.index()),
2238                PseudoStateType::Focus,
2239                css_property_type,
2240            ) {
2241                return Some(p);
2242            }
2243
2244            // PRIORITY 3: Cascaded/inherited properties
2245            if let Some(p) = Self::find_in_stateful(
2246                self.cascaded_props.get_slice(node_id.index()),
2247                PseudoStateType::Focus,
2248                css_property_type,
2249            ) {
2250                return Some(p);
2251            }
2252        }
2253
2254        if node_state.active {
2255            // PRIORITY 1: Inline CSS properties (highest priority per CSS spec)
2256            if let Some(p) = node_data.style.iter_inline_properties().fold(None, |acc, (prop, conds)| {
2257                if matches_pseudo_state(conds, PseudoStateType::Active)
2258                    && prop.get_type() == *css_property_type
2259                {
2260                    // LAST matching inline declaration wins (CSS source order),
2261                    // same as the compact builder's later-overwrites-earlier and
2262                    // get_property_with_context - a widget's merged_style()
2263                    // appends overrides and relies on exactly this.
2264                    Some(prop)
2265                } else {
2266                    acc
2267                }
2268            }) {
2269                return Some(p);
2270            }
2271
2272            // PRIORITY 2: CSS stylesheet properties
2273            if let Some(p) = Self::find_in_stateful(
2274                self.css_props.get_slice(node_id.index()),
2275                PseudoStateType::Active,
2276                css_property_type,
2277            ) {
2278                return Some(p);
2279            }
2280
2281            // PRIORITY 3: Cascaded/inherited properties
2282            if let Some(p) = Self::find_in_stateful(
2283                self.cascaded_props.get_slice(node_id.index()),
2284                PseudoStateType::Active,
2285                css_property_type,
2286            ) {
2287                return Some(p);
2288            }
2289        }
2290
2291        // :dragging pseudo-state (higher priority than :hover)
2292        if node_state.dragging {
2293            if let Some(p) = node_data.style.iter_inline_properties().fold(None, |acc, (prop, conds)| {
2294                if matches_pseudo_state(conds, PseudoStateType::Dragging)
2295                    && prop.get_type() == *css_property_type
2296                {
2297                    // LAST matching inline declaration wins (CSS source order),
2298                    // same as the compact builder's later-overwrites-earlier and
2299                    // get_property_with_context - a widget's merged_style()
2300                    // appends overrides and relies on exactly this.
2301                    Some(prop)
2302                } else {
2303                    acc
2304                }
2305            }) {
2306                return Some(p);
2307            }
2308
2309            if let Some(p) = Self::find_in_stateful(
2310                self.css_props.get_slice(node_id.index()),
2311                PseudoStateType::Dragging,
2312                css_property_type,
2313            ) {
2314                return Some(p);
2315            }
2316
2317            if let Some(p) = Self::find_in_stateful(
2318                self.cascaded_props.get_slice(node_id.index()),
2319                PseudoStateType::Dragging,
2320                css_property_type,
2321            ) {
2322                return Some(p);
2323            }
2324        }
2325
2326        // :drag-over pseudo-state (higher priority than :hover)
2327        if node_state.drag_over {
2328            if let Some(p) = node_data.style.iter_inline_properties().fold(None, |acc, (prop, conds)| {
2329                if matches_pseudo_state(conds, PseudoStateType::DragOver)
2330                    && prop.get_type() == *css_property_type
2331                {
2332                    // LAST matching inline declaration wins (CSS source order),
2333                    // same as the compact builder's later-overwrites-earlier and
2334                    // get_property_with_context - a widget's merged_style()
2335                    // appends overrides and relies on exactly this.
2336                    Some(prop)
2337                } else {
2338                    acc
2339                }
2340            }) {
2341                return Some(p);
2342            }
2343
2344            if let Some(p) = Self::find_in_stateful(
2345                self.css_props.get_slice(node_id.index()),
2346                PseudoStateType::DragOver,
2347                css_property_type,
2348            ) {
2349                return Some(p);
2350            }
2351
2352            if let Some(p) = Self::find_in_stateful(
2353                self.cascaded_props.get_slice(node_id.index()),
2354                PseudoStateType::DragOver,
2355                css_property_type,
2356            ) {
2357                return Some(p);
2358            }
2359        }
2360
2361        if node_state.hover {
2362            // PRIORITY 1: Inline CSS properties (highest priority per CSS spec)
2363            if let Some(p) = node_data.style.iter_inline_properties().fold(None, |acc, (prop, conds)| {
2364                if matches_pseudo_state(conds, PseudoStateType::Hover)
2365                    && prop.get_type() == *css_property_type
2366                {
2367                    // LAST matching inline declaration wins (CSS source order),
2368                    // same as the compact builder's later-overwrites-earlier and
2369                    // get_property_with_context - a widget's merged_style()
2370                    // appends overrides and relies on exactly this.
2371                    Some(prop)
2372                } else {
2373                    acc
2374                }
2375            }) {
2376                return Some(p);
2377            }
2378
2379            // PRIORITY 2: CSS stylesheet properties
2380            if let Some(p) = Self::find_in_stateful(
2381                self.css_props.get_slice(node_id.index()),
2382                PseudoStateType::Hover,
2383                css_property_type,
2384            ) {
2385                return Some(p);
2386            }
2387
2388            // PRIORITY 3: Cascaded/inherited properties
2389            if let Some(p) = Self::find_in_stateful(
2390                self.cascaded_props.get_slice(node_id.index()),
2391                PseudoStateType::Hover,
2392                css_property_type,
2393            ) {
2394                return Some(p);
2395            }
2396        }
2397
2398        // Normal/fallback properties - always apply as base layer
2399        // PRIORITY 1: Inline CSS properties (highest priority per CSS spec)
2400        if let Some(p) = node_data.style.iter_inline_properties().fold(None, |acc, (prop, conds)| {
2401            if matches_pseudo_state(conds, PseudoStateType::Normal)
2402                && prop.get_type() == *css_property_type
2403            {
2404                // LAST matching inline declaration wins (CSS source order),
2405                // same as the compact builder's later-overwrites-earlier and
2406                // get_property_with_context - the widget pattern
2407                // "display:none + display:flex @media(max-width)" relies on
2408                // exactly this ordering.
2409                Some(prop)
2410            } else {
2411                acc
2412            }
2413        }) {
2414            return Some(p);
2415        }
2416
2417        // PRIORITY 2: CSS stylesheet properties
2418        if let Some(p) = Self::find_in_stateful(
2419            self.css_props.get_slice(node_id.index()),
2420            PseudoStateType::Normal,
2421            css_property_type,
2422        ) {
2423            return Some(p);
2424        }
2425
2426        // PRIORITY 2b: Global `*` selector properties (specificity 0,0,0)
2427        // These are collected once during restyle and apply to all nodes.
2428        // Lower priority than per-node rules but higher than inheritance/UA.
2429        if let Some(p) = self.global_css_props.iter().find(|p| p.get_type() == *css_property_type) {
2430            return Some(p);
2431        }
2432
2433        // PRIORITY 3: Cascaded/inherited properties
2434        if let Some(p) = Self::find_in_stateful(
2435            self.cascaded_props.get_slice(node_id.index()),
2436            PseudoStateType::Normal,
2437            css_property_type,
2438        ) {
2439            return Some(p);
2440        }
2441
2442        // Check computed values cache for inherited properties
2443        // Sorted Vec with binary search
2444        if css_property_type.is_inheritable() {
2445            if let Some(vec) = self.computed_values.get(node_id.index()) {
2446                if let Ok(idx) = vec.binary_search_by_key(css_property_type, |(k, _)| *k) {
2447                    return Some(&vec[idx].1.property);
2448                }
2449            }
2450        }
2451
2452        // User-agent stylesheet fallback (lowest precedence)
2453        // Check if the node type has a default value for this property
2454        crate::ua_css::get_ua_property(&node_data.node_type, *css_property_type)
2455    }
2456
2457    /// Get a CSS property using `DynamicSelectorContext` for evaluation.
2458    ///
2459    /// This is the new API that supports @media queries, @container queries,
2460    /// OS-specific styles, and all pseudo-states via `CssPropertyWithConditions`.
2461    ///
2462    /// The evaluation follows "last wins" semantics - properties are evaluated
2463    /// in reverse order and the first matching property wins.
2464    #[allow(clippy::trivially_copy_pass_by_ref)] // uniform by-ref cascade-API convention (see find_in_stateful)
2465    pub(crate) fn get_property_with_context<'a>(
2466        &'a self,
2467        node_data: &'a NodeData,
2468        node_id: &NodeId,
2469        context: &DynamicSelectorContext,
2470        css_property_type: &CssPropertyType,
2471    ) -> Option<&'a CssProperty> {
2472        // First test if there is some user-defined override for the property
2473        if let Some(v) = self.user_overridden_properties.get(node_id.index()) {
2474            if let Ok(idx) = v.binary_search_by_key(css_property_type, |(k, _)| *k) {
2475                return Some(&v[idx].1);
2476            }
2477        }
2478
2479        // Check inline CSS properties with DynamicSelectorContext evaluation.
2480        // Iterate in REVERSE order across the flat (prop, conds) view —
2481        // "last found wins" semantics, replacing the old Focus > Active >
2482        // Hover > Normal priority chain.
2483        // "last found wins": scan the flat (prop, conds) view forward and keep the
2484        // last match (iter_inline_properties is not DoubleEndedIterator, so this
2485        // replaces an earlier collect-then-rev-find_map).
2486        let mut last_inline = None;
2487        for (prop, conds) in node_data.style.iter_inline_properties() {
2488            let conditions_match = conds.as_slice().iter().all(|c| c.matches(context));
2489            if prop.get_type() == *css_property_type && conditions_match {
2490                last_inline = Some(prop);
2491            }
2492        }
2493        if let Some(prop) = last_inline {
2494            return Some(prop);
2495        }
2496
2497        // Fall back to CSS file and cascaded properties
2498        let legacy_state = StyledNodeState::from_pseudo_state_flags(&context.pseudo_state);
2499        if let Some(p) = self.get_property(node_data, node_id, &legacy_state, css_property_type) {
2500            return Some(p);
2501        }
2502
2503        None
2504    }
2505
2506    /// Check if any properties with conditions would change between two contexts.
2507    /// This is used for re-layout detection on viewport/container resize.
2508    pub(crate) fn check_properties_changed(
2509        node_data: &NodeData,
2510        old_context: &DynamicSelectorContext,
2511        new_context: &DynamicSelectorContext,
2512    ) -> bool {
2513        for (_prop, conds) in node_data.style.iter_inline_properties() {
2514            let was_active = conds.as_slice().iter().all(|c| c.matches(old_context));
2515            let is_active = conds.as_slice().iter().all(|c| c.matches(new_context));
2516            if was_active != is_active {
2517                return true;
2518            }
2519        }
2520        false
2521    }
2522
2523    /// Check if any layout-affecting properties would change between two contexts.
2524    /// This is a more targeted check for re-layout detection.
2525    pub(crate) fn check_layout_properties_changed(
2526        node_data: &NodeData,
2527        old_context: &DynamicSelectorContext,
2528        new_context: &DynamicSelectorContext,
2529    ) -> bool {
2530        for (prop, conds) in node_data.style.iter_inline_properties() {
2531            // Skip non-layout-affecting properties
2532            if !prop.get_type().can_trigger_relayout() {
2533                continue;
2534            }
2535
2536            let was_active = conds.as_slice().iter().all(|c| c.matches(old_context));
2537            let is_active = conds.as_slice().iter().all(|c| c.matches(new_context));
2538            if was_active != is_active {
2539                return true;
2540            }
2541        }
2542        false
2543    }
2544
2545    impl_get_prop!(get_background_content, StyleBackgroundContentVecValue, BackgroundContent, as_background_content);
2546
2547    impl_get_prop!(get_hyphens, StyleHyphensValue, Hyphens, as_hyphens);
2548
2549    impl_get_prop!(get_word_break, StyleWordBreakValue, WordBreak, as_word_break);
2550
2551    impl_get_prop!(get_overflow_wrap, StyleOverflowWrapValue, OverflowWrap, as_overflow_wrap);
2552
2553    impl_get_prop!(get_line_break, StyleLineBreakValue, LineBreak, as_line_break);
2554
2555    impl_get_prop!(get_text_align_last, StyleTextAlignLastValue, TextAlignLast, as_text_align_last);
2556
2557    impl_get_prop!(get_text_transform, StyleTextTransformValue, TextTransform, as_text_transform);
2558
2559    impl_get_prop!(get_object_fit, StyleObjectFitValue, ObjectFit, as_object_fit);
2560
2561    impl_get_prop!(get_text_overflow, StyleTextOverflowValue, TextOverflow, as_text_overflow);
2562
2563    impl_get_prop!(get_text_orientation, StyleTextOrientationValue, TextOrientation, as_text_orientation);
2564
2565    impl_get_prop!(get_object_position, StyleObjectPositionValue, ObjectPosition, as_object_position);
2566
2567    impl_get_prop!(get_aspect_ratio, StyleAspectRatioValue, AspectRatio, as_aspect_ratio);
2568
2569    impl_get_prop!(get_direction, StyleDirectionValue, Direction, as_direction);
2570
2571    impl_get_prop!(get_unicode_bidi, StyleUnicodeBidiValue, UnicodeBidi, as_unicode_bidi);
2572
2573    impl_get_prop!(get_text_box_trim, StyleTextBoxTrimValue, TextBoxTrim, as_text_box_trim);
2574
2575    impl_get_prop!(get_text_box_edge, StyleTextBoxEdgeValue, TextBoxEdge, as_text_box_edge);
2576
2577    impl_get_prop!(get_dominant_baseline, StyleDominantBaselineValue, DominantBaseline, as_dominant_baseline);
2578
2579    impl_get_prop!(get_alignment_baseline, StyleAlignmentBaselineValue, AlignmentBaseline, as_alignment_baseline);
2580
2581    impl_get_prop!(get_baseline_source, StyleBaselineSourceValue, BaselineSource, as_baseline_source);
2582
2583    impl_get_prop!(get_line_fit_edge, StyleLineFitEdgeValue, LineFitEdge, as_line_fit_edge);
2584
2585    impl_get_prop!(get_initial_letter_align, StyleInitialLetterAlignValue, InitialLetterAlign, as_initial_letter_align);
2586
2587    impl_get_prop!(get_initial_letter_wrap, StyleInitialLetterWrapValue, InitialLetterWrap, as_initial_letter_wrap);
2588
2589    impl_get_prop!(get_scrollbar_gutter, StyleScrollbarGutterValue, ScrollbarGutter, as_scrollbar_gutter);
2590
2591    impl_get_prop!(get_overflow_clip_margin, StyleOverflowClipMarginValue, OverflowClipMargin, as_overflow_clip_margin);
2592
2593    impl_get_prop!(get_clip, StyleClipRectValue, Clip, as_clip);
2594
2595    impl_get_prop!(get_white_space, StyleWhiteSpaceValue, WhiteSpace, as_white_space);
2596    impl_get_prop!(get_background_position, StyleBackgroundPositionVecValue, BackgroundPosition, as_background_position);
2597    impl_get_prop!(get_background_size, StyleBackgroundSizeVecValue, BackgroundSize, as_background_size);
2598    impl_get_prop!(get_background_repeat, StyleBackgroundRepeatVecValue, BackgroundRepeat, as_background_repeat);
2599    impl_get_prop!(get_font_size, StyleFontSizeValue, FontSize, as_font_size);
2600    impl_get_prop!(get_font_family, StyleFontFamilyVecValue, FontFamily, as_font_family);
2601    impl_get_prop!(get_font_weight, StyleFontWeightValue, FontWeight, as_font_weight);
2602    impl_get_prop!(get_font_style, StyleFontStyleValue, FontStyle, as_font_style);
2603    impl_get_prop!(get_text_color, StyleTextColorValue, TextColor, as_text_color);
2604    impl_get_prop!(get_text_indent, StyleTextIndentValue, TextIndent, as_text_indent);
2605    impl_get_prop!(get_initial_letter, StyleInitialLetterValue, InitialLetter, as_initial_letter);
2606    impl_get_prop!(get_line_clamp, StyleLineClampValue, LineClamp, as_line_clamp);
2607    impl_get_prop!(get_hanging_punctuation, StyleHangingPunctuationValue, HangingPunctuation, as_hanging_punctuation);
2608    impl_get_prop!(get_text_combine_upright, StyleTextCombineUprightValue, TextCombineUpright, as_text_combine_upright);
2609    impl_get_prop!(get_exclusion_margin, StyleExclusionMarginValue, ExclusionMargin, as_exclusion_margin);
2610    impl_get_prop!(get_hyphenation_language, StyleHyphenationLanguageValue, HyphenationLanguage, as_hyphenation_language);
2611    impl_get_prop!(get_caret_color, CaretColorValue, CaretColor, as_caret_color);
2612
2613    impl_get_prop!(get_caret_width, CaretWidthValue, CaretWidth, as_caret_width);
2614
2615    impl_get_prop!(get_caret_animation_duration, CaretAnimationDurationValue, CaretAnimationDuration, as_caret_animation_duration);
2616
2617    impl_get_prop!(get_selection_background_color, SelectionBackgroundColorValue, SelectionBackgroundColor, as_selection_background_color);
2618
2619    impl_get_prop!(get_selection_color, SelectionColorValue, SelectionColor, as_selection_color);
2620
2621    impl_get_prop!(get_selection_radius, SelectionRadiusValue, SelectionRadius, as_selection_radius);
2622
2623    impl_get_prop!(get_text_justify, LayoutTextJustifyValue, TextJustify, as_text_justify);
2624
2625    impl_get_prop!(get_z_index, LayoutZIndexValue, ZIndex, as_z_index);
2626
2627    impl_get_prop!(get_flex_basis, LayoutFlexBasisValue, FlexBasis, as_flex_basis);
2628
2629    impl_get_prop!(get_column_gap, LayoutColumnGapValue, ColumnGap, as_column_gap);
2630
2631    impl_get_prop!(get_row_gap, LayoutRowGapValue, RowGap, as_row_gap);
2632
2633    impl_get_prop!(get_grid_template_columns, LayoutGridTemplateColumnsValue, GridTemplateColumns, as_grid_template_columns);
2634
2635    impl_get_prop!(get_grid_template_rows, LayoutGridTemplateRowsValue, GridTemplateRows, as_grid_template_rows);
2636
2637    impl_get_prop!(get_grid_auto_columns, LayoutGridAutoColumnsValue, GridAutoColumns, as_grid_auto_columns);
2638
2639    impl_get_prop!(get_grid_auto_rows, LayoutGridAutoRowsValue, GridAutoRows, as_grid_auto_rows);
2640
2641    impl_get_prop!(get_grid_column, LayoutGridColumnValue, GridColumn, as_grid_column);
2642
2643    impl_get_prop!(get_grid_row, LayoutGridRowValue, GridRow, as_grid_row);
2644
2645    impl_get_prop!(get_grid_auto_flow, LayoutGridAutoFlowValue, GridAutoFlow, as_grid_auto_flow);
2646
2647    impl_get_prop!(get_justify_self, LayoutJustifySelfValue, JustifySelf, as_justify_self);
2648
2649    impl_get_prop!(get_justify_items, LayoutJustifyItemsValue, JustifyItems, as_justify_items);
2650
2651    impl_get_prop!(get_gap, LayoutGapValue, Gap, as_gap);
2652
2653    /// Method for getting grid-gap property
2654    #[allow(clippy::trivially_copy_pass_by_ref)] // uniform by-ref cascade-API convention (see find_in_stateful)
2655    pub(crate) fn get_grid_gap<'a>(
2656        &'a self,
2657        node_data: &'a NodeData,
2658        node_id: &NodeId,
2659        node_state: &StyledNodeState,
2660    ) -> Option<&'a LayoutGapValue> {
2661        self.get_property(node_data, node_id, node_state, &CssPropertyType::GridGap)
2662            .and_then(|p| p.as_grid_gap())
2663    }
2664
2665    impl_get_prop!(get_align_self, LayoutAlignSelfValue, AlignSelf, as_align_self);
2666
2667    impl_get_prop!(get_font, StyleFontValue, Font, as_font);
2668
2669    impl_get_prop!(get_writing_mode, LayoutWritingModeValue, WritingMode, as_writing_mode);
2670
2671    impl_get_prop!(get_clear, LayoutClearValue, Clear, as_clear);
2672
2673    impl_get_prop!(get_shape_outside, ShapeOutsideValue, ShapeOutside, as_shape_outside);
2674
2675    impl_get_prop!(get_shape_inside, ShapeInsideValue, ShapeInside, as_shape_inside);
2676
2677    impl_get_prop!(get_clip_path, ClipPathValue, ClipPath, as_clip_path);
2678
2679    /// Method for getting scrollbar track background
2680    pub fn get_scrollbar_track<'a>(
2681        &'a self,
2682        node_data: &'a NodeData,
2683        node_id: &NodeId,
2684        node_state: &StyledNodeState,
2685    ) -> Option<&'a StyleBackgroundContentValue> {
2686        self.get_property(node_data, node_id, node_state, &CssPropertyType::ScrollbarTrack)
2687            .and_then(|p| p.as_scrollbar_track())
2688    }
2689
2690    /// Method for getting scrollbar thumb background
2691    pub fn get_scrollbar_thumb<'a>(
2692        &'a self,
2693        node_data: &'a NodeData,
2694        node_id: &NodeId,
2695        node_state: &StyledNodeState,
2696    ) -> Option<&'a StyleBackgroundContentValue> {
2697        self.get_property(node_data, node_id, node_state, &CssPropertyType::ScrollbarThumb)
2698            .and_then(|p| p.as_scrollbar_thumb())
2699    }
2700
2701    /// Method for getting scrollbar button background
2702    pub fn get_scrollbar_button<'a>(
2703        &'a self,
2704        node_data: &'a NodeData,
2705        node_id: &NodeId,
2706        node_state: &StyledNodeState,
2707    ) -> Option<&'a StyleBackgroundContentValue> {
2708        self.get_property(node_data, node_id, node_state, &CssPropertyType::ScrollbarButton)
2709            .and_then(|p| p.as_scrollbar_button())
2710    }
2711
2712    /// Method for getting scrollbar corner background
2713    pub fn get_scrollbar_corner<'a>(
2714        &'a self,
2715        node_data: &'a NodeData,
2716        node_id: &NodeId,
2717        node_state: &StyledNodeState,
2718    ) -> Option<&'a StyleBackgroundContentValue> {
2719        self.get_property(node_data, node_id, node_state, &CssPropertyType::ScrollbarCorner)
2720            .and_then(|p| p.as_scrollbar_corner())
2721    }
2722
2723    /// Method for getting scrollbar resizer background
2724    pub fn get_scrollbar_resizer<'a>(
2725        &'a self,
2726        node_data: &'a NodeData,
2727        node_id: &NodeId,
2728        node_state: &StyledNodeState,
2729    ) -> Option<&'a StyleBackgroundContentValue> {
2730        self.get_property(node_data, node_id, node_state, &CssPropertyType::ScrollbarResizer)
2731            .and_then(|p| p.as_scrollbar_resizer())
2732    }
2733
2734    impl_get_prop!(get_scrollbar_width, LayoutScrollbarWidthValue, ScrollbarWidth, as_scrollbar_width);
2735
2736    impl_get_prop!(get_scrollbar_color, StyleScrollbarColorValue, ScrollbarColor, as_scrollbar_color);
2737
2738    impl_get_prop!(get_scrollbar_visibility, ScrollbarVisibilityModeValue, ScrollbarVisibility, as_scrollbar_visibility);
2739
2740    impl_get_prop!(get_scrollbar_fade_delay, ScrollbarFadeDelayValue, ScrollbarFadeDelay, as_scrollbar_fade_delay);
2741
2742    impl_get_prop!(get_scrollbar_fade_duration, ScrollbarFadeDurationValue, ScrollbarFadeDuration, as_scrollbar_fade_duration);
2743
2744    impl_get_prop!(get_visibility, StyleVisibilityValue, Visibility, as_visibility);
2745
2746    impl_get_prop!(get_break_before, PageBreakValue, BreakBefore, as_break_before);
2747
2748    impl_get_prop!(get_break_after, PageBreakValue, BreakAfter, as_break_after);
2749
2750    impl_get_prop!(get_break_inside, BreakInsideValue, BreakInside, as_break_inside);
2751
2752    impl_get_prop!(get_orphans, OrphansValue, Orphans, as_orphans);
2753
2754    impl_get_prop!(get_widows, WidowsValue, Widows, as_widows);
2755
2756    impl_get_prop!(get_box_decoration_break, BoxDecorationBreakValue, BoxDecorationBreak, as_box_decoration_break);
2757
2758    impl_get_prop!(get_column_count, ColumnCountValue, ColumnCount, as_column_count);
2759
2760    impl_get_prop!(get_column_width, ColumnWidthValue, ColumnWidth, as_column_width);
2761
2762    impl_get_prop!(get_column_span, ColumnSpanValue, ColumnSpan, as_column_span);
2763
2764    impl_get_prop!(get_column_fill, ColumnFillValue, ColumnFill, as_column_fill);
2765
2766    impl_get_prop!(get_column_rule_width, ColumnRuleWidthValue, ColumnRuleWidth, as_column_rule_width);
2767
2768    impl_get_prop!(get_column_rule_style, ColumnRuleStyleValue, ColumnRuleStyle, as_column_rule_style);
2769
2770    impl_get_prop!(get_column_rule_color, ColumnRuleColorValue, ColumnRuleColor, as_column_rule_color);
2771
2772    impl_get_prop!(get_flow_into, FlowIntoValue, FlowInto, as_flow_into);
2773
2774    impl_get_prop!(get_flow_from, FlowFromValue, FlowFrom, as_flow_from);
2775
2776    impl_get_prop!(get_shape_margin, ShapeMarginValue, ShapeMargin, as_shape_margin);
2777
2778    impl_get_prop!(get_shape_image_threshold, ShapeImageThresholdValue, ShapeImageThreshold, as_shape_image_threshold);
2779
2780    impl_get_prop!(get_content, ContentValue, Content, as_content);
2781
2782    impl_get_prop!(get_counter_reset, CounterResetValue, CounterReset, as_counter_reset);
2783
2784    impl_get_prop!(get_counter_increment, CounterIncrementValue, CounterIncrement, as_counter_increment);
2785
2786    impl_get_prop!(get_string_set, StringSetValue, StringSet, as_string_set);
2787    impl_get_prop!(get_text_align, StyleTextAlignValue, TextAlign, as_text_align);
2788    impl_get_prop!(get_user_select, StyleUserSelectValue, UserSelect, as_user_select);
2789    impl_get_prop!(get_text_decoration, StyleTextDecorationValue, TextDecoration, as_text_decoration);
2790    impl_get_prop!(get_vertical_align, StyleVerticalAlignValue, VerticalAlign, as_vertical_align);
2791    impl_get_prop!(get_line_height, StyleLineHeightValue, LineHeight, as_line_height);
2792    impl_get_prop!(get_letter_spacing, StyleLetterSpacingValue, LetterSpacing, as_letter_spacing);
2793    impl_get_prop!(get_word_spacing, StyleWordSpacingValue, WordSpacing, as_word_spacing);
2794    impl_get_prop!(get_tab_size, StyleTabSizeValue, TabSize, as_tab_size);
2795    impl_get_prop!(get_cursor, StyleCursorValue, Cursor, as_cursor);
2796    impl_get_prop!(get_box_shadow_left, StyleBoxShadowValue, BoxShadowLeft, as_box_shadow_left);
2797    impl_get_prop!(get_box_shadow_right, StyleBoxShadowValue, BoxShadowRight, as_box_shadow_right);
2798    impl_get_prop!(get_box_shadow_top, StyleBoxShadowValue, BoxShadowTop, as_box_shadow_top);
2799    impl_get_prop!(get_box_shadow_bottom, StyleBoxShadowValue, BoxShadowBottom, as_box_shadow_bottom);
2800    impl_get_prop!(get_border_top_color, StyleBorderTopColorValue, BorderTopColor, as_border_top_color);
2801    impl_get_prop!(get_border_left_color, StyleBorderLeftColorValue, BorderLeftColor, as_border_left_color);
2802    impl_get_prop!(get_border_right_color, StyleBorderRightColorValue, BorderRightColor, as_border_right_color);
2803    impl_get_prop!(get_border_bottom_color, StyleBorderBottomColorValue, BorderBottomColor, as_border_bottom_color);
2804    impl_get_prop!(get_border_top_style, StyleBorderTopStyleValue, BorderTopStyle, as_border_top_style);
2805    impl_get_prop!(get_border_left_style, StyleBorderLeftStyleValue, BorderLeftStyle, as_border_left_style);
2806    impl_get_prop!(get_border_right_style, StyleBorderRightStyleValue, BorderRightStyle, as_border_right_style);
2807    impl_get_prop!(get_border_bottom_style, StyleBorderBottomStyleValue, BorderBottomStyle, as_border_bottom_style);
2808    impl_get_prop!(get_border_top_left_radius, StyleBorderTopLeftRadiusValue, BorderTopLeftRadius, as_border_top_left_radius);
2809    impl_get_prop!(get_border_top_right_radius, StyleBorderTopRightRadiusValue, BorderTopRightRadius, as_border_top_right_radius);
2810    impl_get_prop!(get_border_bottom_left_radius, StyleBorderBottomLeftRadiusValue, BorderBottomLeftRadius, as_border_bottom_left_radius);
2811    impl_get_prop!(get_border_bottom_right_radius, StyleBorderBottomRightRadiusValue, BorderBottomRightRadius, as_border_bottom_right_radius);
2812    impl_get_prop!(get_opacity, StyleOpacityValue, Opacity, as_opacity);
2813    impl_get_prop!(get_transform, StyleTransformVecValue, Transform, as_transform);
2814    impl_get_prop!(get_transform_origin, StyleTransformOriginValue, TransformOrigin, as_transform_origin);
2815    impl_get_prop!(get_perspective_origin, StylePerspectiveOriginValue, PerspectiveOrigin, as_perspective_origin);
2816    impl_get_prop!(get_backface_visibility, StyleBackfaceVisibilityValue, BackfaceVisibility, as_backface_visibility);
2817    impl_get_prop!(get_display, LayoutDisplayValue, Display, as_display);
2818    impl_get_prop!(get_float, LayoutFloatValue, Float, as_float);
2819    impl_get_prop!(get_box_sizing, LayoutBoxSizingValue, BoxSizing, as_box_sizing);
2820    impl_get_prop!(get_width, LayoutWidthValue, Width, as_width);
2821    impl_get_prop!(get_height, LayoutHeightValue, Height, as_height);
2822    impl_get_prop!(get_min_width, LayoutMinWidthValue, MinWidth, as_min_width);
2823    impl_get_prop!(get_min_height, LayoutMinHeightValue, MinHeight, as_min_height);
2824    impl_get_prop!(get_max_width, LayoutMaxWidthValue, MaxWidth, as_max_width);
2825    impl_get_prop!(get_max_height, LayoutMaxHeightValue, MaxHeight, as_max_height);
2826    impl_get_prop!(get_position, LayoutPositionValue, Position, as_position);
2827    impl_get_prop!(get_top, LayoutTopValue, Top, as_top);
2828    impl_get_prop!(get_bottom, LayoutInsetBottomValue, Bottom, as_bottom);
2829    impl_get_prop!(get_right, LayoutRightValue, Right, as_right);
2830    impl_get_prop!(get_left, LayoutLeftValue, Left, as_left);
2831    impl_get_prop!(get_padding_top, LayoutPaddingTopValue, PaddingTop, as_padding_top);
2832    impl_get_prop!(get_padding_bottom, LayoutPaddingBottomValue, PaddingBottom, as_padding_bottom);
2833    impl_get_prop!(get_padding_left, LayoutPaddingLeftValue, PaddingLeft, as_padding_left);
2834    impl_get_prop!(get_padding_right, LayoutPaddingRightValue, PaddingRight, as_padding_right);
2835    impl_get_prop!(get_margin_top, LayoutMarginTopValue, MarginTop, as_margin_top);
2836    impl_get_prop!(get_margin_bottom, LayoutMarginBottomValue, MarginBottom, as_margin_bottom);
2837    impl_get_prop!(get_margin_left, LayoutMarginLeftValue, MarginLeft, as_margin_left);
2838    impl_get_prop!(get_margin_right, LayoutMarginRightValue, MarginRight, as_margin_right);
2839    impl_get_prop!(get_border_top_width, LayoutBorderTopWidthValue, BorderTopWidth, as_border_top_width);
2840    impl_get_prop!(get_border_left_width, LayoutBorderLeftWidthValue, BorderLeftWidth, as_border_left_width);
2841    impl_get_prop!(get_border_right_width, LayoutBorderRightWidthValue, BorderRightWidth, as_border_right_width);
2842    impl_get_prop!(get_border_bottom_width, LayoutBorderBottomWidthValue, BorderBottomWidth, as_border_bottom_width);
2843    impl_get_prop!(get_overflow_x, LayoutOverflowValue, OverflowX, as_overflow_x);
2844    impl_get_prop!(get_overflow_y, LayoutOverflowValue, OverflowY, as_overflow_y);
2845    impl_get_prop!(get_overflow_block, LayoutOverflowValue, OverflowBlock, as_overflow_block);
2846    impl_get_prop!(get_overflow_inline, LayoutOverflowValue, OverflowInline, as_overflow_inline);
2847    impl_get_prop!(get_flex_direction, LayoutFlexDirectionValue, FlexDirection, as_flex_direction);
2848    impl_get_prop!(get_flex_wrap, LayoutFlexWrapValue, FlexWrap, as_flex_wrap);
2849    impl_get_prop!(get_flex_grow, LayoutFlexGrowValue, FlexGrow, as_flex_grow);
2850    impl_get_prop!(get_flex_shrink, LayoutFlexShrinkValue, FlexShrink, as_flex_shrink);
2851    impl_get_prop!(get_justify_content, LayoutJustifyContentValue, JustifyContent, as_justify_content);
2852    impl_get_prop!(get_align_items, LayoutAlignItemsValue, AlignItems, as_align_items);
2853    impl_get_prop!(get_align_content, LayoutAlignContentValue, AlignContent, as_align_content);
2854    impl_get_prop!(get_mix_blend_mode, StyleMixBlendModeValue, MixBlendMode, as_mix_blend_mode);
2855    impl_get_prop!(get_filter, StyleFilterVecValue, Filter, as_filter);
2856    impl_get_prop!(get_backdrop_filter, StyleFilterVecValue, BackdropFilter, as_backdrop_filter);
2857    impl_get_prop!(get_text_shadow, StyleBoxShadowValue, TextShadow, as_text_shadow);
2858    impl_get_prop!(get_list_style_type, StyleListStyleTypeValue, ListStyleType, as_list_style_type);
2859    impl_get_prop!(get_list_style_position, StyleListStylePositionValue, ListStylePosition, as_list_style_position);
2860    impl_get_prop!(get_table_layout, LayoutTableLayoutValue, TableLayout, as_table_layout);
2861    impl_get_prop!(get_border_collapse, StyleBorderCollapseValue, BorderCollapse, as_border_collapse);
2862    impl_get_prop!(get_border_spacing, LayoutBorderSpacingValue, BorderSpacing, as_border_spacing);
2863    impl_get_prop!(get_caption_side, StyleCaptionSideValue, CaptionSide, as_caption_side);
2864    impl_get_prop!(get_empty_cells, StyleEmptyCellsValue, EmptyCells, as_empty_cells);
2865
2866    // Width calculation methods
2867    pub fn calc_width(
2868        &self,
2869        node_data: &NodeData,
2870        node_id: &NodeId,
2871        styled_node_state: &StyledNodeState,
2872        reference_width: f32,
2873    ) -> f32 {
2874        self.get_width(node_data, node_id, styled_node_state)
2875            .and_then(|w| match w.get_property()? {
2876                LayoutWidth::Px(px) => Some(px.to_pixels_internal(
2877                    reference_width,
2878                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2879                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2880                )),
2881                _ => Some(0.0), // min-content/max-content not resolved here
2882            })
2883            .unwrap_or(0.0)
2884    }
2885
2886    pub fn calc_min_width(
2887        &self,
2888        node_data: &NodeData,
2889        node_id: &NodeId,
2890        styled_node_state: &StyledNodeState,
2891        reference_width: f32,
2892    ) -> f32 {
2893        self.get_min_width(node_data, node_id, styled_node_state)
2894            .and_then(|w| {
2895                Some(w.get_property()?.inner.to_pixels_internal(
2896                    reference_width,
2897                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2898                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2899                ))
2900            })
2901            .unwrap_or(0.0)
2902    }
2903
2904    pub fn calc_max_width(
2905        &self,
2906        node_data: &NodeData,
2907        node_id: &NodeId,
2908        styled_node_state: &StyledNodeState,
2909        reference_width: f32,
2910    ) -> Option<f32> {
2911        self.get_max_width(node_data, node_id, styled_node_state)
2912            .and_then(|w| {
2913                Some(w.get_property()?.inner.to_pixels_internal(
2914                    reference_width,
2915                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2916                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2917                ))
2918            })
2919    }
2920
2921    // Height calculation methods
2922    pub fn calc_height(
2923        &self,
2924        node_data: &NodeData,
2925        node_id: &NodeId,
2926        styled_node_state: &StyledNodeState,
2927        reference_height: f32,
2928    ) -> f32 {
2929        self.get_height(node_data, node_id, styled_node_state)
2930            .and_then(|h| match h.get_property()? {
2931                LayoutHeight::Px(px) => Some(px.to_pixels_internal(
2932                    reference_height,
2933                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2934                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2935                )),
2936                _ => Some(0.0), // min-content/max-content not resolved here
2937            })
2938            .unwrap_or(0.0)
2939    }
2940
2941    pub fn calc_min_height(
2942        &self,
2943        node_data: &NodeData,
2944        node_id: &NodeId,
2945        styled_node_state: &StyledNodeState,
2946        reference_height: f32,
2947    ) -> f32 {
2948        self.get_min_height(node_data, node_id, styled_node_state)
2949            .and_then(|h| {
2950                Some(h.get_property()?.inner.to_pixels_internal(
2951                    reference_height,
2952                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2953                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2954                ))
2955            })
2956            .unwrap_or(0.0)
2957    }
2958
2959    pub fn calc_max_height(
2960        &self,
2961        node_data: &NodeData,
2962        node_id: &NodeId,
2963        styled_node_state: &StyledNodeState,
2964        reference_height: f32,
2965    ) -> Option<f32> {
2966        self.get_max_height(node_data, node_id, styled_node_state)
2967            .and_then(|h| {
2968                Some(h.get_property()?.inner.to_pixels_internal(
2969                    reference_height,
2970                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2971                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2972                ))
2973            })
2974    }
2975
2976    // Position calculation methods
2977    pub fn calc_left(
2978        &self,
2979        node_data: &NodeData,
2980        node_id: &NodeId,
2981        styled_node_state: &StyledNodeState,
2982        reference_width: f32,
2983    ) -> Option<f32> {
2984        self.get_left(node_data, node_id, styled_node_state)
2985            .and_then(|l| {
2986                Some(l.get_property()?.inner.to_pixels_internal(
2987                    reference_width,
2988                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2989                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2990                ))
2991            })
2992    }
2993
2994    pub fn calc_right(
2995        &self,
2996        node_data: &NodeData,
2997        node_id: &NodeId,
2998        styled_node_state: &StyledNodeState,
2999        reference_width: f32,
3000    ) -> Option<f32> {
3001        self.get_right(node_data, node_id, styled_node_state)
3002            .and_then(|r| {
3003                Some(r.get_property()?.inner.to_pixels_internal(
3004                    reference_width,
3005                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3006                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3007                ))
3008            })
3009    }
3010
3011    pub fn calc_top(
3012        &self,
3013        node_data: &NodeData,
3014        node_id: &NodeId,
3015        styled_node_state: &StyledNodeState,
3016        reference_height: f32,
3017    ) -> Option<f32> {
3018        self.get_top(node_data, node_id, styled_node_state)
3019            .and_then(|t| {
3020                Some(t.get_property()?.inner.to_pixels_internal(
3021                    reference_height,
3022                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3023                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3024                ))
3025            })
3026    }
3027
3028    pub fn calc_bottom(
3029        &self,
3030        node_data: &NodeData,
3031        node_id: &NodeId,
3032        styled_node_state: &StyledNodeState,
3033        reference_height: f32,
3034    ) -> Option<f32> {
3035        self.get_bottom(node_data, node_id, styled_node_state)
3036            .and_then(|b| {
3037                Some(b.get_property()?.inner.to_pixels_internal(
3038                    reference_height,
3039                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3040                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3041                ))
3042            })
3043    }
3044
3045    // Border calculation methods
3046    pub fn calc_border_left_width(
3047        &self,
3048        node_data: &NodeData,
3049        node_id: &NodeId,
3050        styled_node_state: &StyledNodeState,
3051        reference_width: f32,
3052    ) -> f32 {
3053        self.get_border_left_width(node_data, node_id, styled_node_state)
3054            .and_then(|b| {
3055                Some(b.get_property()?.inner.to_pixels_internal(
3056                    reference_width,
3057                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3058                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3059                ))
3060            })
3061            .unwrap_or(0.0)
3062    }
3063
3064    pub fn calc_border_right_width(
3065        &self,
3066        node_data: &NodeData,
3067        node_id: &NodeId,
3068        styled_node_state: &StyledNodeState,
3069        reference_width: f32,
3070    ) -> f32 {
3071        self.get_border_right_width(node_data, node_id, styled_node_state)
3072            .and_then(|b| {
3073                Some(b.get_property()?.inner.to_pixels_internal(
3074                    reference_width,
3075                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3076                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3077                ))
3078            })
3079            .unwrap_or(0.0)
3080    }
3081
3082    pub fn calc_border_top_width(
3083        &self,
3084        node_data: &NodeData,
3085        node_id: &NodeId,
3086        styled_node_state: &StyledNodeState,
3087        reference_height: f32,
3088    ) -> f32 {
3089        self.get_border_top_width(node_data, node_id, styled_node_state)
3090            .and_then(|b| {
3091                Some(b.get_property()?.inner.to_pixels_internal(
3092                    reference_height,
3093                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3094                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3095                ))
3096            })
3097            .unwrap_or(0.0)
3098    }
3099
3100    pub fn calc_border_bottom_width(
3101        &self,
3102        node_data: &NodeData,
3103        node_id: &NodeId,
3104        styled_node_state: &StyledNodeState,
3105        reference_height: f32,
3106    ) -> f32 {
3107        self.get_border_bottom_width(node_data, node_id, styled_node_state)
3108            .and_then(|b| {
3109                Some(b.get_property()?.inner.to_pixels_internal(
3110                    reference_height,
3111                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3112                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3113                ))
3114            })
3115            .unwrap_or(0.0)
3116    }
3117
3118    // Padding calculation methods
3119    pub fn calc_padding_left(
3120        &self,
3121        node_data: &NodeData,
3122        node_id: &NodeId,
3123        styled_node_state: &StyledNodeState,
3124        reference_width: f32,
3125    ) -> f32 {
3126        self.get_padding_left(node_data, node_id, styled_node_state)
3127            .and_then(|p| {
3128                Some(p.get_property()?.inner.to_pixels_internal(
3129                    reference_width,
3130                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3131                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3132                ))
3133            })
3134            .unwrap_or(0.0)
3135    }
3136
3137    pub fn calc_padding_right(
3138        &self,
3139        node_data: &NodeData,
3140        node_id: &NodeId,
3141        styled_node_state: &StyledNodeState,
3142        reference_width: f32,
3143    ) -> f32 {
3144        self.get_padding_right(node_data, node_id, styled_node_state)
3145            .and_then(|p| {
3146                Some(p.get_property()?.inner.to_pixels_internal(
3147                    reference_width,
3148                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3149                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3150                ))
3151            })
3152            .unwrap_or(0.0)
3153    }
3154
3155    pub fn calc_padding_top(
3156        &self,
3157        node_data: &NodeData,
3158        node_id: &NodeId,
3159        styled_node_state: &StyledNodeState,
3160        reference_height: f32,
3161    ) -> f32 {
3162        self.get_padding_top(node_data, node_id, styled_node_state)
3163            .and_then(|p| {
3164                Some(p.get_property()?.inner.to_pixels_internal(
3165                    reference_height,
3166                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3167                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3168                ))
3169            })
3170            .unwrap_or(0.0)
3171    }
3172
3173    pub fn calc_padding_bottom(
3174        &self,
3175        node_data: &NodeData,
3176        node_id: &NodeId,
3177        styled_node_state: &StyledNodeState,
3178        reference_height: f32,
3179    ) -> f32 {
3180        self.get_padding_bottom(node_data, node_id, styled_node_state)
3181            .and_then(|p| {
3182                Some(p.get_property()?.inner.to_pixels_internal(
3183                    reference_height,
3184                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3185                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3186                ))
3187            })
3188            .unwrap_or(0.0)
3189    }
3190
3191    // Margin calculation methods
3192    pub fn calc_margin_left(
3193        &self,
3194        node_data: &NodeData,
3195        node_id: &NodeId,
3196        styled_node_state: &StyledNodeState,
3197        reference_width: f32,
3198    ) -> f32 {
3199        self.get_margin_left(node_data, node_id, styled_node_state)
3200            .and_then(|m| {
3201                Some(m.get_property()?.inner.to_pixels_internal(
3202                    reference_width,
3203                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3204                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3205                ))
3206            })
3207            .unwrap_or(0.0)
3208    }
3209
3210    pub fn calc_margin_right(
3211        &self,
3212        node_data: &NodeData,
3213        node_id: &NodeId,
3214        styled_node_state: &StyledNodeState,
3215        reference_width: f32,
3216    ) -> f32 {
3217        self.get_margin_right(node_data, node_id, styled_node_state)
3218            .and_then(|m| {
3219                Some(m.get_property()?.inner.to_pixels_internal(
3220                    reference_width,
3221                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3222                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3223                ))
3224            })
3225            .unwrap_or(0.0)
3226    }
3227
3228    pub fn calc_margin_top(
3229        &self,
3230        node_data: &NodeData,
3231        node_id: &NodeId,
3232        styled_node_state: &StyledNodeState,
3233        reference_height: f32,
3234    ) -> f32 {
3235        self.get_margin_top(node_data, node_id, styled_node_state)
3236            .and_then(|m| {
3237                Some(m.get_property()?.inner.to_pixels_internal(
3238                    reference_height,
3239                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3240                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3241                ))
3242            })
3243            .unwrap_or(0.0)
3244    }
3245
3246    pub fn calc_margin_bottom(
3247        &self,
3248        node_data: &NodeData,
3249        node_id: &NodeId,
3250        styled_node_state: &StyledNodeState,
3251        reference_height: f32,
3252    ) -> f32 {
3253        self.get_margin_bottom(node_data, node_id, styled_node_state)
3254            .and_then(|m| {
3255                Some(m.get_property()?.inner.to_pixels_internal(
3256                    reference_height,
3257                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3258                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3259                ))
3260            })
3261            .unwrap_or(0.0)
3262    }
3263
3264    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
3265    fn resolve_property_dependency(
3266        target_property: &CssProperty,
3267        reference_property: &CssProperty,
3268    ) -> Option<CssProperty> {
3269        // wildcard import: this big property-dispatch match references the full set
3270        // of layout property types; enumerating them all is unmaintainable.
3271        #[allow(clippy::wildcard_imports)]
3272        use azul_css::{
3273            css::CssPropertyValue,
3274            props::{
3275                basic::{font::StyleFontSize, length::SizeMetric, pixel::PixelValue},
3276                layout::*,
3277                style::{SelectionRadius, StyleLetterSpacing, StyleWordSpacing},
3278            },
3279        };
3280
3281        // Extract PixelValue from various property types (returns owned value)
3282        let get_pixel_value = |prop: &CssProperty| -> Option<PixelValue> {
3283            match prop {
3284                CssProperty::FontSize(val) => val.get_property().map(|v| v.inner),
3285                CssProperty::LetterSpacing(val) => val.get_property().map(|v| v.inner),
3286                CssProperty::WordSpacing(val) => val.get_property().map(|v| v.inner),
3287                CssProperty::PaddingLeft(val) => val.get_property().map(|v| v.inner),
3288                CssProperty::PaddingRight(val) => val.get_property().map(|v| v.inner),
3289                CssProperty::PaddingTop(val) => val.get_property().map(|v| v.inner),
3290                CssProperty::PaddingBottom(val) => val.get_property().map(|v| v.inner),
3291                CssProperty::MarginLeft(val) => val.get_property().map(|v| v.inner),
3292                CssProperty::MarginRight(val) => val.get_property().map(|v| v.inner),
3293                CssProperty::MarginTop(val) => val.get_property().map(|v| v.inner),
3294                CssProperty::MarginBottom(val) => val.get_property().map(|v| v.inner),
3295                CssProperty::MinWidth(val) => val.get_property().map(|v| v.inner),
3296                CssProperty::MinHeight(val) => val.get_property().map(|v| v.inner),
3297                CssProperty::MaxWidth(val) => val.get_property().map(|v| v.inner),
3298                CssProperty::MaxHeight(val) => val.get_property().map(|v| v.inner),
3299                CssProperty::SelectionRadius(val) => val.get_property().map(|v| v.inner),
3300                _ => None,
3301            }
3302        };
3303
3304        let target_pixel_value = get_pixel_value(target_property)?;
3305        let reference_pixel_value = get_pixel_value(reference_property)?;
3306
3307        // Convert reference to absolute pixels first
3308        let reference_px = match reference_pixel_value.metric {
3309            SizeMetric::Px => reference_pixel_value.number.get(),
3310            SizeMetric::Pt => reference_pixel_value.number.get() * PT_TO_PX,
3311            SizeMetric::In => reference_pixel_value.number.get() * IN_TO_PX,
3312            SizeMetric::Cm => reference_pixel_value.number.get() * CM_TO_PX,
3313            SizeMetric::Mm => reference_pixel_value.number.get() * MM_TO_PX,
3314            // Reference can't be relative (em/rem/%) or viewport-relative.
3315            SizeMetric::Em
3316            | SizeMetric::Rem
3317            | SizeMetric::Percent
3318            | SizeMetric::Vw
3319            | SizeMetric::Vh
3320            | SizeMetric::Vmin
3321            | SizeMetric::Vmax => return None,
3322        };
3323
3324        // Resolve target based on reference
3325        let resolved_px = match target_pixel_value.metric {
3326            SizeMetric::Px => target_pixel_value.number.get(),
3327            SizeMetric::Pt => target_pixel_value.number.get() * PT_TO_PX,
3328            SizeMetric::In => target_pixel_value.number.get() * IN_TO_PX,
3329            SizeMetric::Cm => target_pixel_value.number.get() * CM_TO_PX,
3330            SizeMetric::Mm => target_pixel_value.number.get() * MM_TO_PX,
3331            // em/rem both scale by reference (rem uses reference as root font-size).
3332            SizeMetric::Em | SizeMetric::Rem => target_pixel_value.number.get() * reference_px,
3333            SizeMetric::Percent => target_pixel_value.number.get() / 100.0 * reference_px,
3334            // Need viewport context
3335            SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => return None,
3336        };
3337
3338        // Create a new property with the resolved value
3339        let resolved_pixel_value = PixelValue::px(resolved_px);
3340
3341        match target_property {
3342            CssProperty::FontSize(_) => Some(CssProperty::FontSize(CssPropertyValue::Exact(
3343                StyleFontSize {
3344                    inner: resolved_pixel_value,
3345                },
3346            ))),
3347            CssProperty::LetterSpacing(_) => Some(CssProperty::LetterSpacing(
3348                CssPropertyValue::Exact(StyleLetterSpacing {
3349                    inner: resolved_pixel_value,
3350                }),
3351            )),
3352            CssProperty::WordSpacing(_) => Some(CssProperty::WordSpacing(CssPropertyValue::Exact(
3353                StyleWordSpacing {
3354                    inner: resolved_pixel_value,
3355                },
3356            ))),
3357            CssProperty::PaddingLeft(_) => Some(CssProperty::PaddingLeft(CssPropertyValue::Exact(
3358                LayoutPaddingLeft {
3359                    inner: resolved_pixel_value,
3360                },
3361            ))),
3362            CssProperty::PaddingRight(_) => Some(CssProperty::PaddingRight(
3363                CssPropertyValue::Exact(LayoutPaddingRight {
3364                    inner: resolved_pixel_value,
3365                }),
3366            )),
3367            CssProperty::PaddingTop(_) => Some(CssProperty::PaddingTop(CssPropertyValue::Exact(
3368                LayoutPaddingTop {
3369                    inner: resolved_pixel_value,
3370                },
3371            ))),
3372            CssProperty::PaddingBottom(_) => Some(CssProperty::PaddingBottom(
3373                CssPropertyValue::Exact(LayoutPaddingBottom {
3374                    inner: resolved_pixel_value,
3375                }),
3376            )),
3377            CssProperty::MarginLeft(_) => Some(CssProperty::MarginLeft(CssPropertyValue::Exact(
3378                LayoutMarginLeft {
3379                    inner: resolved_pixel_value,
3380                },
3381            ))),
3382            CssProperty::MarginRight(_) => Some(CssProperty::MarginRight(CssPropertyValue::Exact(
3383                LayoutMarginRight {
3384                    inner: resolved_pixel_value,
3385                },
3386            ))),
3387            CssProperty::MarginTop(_) => Some(CssProperty::MarginTop(CssPropertyValue::Exact(
3388                LayoutMarginTop {
3389                    inner: resolved_pixel_value,
3390                },
3391            ))),
3392            CssProperty::MarginBottom(_) => Some(CssProperty::MarginBottom(
3393                CssPropertyValue::Exact(LayoutMarginBottom {
3394                    inner: resolved_pixel_value,
3395                }),
3396            )),
3397            CssProperty::MinWidth(_) => Some(CssProperty::MinWidth(CssPropertyValue::Exact(
3398                LayoutMinWidth {
3399                    inner: resolved_pixel_value,
3400                },
3401            ))),
3402            CssProperty::MinHeight(_) => Some(CssProperty::MinHeight(CssPropertyValue::Exact(
3403                LayoutMinHeight {
3404                    inner: resolved_pixel_value,
3405                },
3406            ))),
3407            CssProperty::MaxWidth(_) => Some(CssProperty::MaxWidth(CssPropertyValue::Exact(
3408                LayoutMaxWidth {
3409                    inner: resolved_pixel_value,
3410                },
3411            ))),
3412            CssProperty::MaxHeight(_) => Some(CssProperty::MaxHeight(CssPropertyValue::Exact(
3413                LayoutMaxHeight {
3414                    inner: resolved_pixel_value,
3415                },
3416            ))),
3417            CssProperty::SelectionRadius(_) => Some(CssProperty::SelectionRadius(
3418                CssPropertyValue::Exact(SelectionRadius {
3419                    inner: resolved_pixel_value,
3420                }),
3421            )),
3422            _ => None,
3423        }
3424    }
3425
3426    /// Applies user-agent (UA) CSS properties to the cascade before inheritance.
3427    ///
3428    /// UA CSS has the lowest priority in the cascade, so it should only be applied
3429    /// if the node doesn't already have the property from inline styles or author CSS.
3430    ///
3431    /// This is critical for text nodes: UA CSS properties (like font-weight: bold for H1)
3432    /// must be in the cascade maps so they can be inherited by child text nodes.
3433    ///
3434    /// Uses a bitset per node to avoid O(n²) scanning of property vecs.
3435    #[allow(clippy::too_many_lines)] // cohesive single-pass walker; splitting adds state-threading
3436    pub fn apply_ua_css(&mut self, node_data: &[NodeData]) {
3437        use azul_css::props::property::CssPropertyType;
3438        use azul_css::dynamic_selector::PseudoStateType;
3439
3440        let node_count = node_data.len();
3441        if node_count == 0 {
3442            return;
3443        }
3444
3445        // Build a bitset per node: which CssPropertyType values are already set (Normal state).
3446        // CssPropertyType has ~178 variants, so we need [u128; 2] per node (256 bits).
3447        let mut prop_set: Vec<[u128; 2]> = vec![[0u128; 2]; node_count];
3448
3449        // Mark properties from css_props (author CSS, Normal state)
3450        for (node_idx, props) in self.css_props.iter_node_slices() {
3451            for p in props {
3452                if p.state == PseudoStateType::Normal {
3453                    let d = p.prop_type as u16 as usize;
3454                    if d < 128 {
3455                        prop_set[node_idx][0] |= 1u128 << d;
3456                    } else {
3457                        prop_set[node_idx][1] |= 1u128 << (d - 128);
3458                    }
3459                }
3460            }
3461        }
3462
3463        // Mark properties from cascaded_props (Normal state)
3464        for (node_idx, props) in self.cascaded_props.iter_node_slices() {
3465            for p in props {
3466                if p.state == PseudoStateType::Normal {
3467                    let d = p.prop_type as u16 as usize;
3468                    if d < 128 {
3469                        prop_set[node_idx][0] |= 1u128 << d;
3470                    } else {
3471                        prop_set[node_idx][1] |= 1u128 << (d - 128);
3472                    }
3473                }
3474            }
3475        }
3476
3477        // Mark properties from inline CSS (NodeData.style, unconditional = Normal)
3478        for (node_idx, node) in node_data.iter().enumerate() {
3479            for (prop, conds) in node.style.iter_inline_properties() {
3480                let is_normal = conds.as_slice().is_empty();
3481                if is_normal {
3482                    let d = prop.get_type() as u16 as usize;
3483                    if d < 128 {
3484                        prop_set[node_idx][0] |= 1u128 << d;
3485                    } else {
3486                        prop_set[node_idx][1] |= 1u128 << (d - 128);
3487                    }
3488                }
3489            }
3490        }
3491
3492        // Mark properties from the GLOBAL `*` bucket. A `* { margin: 0 }`
3493        // reset is author CSS and must beat UA defaults on every ELEMENT
3494        // (origin beats specificity), but it is stored once globally rather
3495        // than per node, so the per-node marking above never saw it - the UA
3496        // body margin (8px) survived the classic reset and every page using
3497        // it rendered shifted against the browser reference. Text nodes are
3498        // exempt: `*` matches elements only (the compact builder makes the
3499        // same distinction), and UA defaults for text nodes must stay.
3500        if !self.global_css_props.is_empty() {
3501            let mut global_bits = [0u128; 2];
3502            for p in &self.global_css_props {
3503                let d = p.get_type() as u16 as usize;
3504                if d < 128 {
3505                    global_bits[0] |= 1u128 << d;
3506                } else {
3507                    global_bits[1] |= 1u128 << (d - 128);
3508                }
3509            }
3510            for (node_idx, node) in node_data.iter().enumerate() {
3511                if !node.is_text_node() {
3512                    prop_set[node_idx][0] |= global_bits[0];
3513                    prop_set[node_idx][1] |= global_bits[1];
3514                }
3515            }
3516        }
3517
3518        // All UA property types that get_ua_property() may return Some for
3519        let property_types = [
3520            CssPropertyType::Display,
3521            CssPropertyType::Width,
3522            CssPropertyType::Height,
3523            CssPropertyType::FontSize,
3524            CssPropertyType::FontWeight,
3525            CssPropertyType::FontFamily,
3526            CssPropertyType::MarginTop,
3527            CssPropertyType::MarginBottom,
3528            CssPropertyType::MarginLeft,
3529            CssPropertyType::MarginRight,
3530            CssPropertyType::PaddingTop,
3531            CssPropertyType::PaddingBottom,
3532            CssPropertyType::PaddingLeft,
3533            CssPropertyType::PaddingRight,
3534            CssPropertyType::BorderTopStyle,
3535            CssPropertyType::BorderTopWidth,
3536            CssPropertyType::BorderTopColor,
3537            CssPropertyType::BreakInside,
3538            CssPropertyType::BreakAfter,
3539            CssPropertyType::ListStyleType,
3540            CssPropertyType::CounterReset,
3541            CssPropertyType::TextDecoration,
3542            CssPropertyType::TextAlign,
3543            CssPropertyType::VerticalAlign,
3544            CssPropertyType::Cursor,
3545        ];
3546
3547        // Apply UA CSS: only insert for property types not yet set (bitset check = O(1))
3548        for (node_index, node) in node_data.iter().enumerate() {
3549            let node_type = &node.node_type;
3550
3551            for prop_type in &property_types {
3552                // Check bitset: if already set, skip entirely
3553                let d = *prop_type as u16 as usize;
3554                let has_prop = if d < 128 {
3555                    (prop_set[node_index][0] & (1u128 << d)) != 0
3556                } else {
3557                    (prop_set[node_index][1] & (1u128 << (d - 128))) != 0
3558                };
3559
3560                if has_prop {
3561                    continue;
3562                }
3563
3564                // Check if UA CSS defines this property for this node type
3565                if let Some(ua_prop) = crate::ua_css::get_ua_property(node_type, *prop_type) {
3566                    self.cascaded_props.push_to(node_index, StatefulCssProperty {
3567                        state: PseudoStateType::Normal,
3568                        prop_type: *prop_type,
3569                        property: ua_prop.clone(),
3570                    });
3571
3572                    // Mark as set in the bitset (prevent duplicate insertion for same node)
3573                    if d < 128 {
3574                        prop_set[node_index][0] |= 1u128 << d;
3575                    } else {
3576                        prop_set[node_index][1] |= 1u128 << (d - 128);
3577                    }
3578                }
3579            }
3580        }
3581    }
3582
3583    /// Sort `cascaded_props` by (state, `prop_type`) and flatten into contiguous memory.
3584    /// Must be called after `apply_ua_css()` which adds entries to `cascaded_props`.
3585    pub fn sort_cascaded_props(&mut self) {
3586        self.cascaded_props.sort_each_and_flatten(|p| (p.state, p.prop_type));
3587    }
3588
3589    /// Compute inherited values for all nodes in the DOM tree.
3590    ///
3591    /// Implements CSS inheritance: walk tree depth-first, apply cascade priority
3592    /// (inherited → cascaded → css → inline → user), create dependency chains for
3593    /// relative values. Call `apply_ua_css()` before this function.
3594    pub fn compute_inherited_values(
3595        &mut self,
3596        node_hierarchy: &[NodeHierarchyItem],
3597        node_data: &[NodeData],
3598    ) -> Vec<NodeId> {
3599        if self.computed_values.len() < node_hierarchy.len() {
3600            self.computed_values.resize(node_hierarchy.len(), Vec::new());
3601        }
3602        node_hierarchy
3603            .iter()
3604            .enumerate()
3605            .filter_map(|(node_index, hierarchy_item)| {
3606                let node_id = NodeId::new(node_index);
3607                let parent_id = hierarchy_item.parent_id();
3608                let parent_computed: Option<Vec<(CssPropertyType, CssPropertyWithOrigin)>> =
3609                    parent_id.and_then(|pid| self.computed_values.get(pid.index()).cloned());
3610
3611                let mut ctx = InheritanceContext {
3612                    node_id,
3613                    parent_id,
3614                    computed_values: Vec::new(),
3615                };
3616
3617                // Step 1: Inherit from parent
3618                if let Some(ref parent_values) = parent_computed {
3619                    Self::inherit_from_parent(&mut ctx, parent_values);
3620                }
3621
3622                // Steps 2-5: Apply cascade in priority order
3623                self.apply_cascade_properties(
3624                    &mut ctx,
3625                    node_id,
3626                    parent_computed.as_ref(),
3627                    node_data,
3628                    node_index,
3629                );
3630
3631                // Check for changes and store
3632                let changed = self.store_if_changed(&ctx);
3633                changed.then_some(node_id)
3634            })
3635            .collect()
3636    }
3637
3638    /// Inherit inheritable properties from parent node
3639    fn inherit_from_parent(
3640        ctx: &mut InheritanceContext,
3641        parent_values: &[(CssPropertyType, CssPropertyWithOrigin)],
3642    ) {
3643        for (prop_type, prop_with_origin) in
3644            parent_values.iter().filter(|(pt, _)| pt.is_inheritable())
3645        {
3646            let entry = (*prop_type, CssPropertyWithOrigin {
3647                property: prop_with_origin.property.clone(),
3648                origin: CssPropertyOrigin::Inherited,
3649            });
3650            // Insert into sorted vec
3651            match ctx.computed_values.binary_search_by_key(prop_type, |(k, _)| *k) {
3652                Ok(idx) => ctx.computed_values[idx] = entry,
3653                Err(idx) => ctx.computed_values.insert(idx, entry),
3654            }
3655        }
3656    }
3657
3658    /// Apply all cascade properties in priority order
3659    fn apply_cascade_properties(
3660        &self,
3661        ctx: &mut InheritanceContext,
3662        node_id: NodeId,
3663        parent_computed: Option<&Vec<(CssPropertyType, CssPropertyWithOrigin)>>,
3664        node_data: &[NodeData],
3665        node_index: usize,
3666    ) {
3667        // Step 2: Cascaded properties (UA CSS)
3668        {
3669            let cascaded_slice = self.cascaded_props.get_slice(node_id.index());
3670            for p in cascaded_slice {
3671                if p.state == azul_css::dynamic_selector::PseudoStateType::Normal
3672                    && Self::should_apply_cascaded(&ctx.computed_values, p.prop_type, &p.property) {
3673                        Self::process_property(ctx, &p.property, parent_computed);
3674                    }
3675            }
3676        }
3677
3678        // Step 3: CSS properties (stylesheets)
3679        {
3680            let css_slice = self.css_props.get_slice(node_id.index());
3681            for p in css_slice {
3682                if p.state == azul_css::dynamic_selector::PseudoStateType::Normal {
3683                    Self::process_property(ctx, &p.property, parent_computed);
3684                }
3685            }
3686        }
3687
3688        // Step 4: Inline CSS properties
3689        for (prop, conds) in node_data[node_index].style.iter_inline_properties() {
3690            // Only apply unconditional (normal) properties
3691            if conds.as_slice().is_empty() {
3692                Self::process_property(ctx, prop, parent_computed);
3693            }
3694        }
3695
3696        // Step 5: User-overridden properties
3697        if let Some(user_props) = self.user_overridden_properties.get(node_id.index()) {
3698            for (_, prop) in user_props {
3699                Self::process_property(ctx, prop, parent_computed);
3700            }
3701        }
3702    }
3703
3704    /// Check if a cascaded property should be applied.
3705    ///
3706    /// A cascaded (UA / author-selector) value applies unless the node has
3707    /// already set its OWN value (`origin == Own`), which wins per the cascade.
3708    /// An `Inherited` placeholder must NOT block it — including a relative
3709    /// `font-size`: an earlier version skipped a cascaded relative font-size when
3710    /// an inherited value existed, which silently dropped `<h1>`'s UA
3711    /// `font-size: 2em` (and every heading), leaving headings at their parent's
3712    /// size. That was wrong: `resolve_font_size_property` resolves the `em`
3713    /// against the *parent's* font-size, not the inherited value, so there is no
3714    /// double-scaling — the cascaded relative size must apply and overwrite the
3715    /// inherited entry.
3716    fn should_apply_cascaded(
3717        computed: &[(CssPropertyType, CssPropertyWithOrigin)],
3718        prop_type: CssPropertyType,
3719        _prop: &CssProperty,
3720    ) -> bool {
3721        computed
3722            .binary_search_by_key(&prop_type, |(k, _)| *k)
3723            .map_or(true, |idx| computed[idx].1.origin == CssPropertyOrigin::Inherited)
3724    }
3725
3726    /// Process a single property: resolve and store
3727    fn process_property(
3728        ctx: &mut InheritanceContext,
3729        prop: &CssProperty,
3730        parent_computed: Option<&Vec<(CssPropertyType, CssPropertyWithOrigin)>>,
3731    ) {
3732        let prop_type = prop.get_type();
3733
3734        let resolved = if prop_type == CssPropertyType::FontSize {
3735            Self::resolve_font_size_property(prop, parent_computed)
3736        } else {
3737            Self::resolve_other_property(prop, &ctx.computed_values)
3738        };
3739
3740        let entry = (prop_type, CssPropertyWithOrigin {
3741            property: resolved,
3742            origin: CssPropertyOrigin::Own,
3743        });
3744        match ctx.computed_values.binary_search_by_key(&prop_type, |(k, _)| *k) {
3745            Ok(idx) => ctx.computed_values[idx] = entry,
3746            Err(idx) => ctx.computed_values.insert(idx, entry),
3747        }
3748    }
3749
3750    /// Resolve font-size property (uses parent's font-size as reference)
3751    fn resolve_font_size_property(
3752        prop: &CssProperty,
3753        parent_computed: Option<&Vec<(CssPropertyType, CssPropertyWithOrigin)>>,
3754    ) -> CssProperty {
3755        let parent_font_size = parent_computed
3756            .and_then(|p| {
3757                p.binary_search_by_key(&CssPropertyType::FontSize, |(k, _)| *k)
3758                    .ok()
3759                    .map(|idx| &p[idx].1)
3760            });
3761
3762        parent_font_size.map_or_else(|| Self::resolve_font_size_to_pixels(
3763                prop,
3764                azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3765            ), |pfs| Self::resolve_property_dependency(prop, &pfs.property).unwrap_or_else(
3766                || {
3767                    Self::resolve_font_size_to_pixels(
3768                        prop,
3769                        azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
3770                    )
3771                },
3772            ))
3773    }
3774
3775    /// Resolve other properties (uses current node's font-size as reference)
3776    fn resolve_other_property(
3777        prop: &CssProperty,
3778        computed: &[(CssPropertyType, CssPropertyWithOrigin)],
3779    ) -> CssProperty {
3780        computed
3781            .binary_search_by_key(&CssPropertyType::FontSize, |(k, _)| *k)
3782            .ok()
3783            .and_then(|idx| Self::resolve_property_dependency(prop, &computed[idx].1.property))
3784            .unwrap_or_else(|| prop.clone())
3785    }
3786
3787    /// Convert font-size to absolute pixels
3788    fn resolve_font_size_to_pixels(prop: &CssProperty, reference_px: f32) -> CssProperty {
3789        use azul_css::{
3790            css::CssPropertyValue,
3791            props::basic::{font::StyleFontSize, length::SizeMetric, pixel::PixelValue},
3792        };
3793
3794        let CssProperty::FontSize(css_val) = prop else {
3795            return prop.clone();
3796        };
3797
3798        let Some(font_size) = css_val.get_property() else {
3799            return prop.clone();
3800        };
3801
3802        let resolved_px = match font_size.inner.metric {
3803            SizeMetric::Px => font_size.inner.number.get(),
3804            SizeMetric::Pt => font_size.inner.number.get() * PT_TO_PX,
3805            SizeMetric::In => font_size.inner.number.get() * IN_TO_PX,
3806            SizeMetric::Cm => font_size.inner.number.get() * CM_TO_PX,
3807            SizeMetric::Mm => font_size.inner.number.get() * MM_TO_PX,
3808            SizeMetric::Em => font_size.inner.number.get() * reference_px,
3809            SizeMetric::Rem => {
3810                font_size.inner.number.get() * azul_css::props::basic::pixel::DEFAULT_FONT_SIZE
3811            }
3812            SizeMetric::Percent => font_size.inner.number.get() / 100.0 * reference_px,
3813            SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => {
3814                return prop.clone();
3815            }
3816        };
3817
3818        CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
3819            inner: PixelValue::px(resolved_px),
3820        }))
3821    }
3822
3823    /// Check if font-size has relative unit (em, rem, %)
3824    fn has_relative_font_size_unit(prop: &CssProperty) -> bool {
3825        use azul_css::props::basic::length::SizeMetric;
3826
3827        let CssProperty::FontSize(css_val) = prop else {
3828            return false;
3829        };
3830
3831        css_val
3832            .get_property()
3833            .is_some_and(|fs| {
3834                matches!(
3835                    fs.inner.metric,
3836                    SizeMetric::Em | SizeMetric::Rem | SizeMetric::Percent
3837                )
3838            })
3839    }
3840
3841    /// Store computed values if changed, returns true if values were updated
3842    fn store_if_changed(&mut self, ctx: &InheritanceContext) -> bool {
3843        let values_changed = self
3844            .computed_values
3845            .get(ctx.node_id.index()) != Some(&ctx.computed_values);
3846
3847        self.computed_values[ctx.node_id.index()].clone_from(&ctx.computed_values);
3848
3849        values_changed
3850    }
3851}
3852
3853/// Context for computing inherited values for a single node
3854struct InheritanceContext {
3855    node_id: NodeId,
3856    parent_id: Option<NodeId>,
3857    computed_values: Vec<(CssPropertyType, CssPropertyWithOrigin)>,
3858}
3859
3860impl CssPropertyCache {
3861
3862    /// Clear the entire compact cache. Call after major DOM changes.
3863    pub(crate) fn invalidate_resolved_cache(&mut self) {
3864        self.compact_cache = None;
3865    }
3866}
3867
3868#[cfg(test)]
3869#[allow(clippy::float_cmp, clippy::too_many_lines)]
3870mod autotest_generated {
3871    use azul_css::{
3872        css::CssPropertyValue,
3873        dynamic_selector::{
3874            CssPropertyWithConditions, DynamicSelector, DynamicSelectorContext, PseudoStateType,
3875        },
3876        props::{
3877            basic::{length::SizeMetric, pixel::PixelValue},
3878            layout::{
3879                LayoutFlexBasis, LayoutInsetBottom, LayoutLeft, LayoutMarginTop, LayoutMaxWidth,
3880                LayoutMinWidth, LayoutOverflow, LayoutPaddingLeft, LayoutRight, LayoutTop,
3881            },
3882            style::LayoutBorderLeftWidth,
3883        },
3884    };
3885
3886    use super::*;
3887
3888    // ---------------------------------------------------------------------
3889    // helpers
3890    // ---------------------------------------------------------------------
3891
3892    /// Approximate float compare — every value here round-trips through
3893    /// `FloatValue`'s fixed-point (1/1000) encoding.
3894    fn close(a: f32, b: f32) -> bool {
3895        (a - b).abs() < 0.01
3896    }
3897
3898    fn n0() -> NodeId {
3899        NodeId::new(0)
3900    }
3901
3902    fn normal() -> StyledNodeState {
3903        StyledNodeState::default()
3904    }
3905
3906    /// A `<div>` carrying `props` as unconditional (Normal-state) inline CSS.
3907    fn div_with(props: Vec<CssProperty>) -> NodeData {
3908        let mut nd = NodeData::create_div();
3909        for property in props {
3910            nd.add_css_property(CssPropertyWithConditions {
3911                property,
3912                apply_if: Vec::new().into(),
3913            });
3914        }
3915        nd
3916    }
3917
3918    /// A `<div>` carrying `props` gated on a single pseudo-state.
3919    fn div_with_pseudo(props: Vec<CssProperty>, state: PseudoStateType) -> NodeData {
3920        let mut nd = NodeData::create_div();
3921        for property in props {
3922            nd.add_css_property(CssPropertyWithConditions {
3923                property,
3924                apply_if: vec![DynamicSelector::PseudoState(state)].into(),
3925            });
3926        }
3927        nd
3928    }
3929
3930    fn width_px(v: f32) -> CssProperty {
3931        CssProperty::Width(CssPropertyValue::Exact(LayoutWidth::Px(PixelValue::px(v))))
3932    }
3933
3934    fn width_pct(v: f32) -> CssProperty {
3935        CssProperty::Width(CssPropertyValue::Exact(LayoutWidth::Px(PixelValue::percent(
3936            v,
3937        ))))
3938    }
3939
3940    fn font_size(pv: PixelValue) -> CssProperty {
3941        CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize { inner: pv }))
3942    }
3943
3944    /// Pull `(metric, number)` back out of a `CssProperty::FontSize`.
3945    fn font_size_parts(p: &CssProperty) -> Option<(SizeMetric, f32)> {
3946        match p {
3947            CssProperty::FontSize(v) => v
3948                .get_property()
3949                .map(|fs| (fs.inner.metric, fs.inner.number.get())),
3950            _ => None,
3951        }
3952    }
3953
3954    fn stateful(state: PseudoStateType, property: CssProperty) -> StatefulCssProperty {
3955        StatefulCssProperty {
3956            state,
3957            prop_type: property.get_type(),
3958            property,
3959        }
3960    }
3961
3962    // =====================================================================
3963    // FlatVecVec — construction / getters / predicates
3964    // =====================================================================
3965
3966    #[test]
3967    fn flatvecvec_new_zero_is_empty() {
3968        let f = FlatVecVec::<i32>::new(0);
3969        assert_eq!(f.len(), 0);
3970        assert!(f.is_empty());
3971        // Quirk worth pinning: with no build slots at all, `is_flattened()` is
3972        // vacuously true (`build.is_empty()`), even though `flatten()` never ran.
3973        assert!(f.is_flattened());
3974        assert!(f.get_slice(0).is_empty());
3975    }
3976
3977    #[test]
3978    fn flatvecvec_new_invariants_hold() {
3979        let f = FlatVecVec::<i32>::new(3);
3980        assert_eq!(f.len(), 3);
3981        assert!(!f.is_empty());
3982        assert!(!f.is_flattened(), "fresh multi-slot vec is in build phase");
3983        assert_eq!(f.build_get(0), Some(&Vec::new()));
3984        assert_eq!(f.build_get(2), Some(&Vec::new()));
3985        assert_eq!(f.build_get(3), None, "one past the end");
3986        assert_eq!(f.build_get(usize::MAX), None);
3987        assert!(f.get_slice(0).is_empty());
3988    }
3989
3990    #[test]
3991    fn flatvecvec_default_is_neutral() {
3992        let f = FlatVecVec::<i32>::default();
3993        assert_eq!(f.len(), 0);
3994        assert!(f.is_empty());
3995        assert_eq!(f.build_get(0), None);
3996        assert!(f.get_slice(0).is_empty());
3997    }
3998
3999    #[test]
4000    fn flatvecvec_get_slice_out_of_bounds_is_empty_in_both_phases() {
4001        let mut f = FlatVecVec::<i32>::new(2);
4002        f.push_to(0, 7);
4003        // build phase
4004        assert_eq!(f.get_slice(0), &[7]);
4005        assert!(f.get_slice(2).is_empty());
4006        assert!(f.get_slice(usize::MAX).is_empty());
4007
4008        f.flatten();
4009        // read phase — same out-of-bounds contract, still no panic
4010        assert_eq!(f.get_slice(0), &[7]);
4011        assert!(f.get_slice(2).is_empty());
4012        assert!(f.get_slice(usize::MAX).is_empty());
4013    }
4014
4015    #[test]
4016    #[should_panic(expected = "index out of bounds")]
4017    fn flatvecvec_push_to_out_of_bounds_panics() {
4018        // Documented in `push_to`: "Panics if ... node_index >= len()".
4019        let mut f = FlatVecVec::<i32>::new(1);
4020        f.push_to(1, 0);
4021    }
4022
4023    #[test]
4024    #[should_panic(expected = "index out of bounds")]
4025    fn flatvecvec_push_to_after_flatten_panics() {
4026        // Documented in `push_to`: "Panics if already flattened".
4027        let mut f = FlatVecVec::<i32>::new(1);
4028        f.flatten();
4029        f.push_to(0, 0);
4030    }
4031
4032    #[test]
4033    #[should_panic(expected = "index out of bounds")]
4034    fn flatvecvec_build_mut_out_of_bounds_panics() {
4035        let mut f = FlatVecVec::<i32>::new(1);
4036        let _ = f.build_mut(usize::MAX);
4037    }
4038
4039    #[test]
4040    fn flatvecvec_build_iter_mut_visits_every_slot() {
4041        let mut f = FlatVecVec::<i32>::new(3);
4042        f.push_to(0, 1);
4043        f.push_to(2, 2);
4044        let mut visited = 0;
4045        for v in f.build_iter_mut() {
4046            visited += 1;
4047            v.clear();
4048        }
4049        assert_eq!(visited, 3);
4050        assert!(f.get_slice(0).is_empty());
4051        assert!(f.get_slice(2).is_empty());
4052    }
4053
4054    #[test]
4055    fn flatvecvec_build_get_returns_none_once_flattened() {
4056        let mut f = FlatVecVec::<i32>::new(1);
4057        f.push_to(0, 5);
4058        f.flatten();
4059        // Doc: "During read phase, returns None (use `get_slice` instead)."
4060        assert_eq!(f.build_get(0), None);
4061        assert_eq!(f.get_slice(0), &[5]);
4062    }
4063
4064    // =====================================================================
4065    // FlatVecVec — heap_bytes (numeric)
4066    // =====================================================================
4067
4068    #[test]
4069    fn flatvecvec_heap_bytes_zero_and_empty() {
4070        let f = FlatVecVec::<i32>::default();
4071        assert_eq!(f.heap_bytes(0), 0, "empty vec, zero element size");
4072        // All three capacities are 0, so even a nonsensical MAX element size
4073        // multiplies out to 0 rather than overflowing.
4074        assert_eq!(f.heap_bytes(usize::MAX), 0);
4075        assert_eq!(f.heap_bytes(size_of::<i32>()), 0);
4076    }
4077
4078    #[test]
4079    fn flatvecvec_heap_bytes_counts_build_and_flat_storage() {
4080        let mut f = FlatVecVec::<i32>::new(4);
4081        // Build-phase slots cost at least the outer Vec headers, even at a
4082        // per-element size of 0.
4083        assert!(f.heap_bytes(0) >= 4 * size_of::<Vec<i32>>());
4084
4085        f.push_to(0, 1);
4086        f.push_to(0, 2);
4087        let build_bytes = f.heap_bytes(size_of::<i32>());
4088        assert!(build_bytes > 0);
4089
4090        f.flatten();
4091        // Flat storage accounts for the 2 elements + the 4-entry offset table.
4092        let flat_bytes = f.heap_bytes(size_of::<i32>());
4093        assert!(flat_bytes >= 2 * size_of::<i32>() + 4 * size_of::<(u32, u32)>());
4094    }
4095
4096    // =====================================================================
4097    // FlatVecVec — flatten / sort_each_and_flatten
4098    // =====================================================================
4099
4100    #[test]
4101    fn flatvecvec_sort_each_and_flatten_keeps_last_of_equal_keys() {
4102        // CSS cascade rule: among equal keys, later source order wins.
4103        let mut f = FlatVecVec::<(i32, i32)>::new(1);
4104        f.push_to(0, (1, 10));
4105        f.push_to(0, (1, 20)); // same key, pushed later => must win
4106        f.push_to(0, (0, 30));
4107        f.sort_each_and_flatten(|p| p.0);
4108
4109        assert!(f.is_flattened());
4110        assert_eq!(f.get_slice(0), &[(0, 30), (1, 20)]);
4111    }
4112
4113    #[test]
4114    fn flatvecvec_sort_each_and_flatten_on_empty_slots() {
4115        let mut f = FlatVecVec::<i32>::new(3);
4116        f.push_to(1, 42);
4117        f.sort_each_and_flatten(|v| *v);
4118        assert_eq!(f.len(), 3);
4119        assert!(f.get_slice(0).is_empty());
4120        assert_eq!(f.get_slice(1), &[42]);
4121        assert!(f.get_slice(2).is_empty());
4122    }
4123
4124    #[test]
4125    fn flatvecvec_sort_each_and_flatten_on_zero_nodes_does_not_panic() {
4126        let mut f = FlatVecVec::<i32>::new(0);
4127        f.sort_each_and_flatten(|v| *v);
4128        assert_eq!(f.len(), 0);
4129        assert!(f.get_slice(0).is_empty());
4130    }
4131
4132    #[test]
4133    fn flatvecvec_flatten_does_not_deduplicate() {
4134        let mut f = FlatVecVec::<i32>::new(2);
4135        f.push_to(0, 5);
4136        f.push_to(0, 5);
4137        f.push_to(1, 9);
4138        f.flatten();
4139        assert!(f.is_flattened());
4140        assert_eq!(f.get_slice(0), &[5, 5], "flatten() must not dedup");
4141        assert_eq!(f.get_slice(1), &[9]);
4142    }
4143
4144    // =====================================================================
4145    // FlatVecVec — retain
4146    // =====================================================================
4147
4148    #[test]
4149    fn flatvecvec_retain_before_flatten_is_a_noop() {
4150        // Doc: "Must be called after flatten." Before that it must not silently
4151        // corrupt the build-phase data — it early-returns.
4152        let mut f = FlatVecVec::<i32>::new(1);
4153        f.push_to(0, 1);
4154        f.push_to(0, 2);
4155        f.retain(|_| false);
4156        assert_eq!(f.get_slice(0), &[1, 2], "build-phase data left untouched");
4157    }
4158
4159    #[test]
4160    fn flatvecvec_retain_preserves_per_node_order() {
4161        let mut f = FlatVecVec::<i32>::new(2);
4162        for v in [1, 2, 3, 4] {
4163            f.push_to(0, v);
4164        }
4165        f.push_to(1, 5);
4166        f.flatten();
4167
4168        f.retain(|v| v % 2 == 0);
4169        assert_eq!(f.get_slice(0), &[2, 4]);
4170        assert!(f.get_slice(1).is_empty());
4171        assert_eq!(f.len(), 2, "node slots survive an empty retain");
4172    }
4173
4174    #[test]
4175    fn flatvecvec_retain_dropping_everything_leaves_empty_slices() {
4176        let mut f = FlatVecVec::<i32>::new(2);
4177        f.push_to(0, 1);
4178        f.push_to(1, 2);
4179        f.flatten();
4180        f.retain(|_| false);
4181        assert_eq!(f.len(), 2);
4182        assert!(f.get_slice(0).is_empty());
4183        assert!(f.get_slice(1).is_empty());
4184    }
4185
4186    #[test]
4187    fn flatvecvec_retain_with_node_index_sees_owning_node() {
4188        let mut f = FlatVecVec::<i32>::new(3);
4189        f.push_to(0, 10);
4190        f.push_to(1, 11);
4191        f.push_to(2, 12);
4192        f.flatten();
4193
4194        f.retain_with_node_index(|idx, _| idx == 1);
4195        assert!(f.get_slice(0).is_empty());
4196        assert_eq!(f.get_slice(1), &[11]);
4197        assert!(f.get_slice(2).is_empty());
4198    }
4199
4200    #[test]
4201    fn flatvecvec_retain_with_node_index_before_flatten_is_a_noop() {
4202        let mut f = FlatVecVec::<i32>::new(1);
4203        f.push_to(0, 1);
4204        f.retain_with_node_index(|_, _| false);
4205        assert_eq!(f.get_slice(0), &[1]);
4206    }
4207
4208    // =====================================================================
4209    // FlatVecVec — iteration / extend_from
4210    // =====================================================================
4211
4212    #[test]
4213    fn flatvecvec_iter_node_slices_covers_all_nodes_in_both_phases() {
4214        let mut f = FlatVecVec::<i32>::new(3);
4215        f.push_to(1, 7);
4216
4217        let build: Vec<(usize, Vec<i32>)> = f
4218            .iter_node_slices()
4219            .map(|(i, s)| (i, s.to_vec()))
4220            .collect();
4221        assert_eq!(build, vec![(0, vec![]), (1, vec![7]), (2, vec![])]);
4222
4223        f.flatten();
4224        let flat: Vec<(usize, Vec<i32>)> = f
4225            .iter_node_slices()
4226            .map(|(i, s)| (i, s.to_vec()))
4227            .collect();
4228        assert_eq!(flat, build, "iteration is phase-independent");
4229    }
4230
4231    #[test]
4232    fn flatvecvec_iter_node_slices_on_empty_yields_nothing() {
4233        let f = FlatVecVec::<i32>::new(0);
4234        assert_eq!(f.iter_node_slices().count(), 0);
4235    }
4236
4237    #[test]
4238    fn flatvecvec_extend_from_both_in_build_phase() {
4239        let mut a = FlatVecVec::<i32>::new(1);
4240        a.push_to(0, 1);
4241        let mut b = FlatVecVec::<i32>::new(2);
4242        b.push_to(0, 2);
4243        b.push_to(1, 3);
4244
4245        a.extend_from(&mut b);
4246        assert_eq!(a.len(), 3);
4247        assert_eq!(a.get_slice(0), &[1]);
4248        assert_eq!(a.get_slice(1), &[2]);
4249        assert_eq!(a.get_slice(2), &[3]);
4250        assert_eq!(b.len(), 0, "other is drained");
4251    }
4252
4253    #[test]
4254    fn flatvecvec_extend_from_both_flattened_rebases_offsets() {
4255        let mut a = FlatVecVec::<i32>::new(2);
4256        a.push_to(0, 1);
4257        a.push_to(1, 2);
4258        a.flatten();
4259
4260        let mut b = FlatVecVec::<i32>::new(2);
4261        b.push_to(0, 3);
4262        b.push_to(1, 4);
4263        b.flatten();
4264
4265        a.extend_from(&mut b);
4266        assert_eq!(a.len(), 4);
4267        assert_eq!(a.get_slice(0), &[1]);
4268        assert_eq!(a.get_slice(1), &[2]);
4269        assert_eq!(a.get_slice(2), &[3], "offsets rebased onto a's flat data");
4270        assert_eq!(a.get_slice(3), &[4]);
4271    }
4272
4273    #[test]
4274    fn flatvecvec_extend_from_across_phases_discards_self_flat_data() {
4275        // Doc precondition: "Both must be in build phase, or both must be
4276        // flattened." This pins what a violation actually does today — the
4277        // flattened side's items are dropped on the floor rather than merged.
4278        let mut a = FlatVecVec::<i32>::new(1);
4279        a.push_to(0, 1);
4280        a.flatten();
4281
4282        let mut b = FlatVecVec::<i32>::new(1);
4283        b.push_to(0, 2);
4284
4285        a.extend_from(&mut b); // no panic...
4286        assert_eq!(a.len(), 1);
4287        assert_eq!(
4288            a.get_slice(0),
4289            &[2],
4290            "a's own flattened item (1) is silently lost"
4291        );
4292    }
4293
4294    #[test]
4295    fn flatvecvec_eq_within_the_same_phase() {
4296        let mut a = FlatVecVec::<i32>::new(1);
4297        a.push_to(0, 1);
4298        let mut b = FlatVecVec::<i32>::new(1);
4299        b.push_to(0, 1);
4300        assert_eq!(a, b);
4301
4302        b.push_to(0, 2);
4303        assert_ne!(a, b);
4304
4305        a.flatten();
4306        // (a and c are both flattened below — equality is only meaningful
4307        // between two caches in the same phase)
4308        let mut c = FlatVecVec::<i32>::new(1);
4309        c.push_to(0, 1);
4310        c.flatten();
4311        assert_eq!(a, c);
4312    }
4313
4314    // =====================================================================
4315    // CssPropertyCacheBreakdown
4316    // =====================================================================
4317
4318    #[test]
4319    fn breakdown_total_bytes_sums_subfields_and_excludes_node_count() {
4320        let b = CssPropertyCacheBreakdown {
4321            node_count: 999_999,
4322            cascaded_props_bytes: 1,
4323            css_props_bytes: 2,
4324            computed_values_bytes: 4,
4325            user_overridden_bytes: 8,
4326            global_css_props_bytes: 16,
4327            compact_cache_bytes: 32,
4328            resolved_font_sizes_bytes: 64,
4329        };
4330        assert_eq!(b.total_bytes(), 127, "node_count is not a byte count");
4331    }
4332
4333    #[test]
4334    fn breakdown_total_bytes_default_is_zero_and_max_single_field_does_not_overflow() {
4335        assert_eq!(CssPropertyCacheBreakdown::default().total_bytes(), 0);
4336
4337        let b = CssPropertyCacheBreakdown {
4338            cascaded_props_bytes: usize::MAX,
4339            ..Default::default()
4340        };
4341        assert_eq!(b.total_bytes(), usize::MAX);
4342    }
4343
4344    // =====================================================================
4345    // CssPropertyCache — construction / memory / append
4346    // =====================================================================
4347
4348    #[test]
4349    fn cache_empty_zero_is_neutral() {
4350        let c = CssPropertyCache::empty(0);
4351        assert_eq!(c.node_count, 0);
4352        assert!(c.css_props.is_empty());
4353        assert!(c.cascaded_props.is_empty());
4354        assert!(c.computed_values.is_empty());
4355        assert!(c.user_overridden_properties.is_empty());
4356        assert!(c.global_css_props.is_empty());
4357        assert!(c.compact_cache.is_none());
4358
4359        let b = c.memory_breakdown();
4360        assert_eq!(b.node_count, 0);
4361        assert_eq!(b.total_bytes(), 0, "a zero-node cache retains no heap");
4362    }
4363
4364    #[test]
4365    fn cache_empty_invariants_hold() {
4366        let c = CssPropertyCache::empty(7);
4367        assert_eq!(c.node_count, 7);
4368        assert_eq!(c.css_props.len(), 7);
4369        assert_eq!(c.cascaded_props.len(), 7);
4370        assert!(!c.css_props.is_flattened(), "starts in build phase");
4371        assert!(c.compact_cache.is_none());
4372
4373        let b = c.memory_breakdown();
4374        assert_eq!(b.node_count, 7);
4375        assert!(b.total_bytes() > 0);
4376        assert_eq!(b.compact_cache_bytes, 0);
4377        assert_eq!(b.resolved_font_sizes_bytes, 0);
4378    }
4379
4380    #[test]
4381    fn cache_invalidate_resolved_font_sizes_clears_the_once_lock() {
4382        let mut c = CssPropertyCache::empty(1);
4383        assert!(c.resolved_font_sizes_px.set(vec![16.0]).is_ok());
4384        assert!(c.resolved_font_sizes_px.get().is_some());
4385
4386        c.invalidate_resolved_font_sizes();
4387        assert!(
4388            c.resolved_font_sizes_px.get().is_none(),
4389            "next read must recompute"
4390        );
4391        // and it can be re-populated afterwards
4392        assert!(c.resolved_font_sizes_px.set(vec![12.0]).is_ok());
4393    }
4394
4395    #[test]
4396    fn cache_append_sums_nodes_and_invalidates_derived_caches() {
4397        let mut a = CssPropertyCache::empty(2);
4398        let mut b = CssPropertyCache::empty(3);
4399        assert!(a.resolved_font_sizes_px.set(vec![16.0, 16.0]).is_ok());
4400
4401        a.append(&mut b);
4402
4403        assert_eq!(a.node_count, 5);
4404        assert_eq!(a.css_props.len(), 5);
4405        assert_eq!(a.cascaded_props.len(), 5);
4406        assert!(
4407            a.resolved_font_sizes_px.get().is_none(),
4408            "node indices shifted"
4409        );
4410        assert!(a.compact_cache.is_none());
4411    }
4412
4413    #[test]
4414    fn cache_append_of_empty_cache_is_a_noop_on_node_count() {
4415        let mut a = CssPropertyCache::empty(2);
4416        let mut b = CssPropertyCache::empty(0);
4417        a.append(&mut b);
4418        assert_eq!(a.node_count, 2);
4419        assert_eq!(a.css_props.len(), 2);
4420    }
4421
4422    #[test]
4423    fn cache_invalidate_resolved_cache_drops_compact_cache() {
4424        let mut c = CssPropertyCache::empty(1);
4425        c.invalidate_resolved_cache();
4426        assert!(c.compact_cache.is_none());
4427    }
4428
4429    #[test]
4430    fn cache_ptr_new_and_downcast_roundtrip() {
4431        let mut p = CssPropertyCachePtr::new(CssPropertyCache::empty(4));
4432        assert!(p.run_destructor);
4433        assert_eq!(p.downcast_mut().node_count, 4);
4434
4435        p.downcast_mut().node_count = 9;
4436        assert_eq!(p.downcast_mut().node_count, 9, "downcast_mut aliases the box");
4437    }
4438
4439    // =====================================================================
4440    // Predicates (overflow / border / box-shadow)
4441    // =====================================================================
4442
4443    #[test]
4444    fn overflow_predicates_default_to_visible_for_a_bare_div() {
4445        let c = CssPropertyCache::empty(1);
4446        let nd = NodeData::create_div();
4447        assert!(c.is_horizontal_overflow_visible(&nd, &n0(), &normal()));
4448        assert!(c.is_vertical_overflow_visible(&nd, &n0(), &normal()));
4449        assert!(!c.is_horizontal_overflow_hidden(&nd, &n0(), &normal()));
4450        assert!(!c.is_vertical_overflow_hidden(&nd, &n0(), &normal()));
4451    }
4452
4453    #[test]
4454    fn overflow_predicates_are_per_axis() {
4455        let c = CssPropertyCache::empty(1);
4456        let nd = div_with(vec![CssProperty::OverflowX(CssPropertyValue::Exact(
4457            LayoutOverflow::Hidden,
4458        ))]);
4459        assert!(c.is_horizontal_overflow_hidden(&nd, &n0(), &normal()));
4460        assert!(!c.is_horizontal_overflow_visible(&nd, &n0(), &normal()));
4461        // the Y axis must be untouched
4462        assert!(!c.is_vertical_overflow_hidden(&nd, &n0(), &normal()));
4463        assert!(c.is_vertical_overflow_visible(&nd, &n0(), &normal()));
4464    }
4465
4466    #[test]
4467    fn overflow_predicates_do_not_panic_on_an_out_of_range_node_id() {
4468        let c = CssPropertyCache::empty(0);
4469        let nd = NodeData::create_div();
4470        let far = NodeId::new(999_999);
4471        assert!(c.is_horizontal_overflow_visible(&nd, &far, &normal()));
4472        assert!(!c.is_vertical_overflow_hidden(&nd, &far, &normal()));
4473    }
4474
4475    #[test]
4476    fn has_border_false_without_and_true_with_a_border_width() {
4477        let c = CssPropertyCache::empty(1);
4478        assert!(!c.has_border(&NodeData::create_div(), &n0(), &normal()));
4479
4480        let bordered = div_with(vec![CssProperty::BorderLeftWidth(CssPropertyValue::Exact(
4481            LayoutBorderLeftWidth {
4482                inner: PixelValue::px(2.0),
4483            },
4484        ))]);
4485        assert!(c.has_border(&bordered, &n0(), &normal()));
4486    }
4487
4488    #[test]
4489    fn has_box_shadow_false_for_a_bare_div() {
4490        let c = CssPropertyCache::empty(1);
4491        assert!(!c.has_box_shadow(&NodeData::create_div(), &n0(), &normal()));
4492        // out-of-range node id must not panic either
4493        assert!(!c.has_box_shadow(&NodeData::create_div(), &NodeId::new(500), &normal()));
4494    }
4495
4496    // =====================================================================
4497    // `*_or_default` getters
4498    // =====================================================================
4499
4500    #[test]
4501    fn or_default_getters_fall_back_to_the_css_defaults() {
4502        let c = CssPropertyCache::empty(1);
4503        let nd = NodeData::create_div();
4504
4505        assert_eq!(
4506            c.get_font_size_or_default(&nd, &n0(), &normal()),
4507            azul_css::defaults::DEFAULT_FONT_SIZE
4508        );
4509        assert_eq!(
4510            c.get_text_color_or_default(&nd, &n0(), &normal()),
4511            azul_css::defaults::DEFAULT_TEXT_COLOR
4512        );
4513
4514        let fams = c.get_font_id_or_default(&nd, &n0(), &normal());
4515        assert_eq!(fams.as_ref().len(), 1);
4516        match &fams.as_ref()[0] {
4517            StyleFontFamily::System(s) => {
4518                assert_eq!(s.as_str(), azul_css::defaults::DEFAULT_FONT_ID);
4519            }
4520            other => panic!("expected the default System font family, got {other:?}"),
4521        }
4522    }
4523
4524    #[test]
4525    fn get_font_size_or_default_prefers_the_inline_value() {
4526        let c = CssPropertyCache::empty(1);
4527        let nd = div_with(vec![font_size(PixelValue::px(42.0))]);
4528        let fs = c.get_font_size_or_default(&nd, &n0(), &normal());
4529        assert!(close(fs.inner.number.get(), 42.0));
4530        assert_eq!(fs.inner.metric, SizeMetric::Px);
4531    }
4532
4533    #[test]
4534    fn or_default_getters_survive_an_out_of_range_node_id() {
4535        let c = CssPropertyCache::empty(0);
4536        let nd = NodeData::create_div();
4537        let far = NodeId::new(usize::MAX / 2);
4538        assert_eq!(
4539            c.get_font_size_or_default(&nd, &far, &normal()),
4540            azul_css::defaults::DEFAULT_FONT_SIZE
4541        );
4542        assert_eq!(c.get_font_id_or_default(&nd, &far, &normal()).as_ref().len(), 1);
4543    }
4544
4545    // =====================================================================
4546    // calc_* (numeric: zero / negative / NaN / inf / saturation)
4547    // =====================================================================
4548
4549    #[test]
4550    fn calc_width_is_zero_when_unset() {
4551        let c = CssPropertyCache::empty(1);
4552        let nd = NodeData::create_div();
4553        assert_eq!(c.calc_width(&nd, &n0(), &normal(), 800.0), 0.0);
4554        assert_eq!(c.calc_width(&nd, &n0(), &normal(), 0.0), 0.0);
4555        assert_eq!(c.calc_height(&nd, &n0(), &normal(), f32::NAN), 0.0);
4556    }
4557
4558    #[test]
4559    fn calc_width_resolves_px_and_percent() {
4560        let c = CssPropertyCache::empty(1);
4561
4562        let px = div_with(vec![width_px(100.0)]);
4563        assert!(close(c.calc_width(&px, &n0(), &normal(), 800.0), 100.0));
4564        // px must ignore the reference entirely
4565        assert!(close(c.calc_width(&px, &n0(), &normal(), 0.0), 100.0));
4566
4567        let pct = div_with(vec![width_pct(50.0)]);
4568        assert!(close(c.calc_width(&pct, &n0(), &normal(), 800.0), 400.0));
4569        assert!(close(c.calc_width(&pct, &n0(), &normal(), 0.0), 0.0));
4570    }
4571
4572    #[test]
4573    fn calc_width_with_a_negative_reference_is_negative_not_clamped() {
4574        let c = CssPropertyCache::empty(1);
4575        let pct = div_with(vec![width_pct(50.0)]);
4576        assert!(close(c.calc_width(&pct, &n0(), &normal(), -800.0), -400.0));
4577    }
4578
4579    #[test]
4580    fn calc_width_with_nan_and_infinite_references_is_defined() {
4581        let c = CssPropertyCache::empty(1);
4582        let pct = div_with(vec![width_pct(50.0)]);
4583
4584        assert!(c.calc_width(&pct, &n0(), &normal(), f32::NAN).is_nan());
4585        assert_eq!(
4586            c.calc_width(&pct, &n0(), &normal(), f32::INFINITY),
4587            f32::INFINITY
4588        );
4589        assert_eq!(
4590            c.calc_width(&pct, &n0(), &normal(), f32::NEG_INFINITY),
4591            f32::NEG_INFINITY
4592        );
4593    }
4594
4595    #[test]
4596    fn calc_width_saturates_non_finite_pixel_values_at_construction() {
4597        let c = CssPropertyCache::empty(1);
4598
4599        // PixelValue stores a fixed-point isize, so `as isize` saturates:
4600        // NaN => 0, +inf => isize::MAX, -inf => isize::MIN. Nothing panics and
4601        // nothing leaks a NaN into layout.
4602        let nan = div_with(vec![width_px(f32::NAN)]);
4603        assert_eq!(c.calc_width(&nan, &n0(), &normal(), 800.0), 0.0);
4604
4605        let inf = div_with(vec![width_px(f32::INFINITY)]);
4606        let got = c.calc_width(&inf, &n0(), &normal(), 800.0);
4607        assert!(got.is_finite() && got > 0.0, "saturated, got {got}");
4608
4609        let neg_inf = div_with(vec![width_px(f32::NEG_INFINITY)]);
4610        let got = c.calc_width(&neg_inf, &n0(), &normal(), 800.0);
4611        assert!(got.is_finite() && got < 0.0, "saturated, got {got}");
4612
4613        let huge = div_with(vec![width_px(f32::MAX)]);
4614        assert!(c.calc_width(&huge, &n0(), &normal(), 800.0).is_finite());
4615    }
4616
4617    #[test]
4618    fn calc_width_of_auto_and_intrinsic_keywords_is_zero() {
4619        let c = CssPropertyCache::empty(1);
4620
4621        let auto = div_with(vec![CssProperty::Width(CssPropertyValue::Auto)]);
4622        assert_eq!(c.calc_width(&auto, &n0(), &normal(), 800.0), 0.0);
4623
4624        // min-content/max-content are not resolvable here; documented as 0.0.
4625        let min_content = div_with(vec![CssProperty::Width(CssPropertyValue::Exact(
4626            LayoutWidth::MinContent,
4627        ))]);
4628        assert_eq!(c.calc_width(&min_content, &n0(), &normal(), 800.0), 0.0);
4629    }
4630
4631    #[test]
4632    fn calc_height_mirrors_calc_width() {
4633        let c = CssPropertyCache::empty(1);
4634        let nd = div_with(vec![CssProperty::Height(CssPropertyValue::Exact(
4635            LayoutHeight::Px(PixelValue::percent(25.0)),
4636        ))]);
4637        assert!(close(c.calc_height(&nd, &n0(), &normal(), 400.0), 100.0));
4638        assert!(c.calc_height(&nd, &n0(), &normal(), f32::NAN).is_nan());
4639    }
4640
4641    #[test]
4642    fn calc_min_width_defaults_to_zero_and_max_width_defaults_to_none() {
4643        let c = CssPropertyCache::empty(1);
4644        let nd = NodeData::create_div();
4645
4646        assert_eq!(c.calc_min_width(&nd, &n0(), &normal(), 800.0), 0.0);
4647        assert_eq!(c.calc_min_height(&nd, &n0(), &normal(), 600.0), 0.0);
4648        assert_eq!(c.calc_max_width(&nd, &n0(), &normal(), 800.0), None);
4649        assert_eq!(c.calc_max_height(&nd, &n0(), &normal(), 600.0), None);
4650    }
4651
4652    #[test]
4653    fn calc_min_max_width_resolve_percentages_and_propagate_nan() {
4654        let c = CssPropertyCache::empty(1);
4655        let nd = div_with(vec![
4656            CssProperty::MinWidth(CssPropertyValue::Exact(LayoutMinWidth {
4657                inner: PixelValue::percent(10.0),
4658            })),
4659            CssProperty::MaxWidth(CssPropertyValue::Exact(LayoutMaxWidth {
4660                inner: PixelValue::percent(90.0),
4661            })),
4662        ]);
4663
4664        assert!(close(c.calc_min_width(&nd, &n0(), &normal(), 1000.0), 100.0));
4665        assert!(close(
4666            c.calc_max_width(&nd, &n0(), &normal(), 1000.0).unwrap(),
4667            900.0
4668        ));
4669        assert!(c.calc_min_width(&nd, &n0(), &normal(), f32::NAN).is_nan());
4670        assert!(c
4671            .calc_max_width(&nd, &n0(), &normal(), f32::NAN)
4672            .unwrap()
4673            .is_nan());
4674    }
4675
4676    #[test]
4677    fn calc_inset_getters_are_none_when_unset_and_some_when_set() {
4678        let c = CssPropertyCache::empty(1);
4679        let bare = NodeData::create_div();
4680        assert_eq!(c.calc_left(&bare, &n0(), &normal(), 800.0), None);
4681        assert_eq!(c.calc_right(&bare, &n0(), &normal(), 800.0), None);
4682        assert_eq!(c.calc_top(&bare, &n0(), &normal(), 600.0), None);
4683        assert_eq!(c.calc_bottom(&bare, &n0(), &normal(), 600.0), None);
4684
4685        let inset = div_with(vec![
4686            CssProperty::Left(CssPropertyValue::Exact(LayoutLeft {
4687                inner: PixelValue::px(5.0),
4688            })),
4689            CssProperty::Right(CssPropertyValue::Exact(LayoutRight {
4690                inner: PixelValue::percent(10.0),
4691            })),
4692            CssProperty::Top(CssPropertyValue::Exact(LayoutTop {
4693                inner: PixelValue::px(-7.0),
4694            })),
4695            CssProperty::Bottom(CssPropertyValue::Exact(LayoutInsetBottom {
4696                inner: PixelValue::px(0.0),
4697            })),
4698        ]);
4699        assert!(close(c.calc_left(&inset, &n0(), &normal(), 800.0).unwrap(), 5.0));
4700        assert!(close(
4701            c.calc_right(&inset, &n0(), &normal(), 800.0).unwrap(),
4702            80.0
4703        ));
4704        assert!(close(
4705            c.calc_top(&inset, &n0(), &normal(), 600.0).unwrap(),
4706            -7.0
4707        ));
4708        assert_eq!(c.calc_bottom(&inset, &n0(), &normal(), 600.0), Some(0.0));
4709    }
4710
4711    #[test]
4712    fn calc_padding_margin_border_default_to_zero() {
4713        let c = CssPropertyCache::empty(1);
4714        let nd = NodeData::create_div();
4715        assert_eq!(c.calc_padding_left(&nd, &n0(), &normal(), 800.0), 0.0);
4716        assert_eq!(c.calc_padding_right(&nd, &n0(), &normal(), 800.0), 0.0);
4717        assert_eq!(c.calc_padding_top(&nd, &n0(), &normal(), 600.0), 0.0);
4718        assert_eq!(c.calc_padding_bottom(&nd, &n0(), &normal(), 600.0), 0.0);
4719        assert_eq!(c.calc_margin_left(&nd, &n0(), &normal(), 800.0), 0.0);
4720        assert_eq!(c.calc_margin_right(&nd, &n0(), &normal(), 800.0), 0.0);
4721        assert_eq!(c.calc_margin_top(&nd, &n0(), &normal(), 600.0), 0.0);
4722        assert_eq!(c.calc_margin_bottom(&nd, &n0(), &normal(), 600.0), 0.0);
4723        assert_eq!(c.calc_border_left_width(&nd, &n0(), &normal(), 800.0), 0.0);
4724        assert_eq!(c.calc_border_right_width(&nd, &n0(), &normal(), 800.0), 0.0);
4725        assert_eq!(c.calc_border_top_width(&nd, &n0(), &normal(), 600.0), 0.0);
4726        assert_eq!(c.calc_border_bottom_width(&nd, &n0(), &normal(), 600.0), 0.0);
4727    }
4728
4729    #[test]
4730    fn calc_padding_em_uses_the_default_font_size_not_the_reference() {
4731        // `calc_*` passes DEFAULT_FONT_SIZE (16px) as both em and rem resolvers,
4732        // so an em padding must be invariant under the reference width.
4733        let c = CssPropertyCache::empty(1);
4734        let nd = div_with(vec![CssProperty::PaddingLeft(CssPropertyValue::Exact(
4735            LayoutPaddingLeft {
4736                inner: PixelValue::em(2.0),
4737            },
4738        ))]);
4739        assert!(close(c.calc_padding_left(&nd, &n0(), &normal(), 800.0), 32.0));
4740        assert!(close(c.calc_padding_left(&nd, &n0(), &normal(), 0.0), 32.0));
4741        assert!(close(
4742            c.calc_padding_left(&nd, &n0(), &normal(), f32::NAN),
4743            32.0
4744        ));
4745    }
4746
4747    #[test]
4748    fn calc_margin_and_border_resolve_px_and_percent() {
4749        let c = CssPropertyCache::empty(1);
4750        let nd = div_with(vec![
4751            CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
4752                inner: PixelValue::percent(50.0),
4753            })),
4754            CssProperty::BorderLeftWidth(CssPropertyValue::Exact(LayoutBorderLeftWidth {
4755                inner: PixelValue::px(3.0),
4756            })),
4757        ]);
4758        assert!(close(c.calc_margin_top(&nd, &n0(), &normal(), 200.0), 100.0));
4759        assert!(close(
4760            c.calc_border_left_width(&nd, &n0(), &normal(), 800.0),
4761            3.0
4762        ));
4763        assert!(c.calc_margin_top(&nd, &n0(), &normal(), f32::NAN).is_nan());
4764    }
4765
4766    #[test]
4767    fn calc_getters_do_not_panic_on_an_out_of_range_node_id() {
4768        let c = CssPropertyCache::empty(0);
4769        let nd = NodeData::create_div();
4770        let far = NodeId::new(usize::MAX / 2);
4771        assert_eq!(c.calc_width(&nd, &far, &normal(), 800.0), 0.0);
4772        assert_eq!(c.calc_max_height(&nd, &far, &normal(), 600.0), None);
4773        assert_eq!(c.calc_padding_top(&nd, &far, &normal(), f32::INFINITY), 0.0);
4774    }
4775
4776    // =====================================================================
4777    // property_needs_slow_path_after_compact
4778    // =====================================================================
4779
4780    #[test]
4781    fn slow_path_only_needed_for_non_px_pixel_values() {
4782        // px round-trips through the compact cache => no slow path
4783        assert!(!property_needs_slow_path_after_compact(&width_px(10.0)));
4784        // % encodes to SENTINEL => must survive the prune
4785        assert!(property_needs_slow_path_after_compact(&width_pct(50.0)));
4786
4787        assert!(!property_needs_slow_path_after_compact(&CssProperty::Height(
4788            CssPropertyValue::Exact(LayoutHeight::Px(PixelValue::px(1.0)))
4789        )));
4790        assert!(property_needs_slow_path_after_compact(&CssProperty::Height(
4791            CssPropertyValue::Exact(LayoutHeight::Px(PixelValue::em(1.0)))
4792        )));
4793    }
4794
4795    #[test]
4796    fn slow_path_covers_the_plain_pixelvalue_wrappers() {
4797        assert!(property_needs_slow_path_after_compact(&font_size(
4798            PixelValue::rem(2.0)
4799        )));
4800        assert!(!property_needs_slow_path_after_compact(&font_size(
4801            PixelValue::px(16.0)
4802        )));
4803
4804        assert!(property_needs_slow_path_after_compact(
4805            &CssProperty::MinWidth(CssPropertyValue::Exact(LayoutMinWidth {
4806                inner: PixelValue::percent(10.0),
4807            }))
4808        ));
4809        assert!(!property_needs_slow_path_after_compact(
4810            &CssProperty::PaddingLeft(CssPropertyValue::Exact(LayoutPaddingLeft {
4811                inner: PixelValue::px(4.0),
4812            }))
4813        ));
4814    }
4815
4816    #[test]
4817    fn slow_path_handles_flex_basis_and_non_pixel_properties() {
4818        assert!(property_needs_slow_path_after_compact(
4819            &CssProperty::FlexBasis(CssPropertyValue::Exact(LayoutFlexBasis::Exact(
4820                PixelValue::percent(50.0)
4821            )))
4822        ));
4823        assert!(!property_needs_slow_path_after_compact(
4824            &CssProperty::FlexBasis(CssPropertyValue::Exact(LayoutFlexBasis::Auto))
4825        ));
4826
4827        // Non-Exact keywords and non-pixel properties never need the slow path.
4828        assert!(!property_needs_slow_path_after_compact(&CssProperty::Width(
4829            CssPropertyValue::Auto
4830        )));
4831        assert!(!property_needs_slow_path_after_compact(
4832            &CssProperty::const_none(CssPropertyType::Display)
4833        ));
4834        assert!(!property_needs_slow_path_after_compact(
4835            &CssProperty::const_none(CssPropertyType::BackgroundContent)
4836        ));
4837    }
4838
4839    // =====================================================================
4840    // clone_inheritable_property (round-trip)
4841    // =====================================================================
4842
4843    #[test]
4844    fn clone_inheritable_property_round_trips_heap_and_pod_variants() {
4845        // The whole point of this hand-rolled clone is that it must be
4846        // byte-equivalent to the derived Clone on native.
4847        let font_family = CssProperty::FontFamily(CssPropertyValue::Exact(
4848            vec![StyleFontFamily::System(AzString::from_const_str("serif"))].into(),
4849        ));
4850        assert_eq!(clone_inheritable_property(&font_family), font_family);
4851
4852        for p in [
4853            CssProperty::const_none(CssPropertyType::Cursor),
4854            CssProperty::const_none(CssPropertyType::TextColor),
4855            CssProperty::const_none(CssPropertyType::BackgroundContent),
4856            CssProperty::const_none(CssPropertyType::Transform),
4857            CssProperty::const_none(CssPropertyType::Content),
4858            width_px(3.0),
4859            font_size(PixelValue::em(1.5)),
4860        ] {
4861            assert_eq!(clone_inheritable_property(&p), p, "clone must be identity");
4862            assert_eq!(clone_inheritable_property(&p).get_type(), p.get_type());
4863        }
4864    }
4865
4866    // =====================================================================
4867    // find_in_stateful / has_state_props / prop_types_for_state
4868    // =====================================================================
4869
4870    fn sorted_stateful_fixture() -> Vec<StatefulCssProperty> {
4871        let mut v = vec![
4872            stateful(PseudoStateType::Normal, width_px(1.0)),
4873            stateful(
4874                PseudoStateType::Normal,
4875                CssProperty::const_none(CssPropertyType::Display),
4876            ),
4877            stateful(PseudoStateType::Hover, width_px(2.0)),
4878        ];
4879        // The lookup helpers require (state, prop_type) sort order.
4880        v.sort_by_key(|p| (p.state, p.prop_type));
4881        v
4882    }
4883
4884    #[test]
4885    fn find_in_stateful_on_an_empty_slice_is_none() {
4886        assert!(CssPropertyCache::find_in_stateful(
4887            &[],
4888            PseudoStateType::Normal,
4889            &CssPropertyType::Width
4890        )
4891        .is_none());
4892    }
4893
4894    #[test]
4895    fn find_in_stateful_is_keyed_on_both_state_and_prop_type() {
4896        let v = sorted_stateful_fixture();
4897
4898        let normal_width =
4899            CssPropertyCache::find_in_stateful(&v, PseudoStateType::Normal, &CssPropertyType::Width)
4900                .expect("normal width present");
4901        assert_eq!(normal_width.get_type(), CssPropertyType::Width);
4902
4903        let hover_width =
4904            CssPropertyCache::find_in_stateful(&v, PseudoStateType::Hover, &CssPropertyType::Width)
4905                .expect("hover width present");
4906        // same prop type, different state => a different entry
4907        assert_ne!(normal_width, hover_width);
4908
4909        // present prop type, absent state
4910        assert!(CssPropertyCache::find_in_stateful(
4911            &v,
4912            PseudoStateType::Focus,
4913            &CssPropertyType::Width
4914        )
4915        .is_none());
4916        // present state, absent prop type
4917        assert!(CssPropertyCache::find_in_stateful(
4918            &v,
4919            PseudoStateType::Hover,
4920            &CssPropertyType::Display
4921        )
4922        .is_none());
4923    }
4924
4925    #[test]
4926    fn has_state_props_true_false_and_edges() {
4927        let v = sorted_stateful_fixture();
4928        assert!(CssPropertyCache::has_state_props(&v, PseudoStateType::Normal));
4929        assert!(CssPropertyCache::has_state_props(&v, PseudoStateType::Hover));
4930        assert!(!CssPropertyCache::has_state_props(&v, PseudoStateType::Focus));
4931        // empty slice: deterministic false, no partition_point OOB read
4932        assert!(!CssPropertyCache::has_state_props(
4933            &[],
4934            PseudoStateType::Normal
4935        ));
4936    }
4937
4938    #[test]
4939    fn prop_types_for_state_filters_by_state() {
4940        let v = sorted_stateful_fixture();
4941
4942        let mut normal: Vec<CssPropertyType> =
4943            CssPropertyCache::prop_types_for_state(&v, PseudoStateType::Normal)
4944                .copied()
4945                .collect();
4946        normal.sort_unstable();
4947        assert_eq!(normal.len(), 2);
4948        assert!(normal.contains(&CssPropertyType::Width));
4949        assert!(normal.contains(&CssPropertyType::Display));
4950
4951        let hover: Vec<CssPropertyType> =
4952            CssPropertyCache::prop_types_for_state(&v, PseudoStateType::Hover)
4953                .copied()
4954                .collect();
4955        assert_eq!(hover, vec![CssPropertyType::Width]);
4956
4957        assert_eq!(
4958            CssPropertyCache::prop_types_for_state(&v, PseudoStateType::Active).count(),
4959            0
4960        );
4961        assert_eq!(
4962            CssPropertyCache::prop_types_for_state(&[], PseudoStateType::Normal).count(),
4963            0
4964        );
4965    }
4966
4967    // =====================================================================
4968    // font-size resolution (numeric)
4969    // =====================================================================
4970
4971    #[test]
4972    fn resolve_font_size_to_pixels_converts_absolute_units() {
4973        let px = CssPropertyCache::resolve_font_size_to_pixels(&font_size(PixelValue::px(20.0)), 10.0);
4974        let (metric, n) = font_size_parts(&px).unwrap();
4975        assert_eq!(metric, SizeMetric::Px);
4976        assert!(close(n, 20.0));
4977
4978        let pt = CssPropertyCache::resolve_font_size_to_pixels(&font_size(PixelValue::pt(12.0)), 10.0);
4979        assert!(close(font_size_parts(&pt).unwrap().1, 12.0 * PT_TO_PX));
4980    }
4981
4982    #[test]
4983    fn resolve_font_size_to_pixels_em_scales_by_reference_but_rem_does_not() {
4984        let em =
4985            CssPropertyCache::resolve_font_size_to_pixels(&font_size(PixelValue::em(2.0)), 10.0);
4986        assert!(close(font_size_parts(&em).unwrap().1, 20.0));
4987
4988        // rem deliberately ignores the reference and uses DEFAULT_FONT_SIZE (16).
4989        let rem =
4990            CssPropertyCache::resolve_font_size_to_pixels(&font_size(PixelValue::rem(2.0)), 10.0);
4991        assert!(close(font_size_parts(&rem).unwrap().1, 32.0));
4992
4993        let pct = CssPropertyCache::resolve_font_size_to_pixels(
4994            &font_size(PixelValue::percent(50.0)),
4995            10.0,
4996        );
4997        assert!(close(font_size_parts(&pct).unwrap().1, 5.0));
4998    }
4999
5000    #[test]
5001    fn resolve_font_size_to_pixels_with_nan_and_infinite_references() {
5002        // NaN * anything => NaN => saturates to 0 in the fixed-point encoding.
5003        let nan =
5004            CssPropertyCache::resolve_font_size_to_pixels(&font_size(PixelValue::em(2.0)), f32::NAN);
5005        let (metric, n) = font_size_parts(&nan).unwrap();
5006        assert_eq!(metric, SizeMetric::Px);
5007        assert_eq!(n, 0.0, "NaN must not escape into the cascade");
5008
5009        let inf = CssPropertyCache::resolve_font_size_to_pixels(
5010            &font_size(PixelValue::em(2.0)),
5011            f32::INFINITY,
5012        );
5013        let n = font_size_parts(&inf).unwrap().1;
5014        assert!(n.is_finite() && n > 0.0, "saturated, got {n}");
5015
5016        let zero =
5017            CssPropertyCache::resolve_font_size_to_pixels(&font_size(PixelValue::em(2.0)), 0.0);
5018        assert_eq!(font_size_parts(&zero).unwrap().1, 0.0);
5019
5020        let neg =
5021            CssPropertyCache::resolve_font_size_to_pixels(&font_size(PixelValue::em(2.0)), -10.0);
5022        assert!(close(font_size_parts(&neg).unwrap().1, -20.0));
5023    }
5024
5025    #[test]
5026    fn resolve_font_size_to_pixels_passes_through_unresolvable_inputs() {
5027        // viewport units need a viewport => returned unchanged
5028        let vw = font_size(PixelValue::from_metric(SizeMetric::Vw, 10.0));
5029        assert_eq!(CssPropertyCache::resolve_font_size_to_pixels(&vw, 16.0), vw);
5030
5031        // a non-font-size property is returned verbatim
5032        let w = width_px(10.0);
5033        assert_eq!(CssPropertyCache::resolve_font_size_to_pixels(&w, 16.0), w);
5034
5035        // a keyword (non-Exact) font-size has no PixelValue to convert
5036        let inherit = CssProperty::FontSize(CssPropertyValue::Inherit);
5037        assert_eq!(
5038            CssPropertyCache::resolve_font_size_to_pixels(&inherit, 16.0),
5039            inherit
5040        );
5041    }
5042
5043    #[test]
5044    fn has_relative_font_size_unit_true_false_and_edges() {
5045        assert!(CssPropertyCache::has_relative_font_size_unit(&font_size(
5046            PixelValue::em(1.0)
5047        )));
5048        assert!(CssPropertyCache::has_relative_font_size_unit(&font_size(
5049            PixelValue::rem(1.0)
5050        )));
5051        assert!(CssPropertyCache::has_relative_font_size_unit(&font_size(
5052            PixelValue::percent(100.0)
5053        )));
5054
5055        assert!(!CssPropertyCache::has_relative_font_size_unit(&font_size(
5056            PixelValue::px(16.0)
5057        )));
5058        assert!(!CssPropertyCache::has_relative_font_size_unit(&font_size(
5059            PixelValue::pt(12.0)
5060        )));
5061        // keyword font-size and non-font-size properties are not "relative"
5062        assert!(!CssPropertyCache::has_relative_font_size_unit(
5063            &CssProperty::FontSize(CssPropertyValue::Auto)
5064        ));
5065        assert!(!CssPropertyCache::has_relative_font_size_unit(&width_px(1.0)));
5066    }
5067
5068    // =====================================================================
5069    // resolve_property_dependency
5070    // =====================================================================
5071
5072    #[test]
5073    fn resolve_property_dependency_scales_relative_targets_by_an_absolute_reference() {
5074        let reference = font_size(PixelValue::px(10.0));
5075
5076        let em = CssPropertyCache::resolve_property_dependency(
5077            &font_size(PixelValue::em(2.0)),
5078            &reference,
5079        )
5080        .expect("em resolves against an absolute reference");
5081        assert!(close(font_size_parts(&em).unwrap().1, 20.0));
5082
5083        let pct = CssPropertyCache::resolve_property_dependency(
5084            &font_size(PixelValue::percent(50.0)),
5085            &reference,
5086        )
5087        .expect("percent resolves");
5088        assert!(close(font_size_parts(&pct).unwrap().1, 5.0));
5089
5090        // The reference itself may be in any absolute unit.
5091        let pt_ref = font_size(PixelValue::pt(10.0));
5092        let em2 =
5093            CssPropertyCache::resolve_property_dependency(&font_size(PixelValue::em(2.0)), &pt_ref)
5094                .expect("pt reference is absolute");
5095        assert!(close(
5096            font_size_parts(&em2).unwrap().1,
5097            2.0 * 10.0 * PT_TO_PX
5098        ));
5099    }
5100
5101    #[test]
5102    fn resolve_property_dependency_rewrites_the_target_variant_in_place() {
5103        let reference = font_size(PixelValue::px(10.0));
5104        let padding = CssProperty::PaddingLeft(CssPropertyValue::Exact(LayoutPaddingLeft {
5105            inner: PixelValue::em(3.0),
5106        }));
5107        let out = CssPropertyCache::resolve_property_dependency(&padding, &reference)
5108            .expect("padding is a supported target");
5109        match out {
5110            CssProperty::PaddingLeft(v) => {
5111                let inner = v.get_property().unwrap().inner;
5112                assert_eq!(inner.metric, SizeMetric::Px);
5113                assert!(close(inner.number.get(), 30.0));
5114            }
5115            other => panic!("variant must be preserved, got {other:?}"),
5116        }
5117    }
5118
5119    #[test]
5120    fn resolve_property_dependency_returns_none_for_unresolvable_inputs() {
5121        let abs = font_size(PixelValue::px(10.0));
5122
5123        // a relative reference cannot anchor anything
5124        assert!(CssPropertyCache::resolve_property_dependency(
5125            &font_size(PixelValue::em(2.0)),
5126            &font_size(PixelValue::em(2.0))
5127        )
5128        .is_none());
5129        // viewport-unit target needs a viewport
5130        assert!(CssPropertyCache::resolve_property_dependency(
5131            &font_size(PixelValue::from_metric(SizeMetric::Vh, 5.0)),
5132            &abs
5133        )
5134        .is_none());
5135        // unsupported target type (no PixelValue to extract)
5136        assert!(CssPropertyCache::resolve_property_dependency(&width_px(5.0), &abs).is_none());
5137        // unsupported reference type
5138        assert!(CssPropertyCache::resolve_property_dependency(
5139            &font_size(PixelValue::em(2.0)),
5140            &width_px(5.0)
5141        )
5142        .is_none());
5143        // keyword (non-Exact) target
5144        assert!(CssPropertyCache::resolve_property_dependency(
5145            &CssProperty::FontSize(CssPropertyValue::Inherit),
5146            &abs
5147        )
5148        .is_none());
5149    }
5150
5151    // =====================================================================
5152    // should_apply_cascaded
5153    // =====================================================================
5154
5155    #[test]
5156    fn should_apply_cascaded_respects_origin_and_relative_font_sizes() {
5157        let own = |p: CssProperty| {
5158            vec![(
5159                p.get_type(),
5160                CssPropertyWithOrigin {
5161                    property: p,
5162                    origin: CssPropertyOrigin::Own,
5163                },
5164            )]
5165        };
5166        let inherited = |p: CssProperty| {
5167            vec![(
5168                p.get_type(),
5169                CssPropertyWithOrigin {
5170                    property: p,
5171                    origin: CssPropertyOrigin::Inherited,
5172                },
5173            )]
5174        };
5175
5176        // nothing computed yet => apply
5177        assert!(CssPropertyCache::should_apply_cascaded(
5178            &[],
5179            CssPropertyType::Width,
5180            &width_px(1.0)
5181        ));
5182
5183        // the node already set it itself => the UA/cascaded value must not win
5184        assert!(!CssPropertyCache::should_apply_cascaded(
5185            &own(width_px(2.0)),
5186            CssPropertyType::Width,
5187            &width_px(1.0)
5188        ));
5189
5190        // an inherited value is weaker than a cascaded one => apply
5191        assert!(CssPropertyCache::should_apply_cascaded(
5192            &inherited(width_px(2.0)),
5193            CssPropertyType::Width,
5194            &width_px(1.0)
5195        ));
5196
5197        // A cascaded (UA/author) font-size — relative OR absolute — overrides an
5198        // inherited value: it is the node's own declared size (e.g. <h1>'s UA
5199        // `font-size: 2em`), and `resolve_font_size_property` resolves the `em`
5200        // against the parent's size, so there is no double-scaling.
5201        let inherited_fs = inherited(font_size(PixelValue::px(20.0)));
5202        assert!(CssPropertyCache::should_apply_cascaded(
5203            &inherited_fs,
5204            CssPropertyType::FontSize,
5205            &font_size(PixelValue::em(2.0))
5206        ));
5207        assert!(CssPropertyCache::should_apply_cascaded(
5208            &inherited_fs,
5209            CssPropertyType::FontSize,
5210            &font_size(PixelValue::px(12.0))
5211        ));
5212    }
5213
5214    // =====================================================================
5215    // get_property / get_property_slow (cascade layering)
5216    // =====================================================================
5217
5218    #[test]
5219    fn get_property_finds_an_inline_normal_property() {
5220        let c = CssPropertyCache::empty(1);
5221        let nd = div_with(vec![width_px(100.0)]);
5222        let got = c
5223            .get_property(&nd, &n0(), &normal(), &CssPropertyType::Width)
5224            .expect("inline width");
5225        assert_eq!(*got, width_px(100.0));
5226    }
5227
5228    #[test]
5229    fn get_property_ignores_pseudo_state_props_unless_the_state_is_active() {
5230        let c = CssPropertyCache::empty(1);
5231        let nd = div_with_pseudo(vec![width_px(100.0)], PseudoStateType::Hover);
5232
5233        assert!(
5234            c.get_property(&nd, &n0(), &normal(), &CssPropertyType::Width)
5235                .is_none(),
5236            ":hover width must not leak into the Normal state"
5237        );
5238
5239        let hovered = StyledNodeState {
5240            hover: true,
5241            ..StyledNodeState::default()
5242        };
5243        assert_eq!(
5244            c.get_property(&nd, &n0(), &hovered, &CssPropertyType::Width),
5245            Some(&width_px(100.0))
5246        );
5247    }
5248
5249    #[test]
5250    fn get_property_user_override_beats_inline_and_stylesheet() {
5251        let mut c = CssPropertyCache::empty(1);
5252        c.user_overridden_properties
5253            .push(vec![(CssPropertyType::Width, width_px(1.0))]);
5254        c.css_props
5255            .push_to(0, stateful(PseudoStateType::Normal, width_px(2.0)));
5256        c.css_props.sort_each_and_flatten(|p| (p.state, p.prop_type));
5257
5258        let nd = div_with(vec![width_px(3.0)]);
5259        assert_eq!(
5260            c.get_property(&nd, &n0(), &normal(), &CssPropertyType::Width),
5261            Some(&width_px(1.0)),
5262            "user override is the top cascade layer"
5263        );
5264    }
5265
5266    #[test]
5267    fn get_property_falls_back_through_stylesheet_global_cascaded_then_ua() {
5268        let nd = NodeData::create_div();
5269
5270        // stylesheet layer
5271        let mut c = CssPropertyCache::empty(1);
5272        c.css_props
5273            .push_to(0, stateful(PseudoStateType::Normal, width_px(2.0)));
5274        c.css_props.sort_each_and_flatten(|p| (p.state, p.prop_type));
5275        assert_eq!(
5276            c.get_property(&nd, &n0(), &normal(), &CssPropertyType::Width),
5277            Some(&width_px(2.0))
5278        );
5279
5280        // `*` global layer (below per-node rules)
5281        let mut c = CssPropertyCache::empty(1);
5282        c.global_css_props.push(width_px(4.0));
5283        assert_eq!(
5284            c.get_property(&nd, &n0(), &normal(), &CssPropertyType::Width),
5285            Some(&width_px(4.0))
5286        );
5287
5288        // cascaded (inherited/UA) layer
5289        let mut c = CssPropertyCache::empty(1);
5290        c.cascaded_props
5291            .push_to(0, stateful(PseudoStateType::Normal, width_px(5.0)));
5292        c.cascaded_props
5293            .sort_each_and_flatten(|p| (p.state, p.prop_type));
5294        assert_eq!(
5295            c.get_property(&nd, &n0(), &normal(), &CssPropertyType::Width),
5296            Some(&width_px(5.0))
5297        );
5298
5299        // UA fallback: a <div> has no UA width, but it does have `display: block`
5300        let c = CssPropertyCache::empty(1);
5301        assert!(c
5302            .get_property(&nd, &n0(), &normal(), &CssPropertyType::Width)
5303            .is_none());
5304        assert!(c
5305            .get_property(&nd, &n0(), &normal(), &CssPropertyType::Display)
5306            .is_some());
5307    }
5308
5309    #[test]
5310    fn get_property_on_an_out_of_range_node_id_falls_through_to_ua_css() {
5311        let c = CssPropertyCache::empty(0);
5312        let nd = NodeData::create_div();
5313        let far = NodeId::new(usize::MAX / 2);
5314
5315        assert!(c
5316            .get_property(&nd, &far, &normal(), &CssPropertyType::Width)
5317            .is_none());
5318        assert!(
5319            c.get_property(&nd, &far, &normal(), &CssPropertyType::Display)
5320                .is_some(),
5321            "UA CSS is node-type-keyed, not index-keyed"
5322        );
5323    }
5324
5325    #[test]
5326    fn get_property_with_context_matches_pseudo_state_conditions() {
5327        let c = CssPropertyCache::empty(1);
5328        let nd = div_with_pseudo(vec![width_px(100.0)], PseudoStateType::Hover);
5329
5330        let plain = DynamicSelectorContext::default();
5331        assert!(c
5332            .get_property_with_context(&nd, &n0(), &plain, &CssPropertyType::Width)
5333            .is_none());
5334
5335        let mut hovered = DynamicSelectorContext::default();
5336        hovered.pseudo_state.hover = true;
5337        assert_eq!(
5338            c.get_property_with_context(&nd, &n0(), &hovered, &CssPropertyType::Width),
5339            Some(&width_px(100.0))
5340        );
5341    }
5342
5343    #[test]
5344    fn check_properties_changed_only_fires_when_a_condition_flips() {
5345        let plain = DynamicSelectorContext::default();
5346        let mut hovered = DynamicSelectorContext::default();
5347        hovered.pseudo_state.hover = true;
5348
5349        // unconditional props never "change" between contexts
5350        let unconditional = div_with(vec![width_px(1.0)]);
5351        assert!(!CssPropertyCache::check_properties_changed(
5352            &unconditional,
5353            &plain,
5354            &hovered
5355        ));
5356
5357        let conditional = div_with_pseudo(vec![width_px(1.0)], PseudoStateType::Hover);
5358        assert!(CssPropertyCache::check_properties_changed(
5359            &conditional,
5360            &plain,
5361            &hovered
5362        ));
5363        assert!(
5364            !CssPropertyCache::check_properties_changed(&conditional, &plain, &plain),
5365            "identical contexts can never differ"
5366        );
5367
5368        // a node with no inline style at all
5369        assert!(!CssPropertyCache::check_properties_changed(
5370            &NodeData::create_div(),
5371            &plain,
5372            &hovered
5373        ));
5374    }
5375
5376    #[test]
5377    fn check_layout_properties_changed_ignores_non_layout_properties() {
5378        let plain = DynamicSelectorContext::default();
5379        let mut hovered = DynamicSelectorContext::default();
5380        hovered.pseudo_state.hover = true;
5381
5382        let layout = div_with_pseudo(vec![width_px(1.0)], PseudoStateType::Hover);
5383        assert!(CssPropertyCache::check_layout_properties_changed(
5384            &layout, &plain, &hovered
5385        ));
5386        assert!(CssPropertyType::Width.can_trigger_relayout());
5387
5388        // A paint-only property flipping must not force a relayout.
5389        let paint = div_with_pseudo(
5390            vec![CssProperty::const_none(CssPropertyType::BackgroundContent)],
5391            PseudoStateType::Hover,
5392        );
5393        assert!(!CssPropertyType::BackgroundContent.can_trigger_relayout());
5394        assert!(!CssPropertyCache::check_layout_properties_changed(
5395            &paint, &plain, &hovered
5396        ));
5397        // ...though the generic check still sees it
5398        assert!(CssPropertyCache::check_properties_changed(
5399            &paint, &plain, &hovered
5400        ));
5401    }
5402
5403    // =====================================================================
5404    // grid-gap / scrollbar getters
5405    // =====================================================================
5406
5407    #[test]
5408    fn grid_gap_and_scrollbar_getters_are_none_on_a_bare_div() {
5409        let c = CssPropertyCache::empty(1);
5410        let nd = NodeData::create_div();
5411        assert!(c.get_grid_gap(&nd, &n0(), &normal()).is_none());
5412        assert!(c.get_scrollbar_track(&nd, &n0(), &normal()).is_none());
5413        assert!(c.get_scrollbar_thumb(&nd, &n0(), &normal()).is_none());
5414        assert!(c.get_scrollbar_button(&nd, &n0(), &normal()).is_none());
5415        assert!(c.get_scrollbar_corner(&nd, &n0(), &normal()).is_none());
5416        assert!(c.get_scrollbar_resizer(&nd, &n0(), &normal()).is_none());
5417
5418        // and on an out-of-range node id
5419        let far = NodeId::new(4_242);
5420        assert!(c.get_grid_gap(&nd, &far, &normal()).is_none());
5421        assert!(c.get_scrollbar_thumb(&nd, &far, &normal()).is_none());
5422    }
5423
5424    // =====================================================================
5425    // get_computed_css_style_string
5426    // =====================================================================
5427
5428    #[test]
5429    fn computed_css_style_string_serializes_set_properties() {
5430        let c = CssPropertyCache::empty(1);
5431
5432        // A bare <div> still gets `display: block` from the UA sheet.
5433        let s = c.get_computed_css_style_string(&NodeData::create_div(), &n0(), &normal());
5434        assert!(s.contains("display:"), "got {s:?}");
5435
5436        let styled = div_with(vec![width_px(100.0), font_size(PixelValue::px(12.0))]);
5437        let s = c.get_computed_css_style_string(&styled, &n0(), &normal());
5438        assert!(s.contains("width:"), "got {s:?}");
5439        assert!(s.contains("font-size:"), "got {s:?}");
5440        assert!(s.ends_with(';'), "each declaration is terminated: {s:?}");
5441    }
5442
5443    #[test]
5444    fn computed_css_style_string_does_not_panic_on_an_out_of_range_node_id() {
5445        let c = CssPropertyCache::empty(0);
5446        let s = c.get_computed_css_style_string(
5447            &NodeData::create_div(),
5448            &NodeId::new(usize::MAX / 2),
5449            &normal(),
5450        );
5451        assert!(s.contains("display:"));
5452    }
5453
5454    // =====================================================================
5455    // apply_ua_css / sort_cascaded_props / prune_compact_normal_props
5456    // =====================================================================
5457
5458    #[test]
5459    fn apply_ua_css_inserts_ua_properties_into_cascaded_props() {
5460        let nodes = vec![NodeData::create_div()];
5461        let mut c = CssPropertyCache::empty(1);
5462        c.apply_ua_css(&nodes);
5463
5464        let props = c.cascaded_props.build_get(0).expect("build phase");
5465        assert!(
5466            props
5467                .iter()
5468                .any(|p| p.prop_type == CssPropertyType::Display
5469                    && p.state == PseudoStateType::Normal),
5470            "UA `div {{ display: block }}` must land in the cascade"
5471        );
5472    }
5473
5474    #[test]
5475    fn apply_ua_css_does_not_override_an_existing_inline_property() {
5476        let nodes = vec![div_with(vec![CssProperty::const_none(
5477            CssPropertyType::Display,
5478        )])];
5479        let mut c = CssPropertyCache::empty(1);
5480        c.apply_ua_css(&nodes);
5481
5482        let props = c.cascaded_props.build_get(0).expect("build phase");
5483        assert!(
5484            !props.iter().any(|p| p.prop_type == CssPropertyType::Display),
5485            "UA CSS is the weakest layer and must not clobber inline"
5486        );
5487    }
5488
5489    #[test]
5490    fn apply_ua_css_on_zero_nodes_returns_early() {
5491        let mut c = CssPropertyCache::empty(0);
5492        c.apply_ua_css(&[]);
5493        assert_eq!(c.cascaded_props.len(), 0);
5494    }
5495
5496    #[test]
5497    fn sort_cascaded_props_flattens_and_orders_by_state_then_type() {
5498        let mut c = CssPropertyCache::empty(1);
5499        c.cascaded_props
5500            .push_to(0, stateful(PseudoStateType::Hover, width_px(1.0)));
5501        c.cascaded_props.push_to(
5502            0,
5503            stateful(
5504                PseudoStateType::Normal,
5505                CssProperty::const_none(CssPropertyType::Display),
5506            ),
5507        );
5508        c.cascaded_props
5509            .push_to(0, stateful(PseudoStateType::Normal, width_px(2.0)));
5510
5511        c.sort_cascaded_props();
5512
5513        assert!(c.cascaded_props.is_flattened());
5514        let slice = c.cascaded_props.get_slice(0);
5515        assert_eq!(slice.len(), 3);
5516        let keys: Vec<_> = slice.iter().map(|p| (p.state, p.prop_type)).collect();
5517        let mut sorted = keys.clone();
5518        sorted.sort_unstable();
5519        assert_eq!(keys, sorted, "binary_search lookups require sort order");
5520    }
5521
5522    #[test]
5523    fn prune_compact_normal_props_keeps_what_the_slow_path_still_needs() {
5524        let mut c = CssPropertyCache::empty(1);
5525        // Normal + compact-encoded + fully representable => droppable
5526        c.cascaded_props.push_to(
5527            0,
5528            stateful(
5529                PseudoStateType::Normal,
5530                CssProperty::const_none(CssPropertyType::Display),
5531            ),
5532        );
5533        // Normal + compact-encoded but SENTINEL-encoded (%) => must survive
5534        c.cascaded_props
5535            .push_to(0, stateful(PseudoStateType::Normal, width_pct(50.0)));
5536        // Normal + no compact encoding at all => must survive
5537        c.cascaded_props.push_to(
5538            0,
5539            stateful(
5540                PseudoStateType::Normal,
5541                CssProperty::const_none(CssPropertyType::BackgroundContent),
5542            ),
5543        );
5544        // non-Normal => always survives
5545        c.cascaded_props.push_to(
5546            0,
5547            stateful(
5548                PseudoStateType::Hover,
5549                CssProperty::const_none(CssPropertyType::Display),
5550            ),
5551        );
5552
5553        c.prune_compact_normal_props();
5554
5555        let kept: Vec<(PseudoStateType, CssPropertyType)> = c
5556            .cascaded_props
5557            .get_slice(0)
5558            .iter()
5559            .map(|p| (p.state, p.prop_type))
5560            .collect();
5561
5562        assert!(
5563            !kept.contains(&(PseudoStateType::Normal, CssPropertyType::Display)),
5564            "the compact cache is authoritative for this one"
5565        );
5566        assert!(kept.contains(&(PseudoStateType::Normal, CssPropertyType::Width)));
5567        assert!(kept.contains(&(PseudoStateType::Normal, CssPropertyType::BackgroundContent)));
5568        assert!(kept.contains(&(PseudoStateType::Hover, CssPropertyType::Display)));
5569        assert_eq!(kept.len(), 3);
5570    }
5571
5572    #[test]
5573    fn prune_compact_normal_props_on_an_empty_cache_does_not_panic() {
5574        let mut c = CssPropertyCache::empty(0);
5575        c.prune_compact_normal_props();
5576        assert_eq!(c.cascaded_props.len(), 0);
5577
5578        let mut c = CssPropertyCache::empty(3);
5579        c.prune_compact_normal_props();
5580        assert_eq!(c.cascaded_props.len(), 3);
5581        assert!(c.cascaded_props.get_slice(0).is_empty());
5582    }
5583
5584    // =====================================================================
5585    // compute_inherited_values
5586    // =====================================================================
5587
5588    /// `[root, child]`, child's parent = root (the hierarchy uses 1-based ids).
5589    fn two_node_hierarchy() -> Vec<NodeHierarchyItem> {
5590        vec![
5591            NodeHierarchyItem {
5592                parent: 0,
5593                previous_sibling: 0,
5594                next_sibling: 0,
5595                last_child: 2,
5596            },
5597            NodeHierarchyItem {
5598                parent: 1,
5599                previous_sibling: 0,
5600                next_sibling: 0,
5601                last_child: 0,
5602            },
5603        ]
5604    }
5605
5606    #[test]
5607    fn compute_inherited_values_propagates_font_size_to_children() {
5608        let hierarchy = two_node_hierarchy();
5609        assert_eq!(hierarchy[1].parent_id(), Some(NodeId::new(0)));
5610
5611        let nodes = vec![
5612            div_with(vec![font_size(PixelValue::px(20.0))]),
5613            NodeData::create_div(),
5614        ];
5615        let mut c = CssPropertyCache::empty(2);
5616        let changed = c.compute_inherited_values(&hierarchy, &nodes);
5617
5618        assert_eq!(c.computed_values.len(), 2);
5619        assert_eq!(changed.len(), 2, "both nodes gained a computed value");
5620
5621        let (t, v) = &c.computed_values[1][0];
5622        assert_eq!(*t, CssPropertyType::FontSize);
5623        assert_eq!(v.origin, CssPropertyOrigin::Inherited);
5624        assert!(close(font_size_parts(&v.property).unwrap().1, 20.0));
5625
5626        // the parent's own value keeps the Own origin
5627        assert_eq!(c.computed_values[0][0].1.origin, CssPropertyOrigin::Own);
5628    }
5629
5630    #[test]
5631    fn compute_inherited_values_resolves_a_child_em_against_the_parent_px() {
5632        let hierarchy = two_node_hierarchy();
5633        let nodes = vec![
5634            div_with(vec![font_size(PixelValue::px(20.0))]),
5635            div_with(vec![font_size(PixelValue::em(2.0))]),
5636        ];
5637        let mut c = CssPropertyCache::empty(2);
5638        c.compute_inherited_values(&hierarchy, &nodes);
5639
5640        let (t, v) = &c.computed_values[1][0];
5641        assert_eq!(*t, CssPropertyType::FontSize);
5642        assert_eq!(v.origin, CssPropertyOrigin::Own);
5643        let (metric, n) = font_size_parts(&v.property).unwrap();
5644        assert_eq!(metric, SizeMetric::Px, "resolved to absolute px");
5645        assert!(close(n, 40.0), "2em of the parent's 20px, got {n}");
5646    }
5647
5648    #[test]
5649    fn compute_inherited_values_is_idempotent_on_a_second_run() {
5650        let hierarchy = two_node_hierarchy();
5651        let nodes = vec![
5652            div_with(vec![font_size(PixelValue::px(20.0))]),
5653            NodeData::create_div(),
5654        ];
5655        let mut c = CssPropertyCache::empty(2);
5656        assert_eq!(c.compute_inherited_values(&hierarchy, &nodes).len(), 2);
5657        assert!(
5658            c.compute_inherited_values(&hierarchy, &nodes).is_empty(),
5659            "nothing changed the second time around"
5660        );
5661    }
5662
5663    #[test]
5664    fn compute_inherited_values_on_an_empty_tree_does_not_panic() {
5665        let mut c = CssPropertyCache::empty(0);
5666        assert!(c.compute_inherited_values(&[], &[]).is_empty());
5667        assert!(c.computed_values.is_empty());
5668    }
5669
5670    // =====================================================================
5671    // restyle / generate_tag_ids
5672    // =====================================================================
5673
5674    fn one_node_scaffold() -> (NodeHierarchyItemVec, NodeDataContainer<CascadeInfo>) {
5675        (
5676            vec![NodeHierarchyItem::zeroed()].into(),
5677            NodeDataContainer::new(vec![CascadeInfo {
5678                index_in_parent: 0,
5679                is_last_child: true,
5680            }]),
5681        )
5682    }
5683
5684    #[test]
5685    fn restyle_with_an_empty_stylesheet_flattens_and_yields_no_tags() {
5686        let (hierarchy, cascade) = one_node_scaffold();
5687        let nodes = NodeDataContainer::new(vec![NodeData::create_div()]);
5688        let non_leaf: ParentWithNodeDepthVec = Vec::new().into();
5689        let mut css = Css::empty();
5690
5691        let mut c = CssPropertyCache::empty(1);
5692        let tags = c.restyle(
5693            &mut css,
5694            &nodes.as_ref(),
5695            &hierarchy,
5696            &non_leaf,
5697            &cascade.as_ref(),
5698        );
5699
5700        assert!(tags.is_empty(), "a plain div needs no hit-test tag");
5701        assert!(
5702            c.css_props.is_flattened(),
5703            "restyle must leave css_props in read phase"
5704        );
5705        assert!(c.resolved_font_sizes_px.get().is_none());
5706    }
5707
5708    #[test]
5709    fn generate_tag_ids_skips_inert_nodes_and_tags_interactive_ones() {
5710        let (hierarchy, _) = one_node_scaffold();
5711
5712        let inert = NodeDataContainer::new(vec![NodeData::create_div()]);
5713        let c = CssPropertyCache::empty(1);
5714        assert!(c.generate_tag_ids(&inert.as_ref(), &hierarchy).is_empty());
5715
5716        // an inline :hover rule makes the node hit-testable
5717        let hoverable = NodeDataContainer::new(vec![div_with_pseudo(
5718            vec![width_px(1.0)],
5719            PseudoStateType::Hover,
5720        )]);
5721        let tags = c.generate_tag_ids(&hoverable.as_ref(), &hierarchy);
5722        assert_eq!(tags.len(), 1);
5723        assert_eq!(
5724            tags[0].node_id.into_crate_internal(),
5725            Some(NodeId::new(0))
5726        );
5727    }
5728
5729    #[test]
5730    fn generate_tag_ids_tags_a_node_with_a_cursor_declaration() {
5731        let (hierarchy, _) = one_node_scaffold();
5732        let nodes = NodeDataContainer::new(vec![div_with(vec![CssProperty::const_none(
5733            CssPropertyType::Cursor,
5734        )])]);
5735        let c = CssPropertyCache::empty(1);
5736        assert_eq!(c.generate_tag_ids(&nodes.as_ref(), &hierarchy).len(), 1);
5737    }
5738
5739    #[test]
5740    fn generate_tag_ids_on_an_empty_dom_yields_nothing() {
5741        let nodes: NodeDataContainer<NodeData> = NodeDataContainer::new(Vec::new());
5742        let hierarchy: NodeHierarchyItemVec = Vec::new().into();
5743        let c = CssPropertyCache::empty(0);
5744        assert!(c.generate_tag_ids(&nodes.as_ref(), &hierarchy).is_empty());
5745    }
5746
5747    // =====================================================================
5748    // std-gated profiling helpers
5749    // =====================================================================
5750
5751    #[cfg(feature = "std")]
5752    #[test]
5753    fn css_prop_type_label_is_interned_and_distinct_per_variant() {
5754        let a = CssPropertyCache::css_prop_type_label(&CssPropertyType::Width);
5755        let b = CssPropertyCache::css_prop_type_label(&CssPropertyType::Width);
5756        assert!(!a.is_empty());
5757        assert_eq!(
5758            a.as_ptr(),
5759            b.as_ptr(),
5760            "the label table must leak at most one &'static str per variant"
5761        );
5762
5763        let other = CssPropertyCache::css_prop_type_label(&CssPropertyType::Height);
5764        assert_ne!(a, other);
5765    }
5766
5767    #[cfg(feature = "std")]
5768    #[test]
5769    fn drain_css_prop_counts_is_sorted_descending_and_drains() {
5770        // The counter is thread-local and only records when AZ_PROP_COUNT=1, so
5771        // the contract to pin here is "never panics, and drains".
5772        let first = drain_css_prop_counts();
5773        for w in first.windows(2) {
5774            assert!(w[0].1 >= w[1].1, "counts must be sorted descending");
5775        }
5776        assert!(
5777            drain_css_prop_counts().is_empty(),
5778            "a drained counter comes back empty"
5779        );
5780    }
5781}