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