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