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