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

            
24
extern crate alloc;
25

            
26
use alloc::{boxed::Box, string::String, vec::Vec};
27
use core::fmt::Write;
28
use core::mem::ManuallyDrop;
29

            
30
use 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)]
35
pub 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)]
44
pub struct CssPropertyWithOrigin {
45
    pub property: CssProperty,
46
    pub origin: CssPropertyOrigin,
47
}
48

            
49
use azul_css::{
50
    css::{Css, CssPath},
51
    props::{
52
        basic::{StyleFontFamily, StyleFontFamilyVec, StyleFontSize},
53
        layout::{LayoutDisplay, LayoutHeight, LayoutWidth},
54
        property::{
55
            BoxDecorationBreakValue, BreakInsideValue, CaretAnimationDurationValue,
56
            CaretColorValue, CaretWidthValue, ClipPathValue, ColumnCountValue, ColumnFillValue,
57
            ColumnRuleColorValue, ColumnRuleStyleValue, ColumnRuleWidthValue, ColumnSpanValue,
58
            ColumnWidthValue, ContentValue, CounterIncrementValue, CounterResetValue, CssProperty,
59
            CssPropertyType, FlowFromValue, FlowIntoValue, LayoutAlignContentValue,
60
            LayoutAlignItemsValue, LayoutAlignSelfValue, LayoutBorderBottomWidthValue,
61
            LayoutBorderLeftWidthValue, LayoutBorderRightWidthValue, LayoutBorderSpacingValue,
62
            LayoutBorderTopWidthValue, LayoutBoxSizingValue, LayoutClearValue,
63
            LayoutColumnGapValue, LayoutDisplayValue, LayoutFlexBasisValue,
64
            LayoutFlexDirectionValue, LayoutFlexGrowValue, LayoutFlexShrinkValue,
65
            LayoutFlexWrapValue, LayoutFloatValue, LayoutGapValue, LayoutGridAutoColumnsValue,
66
            LayoutGridAutoFlowValue, LayoutGridAutoRowsValue, LayoutGridColumnValue,
67
            LayoutGridRowValue, LayoutGridTemplateColumnsValue, LayoutGridTemplateRowsValue,
68
            LayoutHeightValue, LayoutInsetBottomValue, LayoutJustifyContentValue,
69
            LayoutJustifyItemsValue, LayoutJustifySelfValue, LayoutLeftValue,
70
            LayoutMarginBottomValue, LayoutMarginLeftValue, LayoutMarginRightValue,
71
            LayoutMarginTopValue, LayoutMaxHeightValue, LayoutMaxWidthValue, LayoutMinHeightValue,
72
            LayoutMinWidthValue, LayoutOverflowValue, LayoutPaddingBottomValue,
73
            LayoutPaddingLeftValue, LayoutPaddingRightValue, LayoutPaddingTopValue,
74
            LayoutPositionValue, LayoutRightValue, LayoutRowGapValue, LayoutScrollbarWidthValue,
75
            LayoutTableLayoutValue, LayoutTextJustifyValue, LayoutTopValue, LayoutWidthValue,
76
            LayoutWritingModeValue, LayoutZIndexValue, OrphansValue, PageBreakValue,
77
            StyleBackgroundContentValue, ScrollbarFadeDelayValue, ScrollbarFadeDurationValue,
78
            ScrollbarVisibilityModeValue, SelectionBackgroundColorValue, SelectionColorValue,
79
            SelectionRadiusValue, ShapeImageThresholdValue, ShapeInsideValue, ShapeMarginValue,
80
            ShapeOutsideValue, StringSetValue, StyleBackfaceVisibilityValue,
81
            StyleBackgroundContentVecValue, StyleBackgroundPositionVecValue,
82
            StyleBackgroundRepeatVecValue, StyleBackgroundSizeVecValue,
83
            StyleBorderBottomColorValue, StyleBorderBottomLeftRadiusValue,
84
            StyleBorderBottomRightRadiusValue, StyleBorderBottomStyleValue,
85
            StyleBorderCollapseValue, StyleBorderLeftColorValue, StyleBorderLeftStyleValue,
86
            StyleBorderRightColorValue, StyleBorderRightStyleValue, StyleBorderTopColorValue,
87
            StyleBorderTopLeftRadiusValue, StyleBorderTopRightRadiusValue,
88
            StyleBorderTopStyleValue, StyleBoxShadowValue, StyleCaptionSideValue, StyleCursorValue,
89
            StyleDirectionValue, StyleEmptyCellsValue, StyleExclusionMarginValue,
90
            StyleFilterVecValue, StyleFontFamilyVecValue, StyleFontSizeValue, StyleFontStyleValue,
91
            StyleFontValue, StyleFontWeightValue, StyleHangingPunctuationValue,
92
            StyleHyphenationLanguageValue, StyleHyphensValue, StyleInitialLetterValue,
93
            StyleLetterSpacingValue, StyleLineBreakValue, StyleLineClampValue, StyleLineHeightValue,
94
            StyleListStylePositionValue, StyleListStyleTypeValue, StyleMixBlendModeValue,
95
            StyleAspectRatioValue, StyleObjectFitValue, StyleObjectPositionValue, StyleTextOverflowValue,
96
            StyleOpacityValue, StylePerspectiveOriginValue,
97
            StyleScrollbarColorValue, StyleOverflowWrapValue, StyleTabSizeValue,
98
            StyleTextAlignLastValue, StyleTextOrientationValue, StyleTextTransformValue,
99
            StyleTextAlignValue, StyleTextColorValue,
100
            StyleTextCombineUprightValue, StyleUnicodeBidiValue,
101
            StyleTextBoxTrimValue, StyleTextBoxEdgeValue,
102
            StyleDominantBaselineValue, StyleAlignmentBaselineValue, StyleBaselineSourceValue,
103
            StyleLineFitEdgeValue,
104
            StyleInitialLetterAlignValue, StyleInitialLetterWrapValue,
105
            StyleScrollbarGutterValue, StyleOverflowClipMarginValue, StyleClipRectValue,
106
            StyleTextDecorationValue, StyleTextIndentValue,
107
            StyleTransformOriginValue, StyleTransformVecValue, StyleUserSelectValue,
108
            StyleVerticalAlignValue, StyleVisibilityValue, StyleWhiteSpaceValue,
109
            StyleWordBreakValue, StyleWordSpacingValue, WidowsValue,
110
        },
111
        style::{StyleCursor, StyleTextColor, StyleTransformOrigin},
112
    },
113
    AzString,
114
};
115

            
116
use crate::{
117
    dom::{NodeData, NodeId, TabIndex, TagId},
118
    id::{NodeDataContainer, NodeDataContainerRef},
119
    style::CascadeInfo,
120
    styled_dom::{
121
        NodeHierarchyItem, NodeHierarchyItemId, NodeHierarchyItemVec, ParentWithNodeDepth,
122
        ParentWithNodeDepthVec, StyledNodeState, TagIdToNodeIdMapping,
123
    },
124
};
125

            
126
use azul_css::dynamic_selector::{
127
    CssPropertyWithConditions, CssPropertyWithConditionsVec, DynamicSelectorContext,
128
};
129

            
130
#[cfg(feature = "std")]
131
std::thread_local! {
132
    static PROP_COUNTS: core::cell::RefCell<
133
        std::collections::HashMap<&'static str, usize>
134
    > = core::cell::RefCell::new(std::collections::HashMap::new());
135
}
136

            
137
/// Drain the per-thread CSS cascade-walk counter populated by
138
/// [`CssPropertyCache::get_property`] when `AZ_PROP_COUNT=1` is set
139
/// in the environment.
140
///
141
/// Returns `(property_label, count)` pairs
142
/// sorted by count descending. Layout-side instrumentation calls
143
/// this after each `layout_document` to print which properties
144
/// drove the most cascade walks.
145
#[cfg(feature = "std")]
146
2
#[must_use] pub fn drain_css_prop_counts() -> Vec<(&'static str, usize)> {
147
    // try_with: no real TLS in the lifted-to-wasm web backend (see the
148
    // get_property recording site) — return empty rather than panic.
149
2
    PROP_COUNTS
150
2
        .try_with(|c| {
151
2
            let map = core::mem::take(&mut *c.borrow_mut());
152
2
            let mut v: Vec<_> = map.into_iter().collect();
153
2
            v.sort_by(|a, b| b.1.cmp(&a.1));
154
2
            v
155
2
        })
156
2
        .unwrap_or_default()
157
2
}
158

            
159
// Unit conversion constants (CSS absolute units → pixels)
160
const PT_TO_PX: f32 = 1.333_333;
161
const IN_TO_PX: f32 = 96.0;
162
const CM_TO_PX: f32 = 37.795_277;
163
const 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)]
167
macro_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)]
342
pub 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)]
367
pub 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

            
376
impl<T: PartialEq> PartialEq for FlatVecVec<T> {
377
5
    fn eq(&self, other: &Self) -> bool {
378
5
        let self_in_build = !self.build.is_empty() && self.offsets.is_empty();
379
5
        let other_in_build = !other.build.is_empty() && other.offsets.is_empty();
380
5
        debug_assert!(
381
            self_in_build == other_in_build,
382
            "FlatVecVec::eq called across phases (one build, one flattened)"
383
        );
384
5
        if self_in_build || other_in_build {
385
4
            self.build == other.build
386
        } else {
387
1
            self.data == other.data && self.offsets == other.offsets
388
        }
389
5
    }
390
}
391

            
392
impl<T> Default for FlatVecVec<T> {
393
6
    fn default() -> Self {
394
6
        Self {
395
6
            build: Vec::new(),
396
6
            data: Vec::new(),
397
6
            offsets: Vec::new(),
398
6
        }
399
6
    }
400
}
401

            
402
impl<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
20
    #[must_use] pub fn heap_bytes(&self, per_element_size: usize) -> usize {
408
20
        let data_bytes = self.data.capacity() * per_element_size;
409
20
        let offsets_bytes =
410
20
            self.offsets.capacity() * size_of::<(u32, u32)>();
411
20
        let mut build_bytes = self.build.capacity() * size_of::<Vec<T>>();
412
144
        for v in &self.build {
413
124
            build_bytes += v.capacity() * per_element_size;
414
124
        }
415
20
        data_bytes + offsets_bytes + build_bytes
416
20
    }
417

            
418
    /// Create a new `FlatVecVec` with `node_count` empty slots (build phase).
419
106207
    #[must_use] pub fn new(node_count: usize) -> Self {
420
106207
        let mut build = Vec::with_capacity(node_count);
421
1811856
        for _ in 0..node_count {
422
1811856
            build.push(Vec::new());
423
1811856
        }
424
106207
        Self {
425
106207
            build,
426
106207
            data: Vec::new(),
427
106207
            offsets: Vec::new(),
428
106207
        }
429
106207
    }
430

            
431
    /// Push an item to the inner Vec at `node_index` (build phase).
432
    ///
433
    /// # Panics
434
    /// Panics if already flattened or if `node_index >= len()`.
435
    #[inline]
436
1382528
    pub fn push_to(&mut self, node_index: usize, item: T) {
437
1382528
        self.build[node_index].push(item);
438
1382528
    }
439

            
440
    /// Get a mutable reference to the inner Vec at `node_index` (build phase).
441
    #[inline]
442
465506
    pub fn build_mut(&mut self, node_index: usize) -> &mut Vec<T> {
443
465506
        &mut self.build[node_index]
444
465506
    }
445

            
446
    /// Iterate mutably over all inner Vecs (build phase, e.g. for clearing).
447
    #[inline]
448
1
    pub fn build_iter_mut(&mut self) -> core::slice::IterMut<'_, Vec<T>> {
449
1
        self.build.iter_mut()
450
1
    }
451

            
452
    /// Get a reference to the inner Vec at `node_index` during build phase.
453
    /// During read phase, returns None (use `get_slice` instead).
454
    #[inline]
455
8
    #[must_use] pub fn build_get(&self, node_index: usize) -> Option<&Vec<T>> {
456
8
        self.build.get(node_index)
457
8
    }
458

            
459
    /// Number of node slots.
460
    #[inline]
461
84146
    #[must_use] pub const fn len(&self) -> usize {
462
84146
        if self.offsets.is_empty() {
463
48629
            self.build.len()
464
        } else {
465
35517
            self.offsets.len()
466
        }
467
84146
    }
468

            
469
    /// Returns `true` if there are no node slots.
470
    #[inline]
471
5
    #[must_use] pub const fn is_empty(&self) -> bool {
472
5
        self.len() == 0
473
5
    }
474

            
475
    /// Returns true if this is in read (flattened) mode.
476
    #[inline]
477
35456
    #[must_use] pub const fn is_flattened(&self) -> bool {
478
35456
        !self.offsets.is_empty() || self.build.is_empty()
479
35456
    }
480

            
481
    /// Get a slice for the node at `node_index` (read phase).
482
    /// Returns empty slice if index is out of bounds or not yet flattened
483
    /// (falls back to build-phase data if not yet flattened).
484
    #[inline]
485
37573111
    #[must_use] pub fn get_slice(&self, node_index: usize) -> &[T] {
486
37573111
        if self.offsets.is_empty() {
487
            // Build phase fallback: use inner Vecs
488
4348937
            self.build.get(node_index).map_or(&[], alloc::vec::Vec::as_slice)
489
        } else {
490
            // Read phase: use flat data
491
33224174
            if let Some(&(start, len)) = self.offsets.get(node_index) {
492
33224172
                let s = start as usize;
493
33224172
                let l = len as usize;
494
33224172
                &self.data[s..s + l]
495
            } else {
496
2
                &[]
497
            }
498
        }
499
37573111
    }
500

            
501
    /// Flatten: sort each inner Vec by key, deduplicate by keeping the last
502
    /// occurrence of each key (CSS cascade: later source order wins among
503
    /// equal specificity), then compact into flat storage.
504
    /// Drains all build-phase Vecs. After this call, only `get_slice()` works.
505
70892
    pub fn sort_each_and_flatten<K: Ord + Eq>(&mut self, key_fn: impl Fn(&T) -> K) {
506
70892
        let node_count = self.build.len();
507
70892
        let total: usize = self.build.iter().map(alloc::vec::Vec::len).sum();
508

            
509
70892
        let mut flat_data = Vec::with_capacity(total);
510
70892
        let mut offsets = Vec::with_capacity(node_count);
511

            
512
1487740
        for inner in &mut self.build {
513
5312171
            inner.sort_by_key(|a| key_fn(a));
514

            
515
            // Deduplicate: keep last of each consecutive-key group (CSS cascade).
516
1416848
            let n = inner.len();
517
1416848
            let mut keep = vec![false; n];
518
2431645
            for i in 0..n {
519
2428317
                if i + 1 >= n || key_fn(&inner[i]) != key_fn(&inner[i + 1]) {
520
2404327
                    keep[i] = true;
521
2404327
                }
522
            }
523

            
524
1416848
            let start = u32::try_from(flat_data.len()).unwrap_or(u32::MAX);
525
            // Drain inner and push only kept items
526
2431645
            for (i, item) in inner.drain(..).enumerate() {
527
2428317
                if keep[i] {
528
2404327
                    flat_data.push(item);
529
2404327
                }
530
            }
531

            
532
1416848
            let len = u32::try_from(flat_data.len()).unwrap_or(u32::MAX) - start;
533
1416848
            offsets.push((start, len));
534
        }
535

            
536
70892
        flat_data.shrink_to_fit();
537
70892
        self.data = flat_data;
538
70892
        self.offsets = offsets;
539
70892
        self.build = Vec::new();
540
70892
    }
541

            
542
    /// Flatten without sorting (for data that's already sorted).
543
14
    pub fn flatten(&mut self) {
544
14
        let node_count = self.build.len();
545
14
        let total: usize = self.build.iter().map(alloc::vec::Vec::len).sum();
546

            
547
14
        let mut flat_data = Vec::with_capacity(total);
548
14
        let mut offsets = Vec::with_capacity(node_count);
549

            
550
41
        for inner in &mut self.build {
551
27
            let start = u32::try_from(flat_data.len()).unwrap_or(u32::MAX);
552
27
            let len = u32::try_from(inner.len()).unwrap_or(u32::MAX);
553
27
            offsets.push((start, len));
554
27
            flat_data.append(inner);
555
27
        }
556

            
557
14
        self.data = flat_data;
558
14
        self.offsets = offsets;
559
14
        self.build = Vec::new();
560
14
    }
561

            
562
    /// Rebuild flat storage, keeping only items matching `predicate`.
563
    /// Must be called after flatten. Preserves per-node ordering.
564
35451
    pub fn retain(&mut self, predicate: impl Fn(&T) -> bool) where T: Clone {
565
35451
        if self.offsets.is_empty() { return; }
566
35383
        let node_count = self.offsets.len();
567
35383
        let mut new_data = Vec::new();
568
35383
        let mut new_offsets = Vec::with_capacity(node_count);
569
722087
        for &(start, len) in &self.offsets {
570
686704
            let s = start as usize;
571
686704
            let l = len as usize;
572
686704
            let new_start = u32::try_from(new_data.len()).unwrap_or(u32::MAX);
573
686704
            let slice = &self.data[s..s + l];
574
686704
            let mut kept = 0u32;
575
2747519
            for item in slice {
576
2060815
                if predicate(item) {
577
822776
                    new_data.push((*item).clone());
578
822776
                    kept += 1;
579
1238039
                }
580
            }
581
686704
            new_offsets.push((new_start, kept));
582
        }
583
35383
        new_data.shrink_to_fit();
584
35383
        self.data = new_data;
585
35383
        self.offsets = new_offsets;
586
35451
    }
587

            
588
    /// Return to build phase from read (flattened) phase, preserving all data, so
589
    /// `push_to` / `build_mut` work again. No-op if already in build phase.
590
    ///
591
    /// The build → flatten → read progression is otherwise one-way, but `restyle()`
592
    /// legitimately runs more than once on the same cache (`StyledDom::create` does one
593
    /// internal pass, then the public API may do more), and building the compact cache
594
    /// flattens these vecs in between. Without re-entering build phase, the next
595
    /// restyle's `push_to` / `build_mut` would index an emptied `build` and panic.
596
    pub fn ensure_build_phase(&mut self) where T: Clone {
597
        if self.offsets.is_empty() {
598
            return; // already in build phase (or wholly empty)
599
        }
600
        let mut build = Vec::with_capacity(self.offsets.len());
601
        for &(start, len) in &self.offsets {
602
            let s = start as usize;
603
            let l = len as usize;
604
            build.push(self.data[s..s + l].to_vec());
605
        }
606
        self.build = build;
607
        self.data = Vec::new();
608
        self.offsets = Vec::new();
609
    }
610

            
611
    /// Like `retain`, but passes each item's owning node index to the predicate.
612
    /// Must be called after flatten. Preserves per-node ordering.
613
2
    pub fn retain_with_node_index(
614
2
        &mut self,
615
2
        predicate: impl Fn(usize, &T) -> bool,
616
2
    ) where T: Clone {
617
2
        if self.offsets.is_empty() { return; }
618
1
        let node_count = self.offsets.len();
619
1
        let mut new_data = Vec::new();
620
1
        let mut new_offsets = Vec::with_capacity(node_count);
621
3
        for (node_idx, &(start, len)) in self.offsets.iter().enumerate() {
622
3
            let s = start as usize;
623
3
            let l = len as usize;
624
3
            let new_start = u32::try_from(new_data.len()).unwrap_or(u32::MAX);
625
3
            let slice = &self.data[s..s + l];
626
3
            let mut kept = 0u32;
627
6
            for item in slice {
628
3
                if predicate(node_idx, item) {
629
1
                    new_data.push((*item).clone());
630
1
                    kept += 1;
631
2
                }
632
            }
633
3
            new_offsets.push((new_start, kept));
634
        }
635
1
        new_data.shrink_to_fit();
636
1
        self.data = new_data;
637
1
        self.offsets = new_offsets;
638
2
    }
639

            
640
    /// Iterate over all nodes, yielding (`node_index`, &[T]) for each.
641
    /// Works in both build and flattened phases.
642
70879
    pub(crate) const fn iter_node_slices(&self) -> FlatVecVecIter<'_, T> {
643
70879
        FlatVecVecIter {
644
70879
            fvv: self,
645
70879
            idx: 0,
646
70879
            count: self.len(),
647
70879
        }
648
70879
    }
649

            
650
    /// Extend this `FlatVecVec` with all nodes from `other` (append for DOM merge).
651
    /// Both must be in build phase, or both must be flattened.
652
491
    pub fn extend_from(&mut self, other: &mut Self) {
653
491
        if !self.offsets.is_empty() && !other.offsets.is_empty() {
654
            // Both flattened: extend flat data with offset adjustment
655
471
            let base = u32::try_from(self.data.len()).unwrap_or(u32::MAX);
656
471
            self.data.append(&mut other.data);
657
756
            self.offsets.extend(other.offsets.drain(..).map(|(s, l)| (s + base, l)));
658
20
        } else {
659
20
            // At least one in build phase: extend build vecs
660
20
            self.build.append(&mut other.build);
661
20
            // Invalidate flat data if it existed
662
20
            self.data.clear();
663
20
            self.offsets.clear();
664
20
        }
665
491
    }
666
}
667

            
668
/// Iterator over (`node_index`, &[T]) pairs from a `FlatVecVec`.
669
pub(crate) struct FlatVecVecIter<'a, T> {
670
    fvv: &'a FlatVecVec<T>,
671
    idx: usize,
672
    count: usize,
673
}
674

            
675
impl<'a, T> Iterator for FlatVecVecIter<'a, T> {
676
    type Item = (usize, &'a [T]);
677

            
678
    #[inline]
679
1530971
    fn next(&mut self) -> Option<Self::Item> {
680
1530971
        if self.idx >= self.count {
681
70879
            return None;
682
1460092
        }
683
1460092
        let i = self.idx;
684
1460092
        self.idx += 1;
685
1460092
        Some((i, self.fvv.get_slice(i)))
686
1530971
    }
687

            
688
2
    fn size_hint(&self) -> (usize, Option<usize>) {
689
2
        let rem = self.count - self.idx;
690
2
        (rem, Some(rem))
691
2
    }
692
}
693

            
694
impl<T> ExactSizeIterator for FlatVecVecIter<'_, T> {}
695

            
696
// NOTE: To avoid large memory allocations, this is a "cache" that stores all the CSS properties
697
// found in the DOM. This cache exists on a per-DOM basis, so it scales independent of how many
698
// nodes are in the DOM.
699
//
700
// If each node would carry its own CSS properties, that would unnecessarily consume memory
701
// because most nodes use the default properties or override only one or two properties.
702
//
703
// The cache can compute the property of any node at any given time, given the current node
704
// state (hover, active, focused, normal). This way we don't have to duplicate the CSS properties
705
// onto every single node and exchange them when the style changes. Two caches can be appended
706
// to each other by simply merging their NodeIds.
707
#[derive(Debug, Default, Clone, PartialEq)]
708
pub struct CssPropertyCache {
709
    // number of nodes in the current DOM
710
    pub node_count: usize,
711

            
712
    // The author stylesheet this cache was last cascaded with. Retained so nodes
713
    // inserted at runtime can be re-styled (`StyledDom::restyle_retained`) — the
714
    // cascade runs once at creation, and without the rules an inserted node could
715
    // only ever receive UA defaults + inheritance, never its author CSS.
716
    // (This struct lives behind `CssPropertyCachePtr`, so the field is invisible
717
    // to the C ABI.)
718
    pub retained_author_css: Css,
719

            
720
    // properties that were overridden in callbacks (not specific to any node state)
721
    pub user_overridden_properties: Vec<Vec<(CssPropertyType, CssProperty)>>,
722
    /// The window's dynamic-selector context (viewport size, theme, OS,
723
    /// media type...), provided by the layout funnel before the first
724
    /// layout. `None` = context UNKNOWN (a freshly created `StyledDom` that no
725
    /// window has adopted yet): non-pseudo-state conditions then evaluate to
726
    /// "does not apply", which is the same behaviour they always had before
727
    /// contexts were wired through. Pseudo-state conditions never depend on
728
    /// this field.
729
    pub dynamic_context: Option<Box<DynamicSelectorContext>>,
730

            
731
    // non-default CSS properties that were cascaded from the parent,
732
    // unified across all pseudo-states (Normal, Hover, Active, Focus, Dragging, DragOver).
733
    // Stored in a flat cache-friendly layout after sort_and_flatten().
734
    pub cascaded_props: FlatVecVec<StatefulCssProperty>,
735

            
736
    // non-default CSS properties that were set via a CSS file,
737
    // unified across all pseudo-states.
738
    pub css_props: FlatVecVec<StatefulCssProperty>,
739

            
740
    // Pre-resolved inherited properties (sorted Vec per node, keyed by CssPropertyType)
741
    pub computed_values: Vec<Vec<(CssPropertyType, CssPropertyWithOrigin)>>,
742

            
743
    // Compact layout cache: three-tier numeric encoding for O(1) layout lookups.
744
    // Built once after restyle + apply_ua_css + compute_inherited_values.
745
    // Non-compact properties (background, shadow, transform) use get_property_slow().
746
    pub compact_cache: Option<azul_css::compact_cache::CompactLayoutCache>,
747

            
748
    // Global CSS properties from `*` rules — shared across all nodes.
749
    // Applied during build_compact_cache_with_inheritance instead of being
750
    // cloned into each node's css_props (saves 50K×N clones).
751
    pub global_css_props: Vec<CssProperty>,
752

            
753
    /// Per-node resolved font-size, in pixels, for the `Normal`
754
    /// pseudo-state. Populated lazily on first call to
755
    /// [`crate::styled_dom::StyledDom::resolved_font_size_px`] via a
756
    /// single bottom-up DOM walk; subsequent reads are O(1) Vec
757
    /// index by `NodeId::index()`.
758
    ///
759
    /// Motivation: `get_font_size` is called ~730× per node per
760
    /// layout pass (see `AZ_PROP_COUNT=1` report — 329 629
761
    /// cascade walks on excel.html alone). Each resolution
762
    /// recursively reads the parent's font-size (for `em`) plus
763
    /// the root's font-size (for `rem`), multiplying the walk
764
    /// count. Caching the pre-resolved pixel value collapses that
765
    /// to a single `Vec<f32>` indexed lookup.
766
    pub resolved_font_sizes_px: crate::sync::OnceLock<Vec<f32>>,
767
}
768

            
769
/// Heap-size breakdown of a `CssPropertyCache`, produced by
770
/// [`CssPropertyCache::memory_breakdown`]. All values in bytes.
771
///
772
/// Primarily a diagnostic — the numbers are capacity-based and
773
/// don't chase into property-variant payloads (e.g. the `Vec`
774
/// inside a `FontFamily(...)`). Intended for "which subfield is
775
/// eating RSS" triage, not for precise accounting.
776
#[derive(Debug, Clone, Copy, Default)]
777
pub struct CssPropertyCacheBreakdown {
778
    pub node_count: usize,
779
    pub cascaded_props_bytes: usize,
780
    pub css_props_bytes: usize,
781
    pub computed_values_bytes: usize,
782
    pub user_overridden_bytes: usize,
783
    pub global_css_props_bytes: usize,
784
    pub compact_cache_bytes: usize,
785
    pub resolved_font_sizes_bytes: usize,
786
}
787

            
788
impl CssPropertyCacheBreakdown {
789
    /// Sum of all subfields.
790
12
    #[must_use] pub const fn total_bytes(&self) -> usize {
791
12
        self.cascaded_props_bytes
792
12
            + self.css_props_bytes
793
12
            + self.computed_values_bytes
794
12
            + self.user_overridden_bytes
795
12
            + self.global_css_props_bytes
796
12
            + self.compact_cache_bytes
797
12
            + self.resolved_font_sizes_bytes
798
12
    }
799
}
800

            
801
impl CssPropertyCache {
802
    /// Approximate heap bytes retained by this cache, broken out by
803
    /// subfield. Used by `StyledDom::memory_breakdown` + the
804
    /// `AZ_PROFILE=memory` reporter. Sums capacity × element size
805
    /// for each Vec and adds a coarse allowance for the inner Vec
806
    /// headers inside `computed_values`.
807
    ///
808
    /// This is a measurement helper, not a tight bound — it doesn't
809
    /// chase into the `CssProperty` enum variants that carry their
810
    /// own `Vec`/`String` allocations (notably `FontFamily` →
811
    /// `StyleFontFamilyVec` → `Vec<StyleFontFamily>`), so the real
812
    /// heap footprint for a property-rich DOM can be 2-3× these
813
    /// numbers. Still useful for spotting gross duplication between
814
    /// the pre-compact and compact caches.
815
5
    pub fn memory_breakdown(&self) -> CssPropertyCacheBreakdown {
816
5
        let stateful_sz = size_of::<StatefulCssProperty>();
817
5
        let computed_entry_sz =
818
5
            size_of::<(CssPropertyType, CssPropertyWithOrigin)>();
819
5
        let outer_vec_sz = size_of::<Vec<(CssPropertyType, CssPropertyWithOrigin)>>();
820

            
821
5
        let cascaded_bytes = self.cascaded_props.heap_bytes(stateful_sz);
822
5
        let css_bytes = self.css_props.heap_bytes(stateful_sz);
823

            
824
5
        let mut computed_bytes = self.computed_values.capacity() * outer_vec_sz;
825
58
        for v in &self.computed_values {
826
53
            computed_bytes += v.capacity() * computed_entry_sz;
827
53
        }
828

            
829
5
        let user_overridden_bytes = {
830
5
            let mut b = self.user_overridden_properties.capacity() * outer_vec_sz;
831
5
            for v in &self.user_overridden_properties {
832
                b += v.capacity()
833
                    * size_of::<(CssPropertyType, CssProperty)>();
834
            }
835
5
            b
836
        };
837

            
838
5
        let global_bytes = self.global_css_props.capacity()
839
5
            * size_of::<CssProperty>();
840

            
841
5
        let compact_bytes = self
842
5
            .compact_cache
843
5
            .as_ref()
844
5
            .map_or(0, |c| {
845
2
                c.tier1_enums.capacity() * 8
846
2
                    + c.tier2_dims.capacity() * 68
847
2
                    + c.tier2_cold.capacity() * 28
848
2
                    + c.tier2b_text.capacity() * 24
849
2
                    + c.prev_font_hashes.capacity() * 8
850
2
                    + c.font_dirty_nodes.capacity() * 8
851
2
            });
852

            
853
5
        let resolved_font_sizes_bytes = self
854
5
            .resolved_font_sizes_px
855
5
            .get()
856
5
            .map_or(0, |v| v.capacity() * size_of::<f32>());
857

            
858
5
        CssPropertyCacheBreakdown {
859
5
            node_count: self.node_count,
860
5
            cascaded_props_bytes: cascaded_bytes,
861
5
            css_props_bytes: css_bytes,
862
5
            computed_values_bytes: computed_bytes,
863
5
            user_overridden_bytes,
864
5
            global_css_props_bytes: global_bytes,
865
5
            compact_cache_bytes: compact_bytes,
866
5
            resolved_font_sizes_bytes,
867
5
        }
868
5
    }
869

            
870
    /// Drop Normal-state properties that have compact encodings from
871
    /// `css_props` and `cascaded_props`. After `build_compact_cache_with_inheritance`,
872
    /// these are redundant — the compact cache is the source of truth for layout.
873
    /// Non-Normal entries (hover/active/focus) and non-compact properties
874
    /// (background, box-shadow, transform, etc.) are kept for `get_property_slow`.
875
35447
    pub fn prune_compact_normal_props(&mut self) {
876
        use azul_css::dynamic_selector::PseudoStateType;
877

            
878
        #[cfg(feature = "std")]
879
        {
880
        static PRUNE_DBG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
881
35447
        let dbg = *PRUNE_DBG.get_or_init(crate::profile::memory_enabled);
882
35447
        if dbg {
883
            let mut normal_compact = 0usize;
884
            let mut normal_noncompact = 0usize;
885
            let mut nonnormal = 0usize;
886
            for i in 0..self.css_props.len() {
887
                for p in self.css_props.get_slice(i) {
888
                    if p.state != PseudoStateType::Normal {
889
                        nonnormal += 1;
890
                    } else if p.prop_type.has_compact_encoding() {
891
                        normal_compact += 1;
892
                    } else {
893
                        normal_noncompact += 1;
894
                    }
895
                }
896
            }
897
            let ssp_sz = size_of::<StatefulCssProperty>();
898
            let mut casc_normal_compact = 0usize;
899
            let mut casc_total = 0usize;
900
            for i in 0..self.cascaded_props.len() {
901
                for p in self.cascaded_props.get_slice(i) {
902
                    casc_total += 1;
903
                    if p.state == PseudoStateType::Normal && p.prop_type.has_compact_encoding() {
904
                        casc_normal_compact += 1;
905
                    }
906
                }
907
            }
908
            eprintln!("[PRUNE] css_props: norm+compact={normal_compact} norm+other={normal_noncompact} nonnorm={nonnormal} SSP={ssp_sz}B | cascaded: total={casc_total} norm+compact={casc_normal_compact}");
909
35447
        }
910
        }
911

            
912
        // The compact cache stores SENTINEL for pixel-valued properties whose inner
913
        // value is Exact with a non-px metric (vh, vw, %, em, rem, calc(), ...).
914
        // Those need the slow `css_props` walk at layout time because the compact
915
        // cache has nothing usable. We must keep them here or the slow path falls
916
        // back to UA CSS and silently clobbers the author's rule.
917
2059808
        let keep = |p: &StatefulCssProperty| -> bool {
918
2059808
            if p.state != PseudoStateType::Normal {
919
18063
                return true;
920
2041745
            }
921
2041745
            if !p.prop_type.has_compact_encoding() {
922
536076
                return true;
923
1505669
            }
924
            // Compact-encoded AND Normal: drop only if the compact cache fully
925
            // captured the value (px metric, or Auto/Initial/Inherit/None).
926
1505669
            if property_needs_slow_path_after_compact(&p.property) {
927
268135
                return true;
928
1237534
            }
929
1237534
            false
930
2059808
        };
931
        // DO NOT prune css_props: regenerate_layout calls
932
        // recompute_inheritance_and_compact_cache() every frame, which REBUILDS the
933
        // compact cache from css_props (build_compact_cache_with_inheritance reads
934
        // css_props in its per-node Step 3). If we drop compact-encoded Normal props
935
        // here, that rebuild reads pruned css_props and resets those props to their
936
        // CSS-initial value — e.g. white-space:pre-wrap on a node regressed to Normal
937
        // on the 2nd (recompute) build, collapsing \n in pre-wrap text into one line
938
        // (#8, intermittently — depends on whether the recompute ran). The doc's
939
        // premise ("the compact cache is the source of truth", implying permanence)
940
        // is false given that per-frame recompute. cascaded_props is NOT read by the
941
        // rebuild (Step 1 inherits from the parent's COMPACT value, not cascaded_props),
942
        // so pruning it remains safe. TODO: re-enable css_props pruning once recompute
943
        // becomes incremental (preserve directly-set compact values instead of rebuilding).
944
35447
        if !self.cascaded_props.is_flattened() {
945
6745238
            self.cascaded_props.sort_each_and_flatten(|p| (p.state, p.prop_type));
946
67
        }
947
35447
        self.cascaded_props.retain(keep);
948
35447
    }
949

            
950
    /// Look up a CSS property for a specific pseudo-state in a stateful property vec.
951
    /// Requires the vec to be sorted by (state, `prop_type`).
952
    #[inline]
953
    // prop_cache threads &NodeId/&CssPropertyType uniformly through its hot cascade
954
    // lookup API (40+ such params); flipping only clippy's few flags to by-value
955
    // would force ref/deref juggling at every boundary with the by-ref majority,
956
    // for no measurable hot-path gain — keep the uniform by-ref convention.
957
    #[allow(clippy::trivially_copy_pass_by_ref)]
958
18692217
    fn find_in_stateful<'a>(
959
18692217
        props: &'a [StatefulCssProperty],
960
18692217
        state: azul_css::dynamic_selector::PseudoStateType,
961
18692217
        prop_type: &CssPropertyType,
962
18692217
    ) -> Option<&'a CssProperty> {
963
18692217
        let key = (state, *prop_type);
964
18746278
        props.binary_search_by_key(&key, |p| (p.state, p.prop_type))
965
18692217
            .ok()
966
18692217
            .map(|idx| &props[idx].property)
967
18692217
    }
968

            
969
    /// Check if any properties exist for a specific pseudo-state in a stateful property vec.
970
    /// Requires the vec to be sorted by (state, `prop_type`).
971
    #[inline]
972
6974808
    fn has_state_props(
973
6974808
        props: &[StatefulCssProperty],
974
6974808
        state: azul_css::dynamic_selector::PseudoStateType,
975
6974808
    ) -> bool {
976
        // All entries with the same state are contiguous. Use partition_point
977
        // to find the first entry >= state, then check if it matches.
978
6981248
        let i = props.partition_point(|p| p.state < state);
979
6974808
        i < props.len() && props[i].state == state
980
6974808
    }
981

            
982
    /// Collect all property types for a specific pseudo-state.
983
1054
    pub(crate) fn prop_types_for_state(
984
1054
        props: &[StatefulCssProperty],
985
1054
        state: azul_css::dynamic_selector::PseudoStateType,
986
1054
    ) -> impl Iterator<Item = &CssPropertyType> + '_ {
987
1157
        props.iter().filter(move |p| p.state == state).map(|p| &p.prop_type)
988
1054
    }
989
}
990

            
991
/// Returns true if `prop`'s value cannot be fully represented in the compact
992
/// cache and therefore needs to survive `prune_compact_normal_props` so the
993
/// slow `css_props` walk can still find it at layout time.
994
///
995
/// Pixel-valued properties (margin, padding, width, height, ...) are the only
996
/// case: `Exact(pv)` with `pv.metric != Px` (vh, vw, %, em, rem, ...) encodes
997
/// to the compact cache's SENTINEL slot, which loses the value. All other
998
/// compact-encoded types (tier1 enums, colors, hashes, etc.) always round-trip
999
/// through the compact encoding.
1505682
fn property_needs_slow_path_after_compact(prop: &CssProperty) -> bool {
    use azul_css::css::CssPropertyValue;
    use azul_css::props::{
        basic::length::SizeMetric,
        layout::{
            dimensions::{LayoutHeight, LayoutWidth},
            flex::LayoutFlexBasis,
        },
    };
    // `inner: PixelValue` wrapper types — check metric directly.
    macro_rules! check_plain {
        ($v:expr) => {{
            if let CssPropertyValue::Exact(ref inner) = $v {
                return inner.inner.metric != SizeMetric::Px;
            }
            false
        }};
    }
1505682
    match prop {
        // LayoutWidth / LayoutHeight: enum with `Px(PixelValue)` variant.
        // Non-pixel variants (Auto / MinContent / MaxContent / FitContent / Calc)
        // are already handled by the tier1 fast path or don't exist as i16 dims.
4
        CssProperty::Width(v) => {
3
            if let CssPropertyValue::Exact(LayoutWidth::Px(pv)) = v {
3
                return pv.metric != SizeMetric::Px;
1
            }
1
            false
        }
2
        CssProperty::Height(v) => {
2
            if let CssPropertyValue::Exact(LayoutHeight::Px(pv)) = v {
2
                return pv.metric != SizeMetric::Px;
            }
            false
        }
        // LayoutFlexBasis: enum with `Exact(PixelValue)` variant.
2
        CssProperty::FlexBasis(v) => {
2
            if let CssPropertyValue::Exact(LayoutFlexBasis::Exact(pv)) = v {
1
                return pv.metric != SizeMetric::Px;
1
            }
1
            false
        }
        // `inner: PixelValue` wrappers
1
        CssProperty::MinWidth(v) => check_plain!(v),
        CssProperty::MaxWidth(v) => check_plain!(v),
        CssProperty::MinHeight(v) => check_plain!(v),
        CssProperty::MaxHeight(v) => check_plain!(v),
18390
        CssProperty::FontSize(v) => check_plain!(v),
1639
        CssProperty::PaddingTop(v) => check_plain!(v),
1694
        CssProperty::PaddingRight(v) => check_plain!(v),
1639
        CssProperty::PaddingBottom(v) => check_plain!(v),
2377
        CssProperty::PaddingLeft(v) => check_plain!(v),
151090
        CssProperty::MarginTop(v) => check_plain!(v),
14370
        CssProperty::MarginRight(v) => check_plain!(v),
145656
        CssProperty::MarginBottom(v) => check_plain!(v),
14370
        CssProperty::MarginLeft(v) => check_plain!(v),
814
        CssProperty::BorderTopWidth(v) => check_plain!(v),
        CssProperty::BorderRightWidth(v) => check_plain!(v),
        CssProperty::BorderBottomWidth(v) => check_plain!(v),
        CssProperty::BorderLeftWidth(v) => check_plain!(v),
        CssProperty::Top(v) => check_plain!(v),
        CssProperty::Right(v) => check_plain!(v),
        CssProperty::Bottom(v) => check_plain!(v),
        CssProperty::Left(v) => check_plain!(v),
        CssProperty::ColumnGap(v) => check_plain!(v),
        CssProperty::RowGap(v) => check_plain!(v),
33
        CssProperty::LetterSpacing(v) => check_plain!(v),
11
        CssProperty::WordSpacing(v) => check_plain!(v),
11
        CssProperty::TextIndent(v) => check_plain!(v),
        CssProperty::TabSize(v) => check_plain!(v),
        // All other compact-encoded types round-trip through the compact cache.
1153579
        _ => false,
    }
1505682
}
/// Clone a `CssProperty` WITHOUT going through its derived `Clone`. The derived clone
/// is a ~179-arm `match self { V(x) => V(x.clone()) }` that LLVM lowers to an indirect
/// HALFWORD jump table (`ldrh`-indexed). The web (remill→wasm) backend mis-lifts that
/// table, so for HEAP/Vec-bearing variants (gradients, font-family, shadows, filters,
/// transforms) the mis-dispatched clone reads wrong-sized data and the cascade traps
/// with "memory access out of bounds" (restyle → inherit → clone). Here every
/// heap-bearing variant is dispatched via single-variant `if let` — a direct
/// discriminant compare, NO jump table — and each inner `v.clone()` is the value
/// type's own clone, which lifts correctly. POD variants fall through to the derived
/// clone: correct on native, and harmless on web (a mis-dispatched discriminant 0 is
/// `CaretColor`, a `Copy` value with no heap pointer to deref). On native this function
/// is byte-for-byte equivalent to `p.clone()`.
/// Inheritable properties whose value must be inherited as the parent's already
/// *resolved* value, NOT propagated as a raw declaration through `cascaded_props`.
///
/// `font-size` is the case: a relative parent value (`1.5em`) propagated raw
/// would be re-resolved against the already-resolved parent at every descendant
/// (multiplicative error: 30px -> 45px -> 67.5px down a chain), and a parent
/// whose own `cascaded` font-size is the *grandparent's* absolute value would
/// skip the parent's own size entirely. Both consuming paths already inherit
/// font-size correctly from the parent's resolved value — `inherit_from_parent`
/// for `computed_values`, and the parent's resolved compact slot in
/// `build_compact_cache_with_inheritance` — so font-size must not ride the raw
/// propagation at all.
1009950
fn is_resolved_parent_inherited(prop_type: CssPropertyType) -> bool {
1009950
    prop_type == CssPropertyType::FontSize
1009950
}
849487
fn clone_inheritable_property(
849487
    p: &CssProperty,
849487
) -> CssProperty {
    use azul_css::props::property::CssProperty;
849487
    if let CssProperty::FontFamily(v) = p { return CssProperty::FontFamily(v.clone()); }
682770
    if let CssProperty::BackgroundContent(v) = p { return CssProperty::BackgroundContent(v.clone()); }
682768
    if let CssProperty::BackgroundPosition(v) = p { return CssProperty::BackgroundPosition(v.clone()); }
682768
    if let CssProperty::BackgroundSize(v) = p { return CssProperty::BackgroundSize(v.clone()); }
682768
    if let CssProperty::BackgroundRepeat(v) = p { return CssProperty::BackgroundRepeat(v.clone()); }
682768
    if let CssProperty::BoxShadowLeft(v) = p { return CssProperty::BoxShadowLeft(v.clone()); }
682768
    if let CssProperty::BoxShadowRight(v) = p { return CssProperty::BoxShadowRight(v.clone()); }
682768
    if let CssProperty::BoxShadowTop(v) = p { return CssProperty::BoxShadowTop(v.clone()); }
682768
    if let CssProperty::BoxShadowBottom(v) = p { return CssProperty::BoxShadowBottom(v.clone()); }
682768
    if let CssProperty::TextShadow(v) = p { return CssProperty::TextShadow(v.clone()); }
682768
    if let CssProperty::ScrollbarTrack(v) = p { return CssProperty::ScrollbarTrack(v.clone()); }
682768
    if let CssProperty::ScrollbarThumb(v) = p { return CssProperty::ScrollbarThumb(v.clone()); }
682768
    if let CssProperty::ScrollbarButton(v) = p { return CssProperty::ScrollbarButton(v.clone()); }
682768
    if let CssProperty::ScrollbarCorner(v) = p { return CssProperty::ScrollbarCorner(v.clone()); }
682768
    if let CssProperty::ScrollbarResizer(v) = p { return CssProperty::ScrollbarResizer(v.clone()); }
682768
    if let CssProperty::Transform(v) = p { return CssProperty::Transform(v.clone()); }
682766
    if let CssProperty::Filter(v) = p { return CssProperty::Filter(v.clone()); }
682766
    if let CssProperty::BackdropFilter(v) = p { return CssProperty::BackdropFilter(v.clone()); }
682766
    if let CssProperty::Content(v) = p { return CssProperty::Content(v.clone()); }
682764
    if let CssProperty::HyphenationLanguage(v) = p { return CssProperty::HyphenationLanguage(v.clone()); }
682764
    if let CssProperty::Cursor(v) = p { return CssProperty::Cursor(*v); }
472453
    p.clone()
849487
}
impl CssPropertyCache {
    /// Match CSS selectors to nodes and populate `css_props`.
    /// Returns tag IDs for hit-testing. If `compact_cache` is available,
    /// uses it for fast display/overflow checks; otherwise falls back to slow path.
    #[must_use]
    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
35503
    pub fn restyle(
35503
        &mut self,
35503
        css: &mut Css,
35503
        node_data: &NodeDataContainerRef<'_, NodeData>,
35503
        node_hierarchy: &NodeHierarchyItemVec,
35503
        non_leaf_nodes: &ParentWithNodeDepthVec,
35503
        html_tree: &NodeDataContainerRef<'_, CascadeInfo>,
35503
    ) -> Vec<TagIdToNodeIdMapping> {
        use azul_css::{
            css::{CssDeclaration, CssPathPseudoSelector::{Hover, Active, Focus, Dragging, DragOver}, CssPathSelector, CssRuleBlock},
            dynamic_selector::{DynamicSelector, PseudoStateType},
            props::layout::LayoutDisplay,
        };
35503
        let css_is_empty = css.is_empty();
        // @-rule conditions (@media width/height, theme, OS...) gate whole
        // rule BLOCKS. Evaluated here against the window's dynamic context —
        // rules whose conditions do not hold are dropped from this cascade
        // exactly as if absent, and `StyledDom::set_dynamic_selector_context`
        // re-runs the cascade when the context changes and the author css
        // has conditional rules. With NO context yet (a StyledDom no window
        // has adopted), conditional rules do not apply — the same behaviour
        // inline conditional properties have always had. (Until 2026-08-10
        // these conditions were silently IGNORED: an author
        // `@media (max-width: 720px)` block applied at every viewport.)
35503
        let dyn_ctx = self.dynamic_context.clone();
1080575
        let rule_applies = |conds: &azul_css::dynamic_selector::DynamicSelectorVec| -> bool {
1080575
            let cs = conds.as_slice();
1080575
            cs.is_empty()
195220
                || dyn_ctx
195220
                    .as_deref()
195220
                    .is_some_and(|c| cs.iter().all(|sel| sel.matches(c)))
1080575
        };
35503
        if !css_is_empty {
13243
            css.sort_by_specificity();
            // Separate CSS rules into "global only" (just `*`) vs "has specific selector".
            // Global-only rules apply to ALL nodes — push directly into css_props
            // without per-node selector matching (avoids m×n for these rules).
            // Specific rules still go through matches_html_element per-node.
13243
            let mut global_only_rules: Vec<&CssRuleBlock> = Vec::new();
13243
            let mut specific_rules: Vec<&CssRuleBlock> = Vec::new();
38036
            for rule in css.rules() {
38036
                let selectors = rule.path.selectors.as_ref();
38036
                let is_global_only = selectors.len() == 1
23139
                    && matches!(selectors.first(), Some(CssPathSelector::Global));
38036
                if is_global_only {
1873
                    global_only_rules.push(rule);
36163
                } else {
36163
                    specific_rules.push(rule);
36163
                }
            }
            // Re-enter build phase before repopulating. restyle() is not
            // single-shot: StyledDom::create runs one restyle internally, and building
            // the compact cache flattens these vecs — so on a later restyle both are in
            // read phase, where the old reset `build_iter_mut().clear()` silently
            // iterated ZERO entries (flatten empties `build`). The push_to / build_mut
            // below then indexed an emptied Vec and panicked.
            //
            // css_props is rebuilt from scratch each restyle (repopulated below,
            // flattened at the end), so replace it with a fresh build-phase vec.
            //
            // cascaded_props is rebuilt from scratch TOO (2026-08-12): the old
            // preserve-and-or_insert approach LEAKED properties of rules whose
            // @-condition turned OFF — a color inherited under a min-width
            // block survived in every descendant after crossing below it, so
            // wide and narrow styling applied SIMULTANEOUSLY (the
            // media_restyle_cost law pin caught it). Preservation is
            // unnecessary: the inheritance walk is top-down (parents' fresh
            // slices are written before children read them — the same
            // ordering css_props relies on), so a fresh build-phase vec
            // repopulates completely. The historical reason for preserving
            // was a phase-bug in the old clear, not a data dependency.
13243
            let node_count = self.css_props.len();
13243
            self.css_props = FlatVecVec::new(node_count);
13243
            self.cascaded_props = FlatVecVec::new(node_count);
            // Collect global-only rule declarations ONCE (not per-node).
            // These are stored in self.global_css_props and applied during
            // build_compact_cache_with_inheritance for each node, avoiding
            // 50K × N clones into per-node css_props Vecs.
13243
            self.global_css_props.clear();
15116
            for rule in &global_only_rules {
1873
                if !rule_applies(&rule.conditions) {
                    continue;
1873
                }
1873
                if crate::style::rule_ends_with(&rule.path, None) {
16400
                    for d in &rule.declarations {
14527
                        if let CssDeclaration::Static(s) = d {
14527
                            self.global_css_props.push(s.clone());
14527
                        }
                    }
                }
            }
            // Phase 2: Match specific rules per-node (only non-global rules)
13243
            if !specific_rules.is_empty() {
            // Per-node "which declarations match" lists are built as
            // `(rule_idx, decl_idx)` pairs — 4 bytes per entry instead of
            // cloning a 140-byte `CssProperty`. The clone only happens at
            // the final push_to step, so the transient peak is ~35× smaller.
            //
            // rule_idx indexes into `specific_rules` (Vec<&CssRuleBlock>),
            // decl_idx indexes into `rule.declarations.as_slice()`. Both
            // fit in u16 since real stylesheets have far fewer than 65k
            // rules and declarations per rule.
            macro_rules! filter_rules {($expected_pseudo_selector:expr, $node_id:expr) => {{
                let mut out: Vec<(u16, u16)> = Vec::new();
                for (rule_idx, rule_block) in specific_rules.iter().enumerate() {
                    if !rule_applies(&rule_block.conditions) {
                        continue;
                    }
                    if !crate::style::rule_ends_with(&rule_block.path, $expected_pseudo_selector) {
                        continue;
                    }
                    if !crate::style::matches_html_element(
                        &rule_block.path,
                        $node_id,
                        &node_hierarchy.as_container(),
                        &node_data,
                        &html_tree,
                        $expected_pseudo_selector,
                    ) {
                        continue;
                    }
                    for (decl_idx, decl) in rule_block.declarations.as_slice().iter().enumerate() {
                        if matches!(decl, CssDeclaration::Static(_)) {
                            out.push((u16::try_from(rule_idx).unwrap_or(u16::MAX), u16::try_from(decl_idx).unwrap_or(u16::MAX)));
                        }
                    }
                }
                out
            }};}
            // Pre-check which pseudo-states have any matching rules at all.
            // This avoids iterating 50K nodes for pseudo-states with zero rules
            // (common: most stylesheets have no :hover/:focus/:active rules).
13193
            let has_normal = specific_rules.iter().any(|r| crate::style::rule_ends_with(&r.path, None));
35965
            let has_hover = specific_rules.iter().any(|r| crate::style::rule_ends_with(&r.path, Some(Hover)));
36163
            let has_active = specific_rules.iter().any(|r| crate::style::rule_ends_with(&r.path, Some(Active)));
36163
            let has_focus = specific_rules.iter().any(|r| crate::style::rule_ends_with(&r.path, Some(Focus)));
36163
            let has_dragging = specific_rules.iter().any(|r| crate::style::rule_ends_with(&r.path, Some(Dragging)));
36163
            let has_drag_over = specific_rules.iter().any(|r| crate::style::rule_ends_with(&r.path, Some(DragOver)));
            macro_rules! collect_and_assign {
                ($pseudo:expr, $state:expr, $has_any:expr) => {
                    if $has_any {
                        let indices: NodeDataContainer<(NodeId, Vec<(u16, u16)>)> = node_data
216150
                            .transform_nodeid_optional(|node_id| {
932462
                                let r = filter_rules!($pseudo, node_id);
216150
                                if r.is_empty() { None } else { Some((node_id, r)) }
216150
                            });
                        for (n, pairs) in indices.internal.into_iter() {
                            for (rule_idx, decl_idx) in pairs {
                                let decl = &specific_rules[rule_idx as usize]
                                    .declarations
                                    .as_slice()[decl_idx as usize];
                                if let CssDeclaration::Static(prop) = decl {
                                    self.css_props.push_to(n.index(), StatefulCssProperty {
                                        state: $state,
                                        prop_type: prop.get_type(),
                                        property: prop.clone(),
                                    });
                                }
                            }
                        }
                    }
                };
            }
366338
            collect_and_assign!(None, PseudoStateType::Normal, has_normal);
13193
            collect_and_assign!(Some(Hover), PseudoStateType::Hover, has_hover);
13193
            collect_and_assign!(Some(Active), PseudoStateType::Active, has_active);
13193
            collect_and_assign!(Some(Focus), PseudoStateType::Focus, has_focus);
13193
            collect_and_assign!(Some(Dragging), PseudoStateType::Dragging, has_dragging);
13193
            collect_and_assign!(Some(DragOver), PseudoStateType::DragOver, has_drag_over);
50
            } // end if !specific_rules.is_empty()
22260
        }
        // Inheritance: Inherit all values of the parent to the children, but
        // only if the property is inheritable and isn't yet set
415117
        for ParentWithNodeDepth { depth: _, node_id } in non_leaf_nodes {
379614
            let Some(parent_id) = node_id.into_crate_internal() else {
                continue;
            };
379614
            let all_states = [
379614
                PseudoStateType::Normal,
379614
                PseudoStateType::Hover,
379614
                PseudoStateType::Active,
379614
                PseudoStateType::Focus,
379614
                PseudoStateType::Dragging,
379614
                PseudoStateType::DragOver,
379614
            ];
2657298
            for &state in &all_states {
                // 1. Inherit inline CSS properties from parent for this pseudo-state
2277684
                let parent_inheritable_inline: Vec<(CssPropertyType, CssProperty)> = node_data[parent_id]
2277684
                    .style
2277684
                    .iter_inline_properties()
18900708
                    .filter(|(_prop, conds)| {
18875406
                        let conditions = conds.as_slice();
18875406
                        if conditions.is_empty() {
17777562
                            state == PseudoStateType::Normal
                        } else {
1097844
                            conditions.iter().all(|c| {
1087152
                                matches!(c, DynamicSelector::PseudoState(s) if *s == state)
1097844
                            })
                        }
18875406
                    })
2277684
                    .map(|(prop, _)| prop)
3172556
                    .filter(|prop| prop.get_type().is_inheritable() && !is_resolved_parent_inherited(prop.get_type()))
2277684
                    .map(|p| (p.get_type(), clone_inheritable_property(p)))
2277684
                    .collect();
                // 2. Inherit CSS stylesheet properties from parent for this pseudo-state
2277684
                let parent_inheritable_css: Vec<(CssPropertyType, CssProperty)> = if css_is_empty {
1677408
                    Vec::new()
                } else {
600276
                    self.css_props.get_slice(parent_id.index())
600276
                        .iter()
1923714
                        .filter(|p| p.state == state && p.prop_type.is_inheritable() && !is_resolved_parent_inherited(p.prop_type))
600276
                        .map(|p| (p.prop_type, clone_inheritable_property(&p.property)))
600276
                        .collect()
                };
                // 3. Inherit cascaded properties from parent for this pseudo-state
2277684
                let parent_inheritable_cascaded: Vec<(CssPropertyType, CssProperty)> =
2277684
                    self.cascaded_props.get_slice(parent_id.index())
2277684
                        .iter()
2277684
                        .filter(|p| p.state == state && p.prop_type.is_inheritable() && !is_resolved_parent_inherited(p.prop_type))
2277684
                        .map(|p| (p.prop_type, clone_inheritable_property(&p.property)))
2277684
                        .collect();
                // Combine all inheritable props (inline first = strongest, cascaded last)
                // Only insert if child doesn't already have that (state, prop_type) combo
2277684
                if parent_inheritable_inline.is_empty()
2092055
                    && parent_inheritable_css.is_empty()
2074899
                    && parent_inheritable_cascaded.is_empty()
                {
1973185
                    continue;
304499
                }
465505
                for child_id in parent_id.az_children(&node_hierarchy.as_container()) {
465505
                    let child_vec = self.cascaded_props.build_mut(child_id.index());
1144348
                    for (prop_type, prop_value) in parent_inheritable_inline
465505
                        .iter()
465505
                        .chain(parent_inheritable_css.iter())
465505
                        .chain(parent_inheritable_cascaded.iter())
                    {
                        // or_insert: only insert if child doesn't already have this (state, prop_type)
1293728
                        if !child_vec.iter().any(|p| p.state == state && p.prop_type == *prop_type) {
1099088
                            child_vec.push(StatefulCssProperty {
1099088
                                state,
1099088
                                prop_type: *prop_type,
1099088
                                property: prop_value.clone(),
1099088
                            });
1099088
                        }
                    }
                }
            }
        }
        // Sort css_props by (state, prop_type) for binary search lookups,
        // then flatten into contiguous memory for cache-friendly reads.
1962642
        self.css_props.sort_each_and_flatten(|p| (p.state, p.prop_type));
        // Restyling can change font-size properties; the memoized resolved font
        // sizes are now stale and must be recomputed on next access.
35503
        self.invalidate_resolved_font_sizes();
35503
        self.generate_tag_ids(node_data, node_hierarchy)
35503
    }
    /// Generate hit-test tag IDs for nodes that need event handling.
    /// Uses compact cache (if available) for fast display/overflow reads.
    /// Can be called separately after `build_compact_cache_with_inheritance`.
72483
    pub fn generate_tag_ids(
72483
        &self,
72483
        node_data: &NodeDataContainerRef<'_, NodeData>,
72483
        node_hierarchy: &NodeHierarchyItemVec,
72483
    ) -> Vec<TagIdToNodeIdMapping> {
        // Tag ID generation: determine which nodes need hit-test tags for
        // hover/click/scroll events. Uses compact cache for display/overflow
        // checks instead of get_property_slow (which searches 6 data structures).
        use azul_css::compact_cache::{
            DISPLAY_SHIFT, DISPLAY_MASK,
            OVERFLOW_X_SHIFT, OVERFLOW_Y_SHIFT, OVERFLOW_MASK,
        };
72483
        let compact_cache = self.compact_cache.as_ref();
72483
        let node_data_container = &node_data.internal;
72483
        let tag_ids = node_data
72483
            .internal
72483
            .iter()
72483
            .enumerate()
1661126
            .filter_map(|(node_idx, node_data)| {
1661126
                let node_id = NodeId::new(node_idx);
1661126
                let should_auto_insert_tabindex = node_data
1661126
                    .get_callbacks()
1661126
                    .iter()
1661126
                    .any(|cb| cb.event.is_focus_callback());
1661126
                let tab_index = node_data.get_tab_index().map_or(if should_auto_insert_tabindex {
5368
                            Some(TabIndex::Auto)
                        } else {
1655758
                            None
                        }, Some);
1661126
                let mut need_tag = false;
                // Single-pass guard block: each check `break`s out early once it
                // decides `need_tag`. Labeled block (not `loop`) makes the
                // never-iterating control flow explicit (clippy::never_loop).
                'compute_need_tag: {
                    // display:none check — read directly from compact tier1 (fast u64 read)
1661126
                    if let Some(cc) = compact_cache.as_ref() {
974508
                        let t1 = cc.tier1_enums[node_idx];
974508
                        let display_val = ((t1 >> DISPLAY_SHIFT) & DISPLAY_MASK) as u8;
974508
                        if display_val == 4 { break 'compute_need_tag; } // 4 = LayoutDisplay::None (new encoding)
686618
                    }
1656429
                    if node_data.has_context_menu() || node_data.get_context_menu().is_some() {
88
                        need_tag = true; break 'compute_need_tag;
1656341
                    }
1656341
                    if tab_index.is_some() { need_tag = true; break 'compute_need_tag; }
                    // Pseudo-state property checks (hover/active/focus/dragging/drag-over)
                    {
                        use azul_css::dynamic_selector::{DynamicSelector, PseudoStateType};
7048325
                        let has_pseudo = |state: PseudoStateType| -> bool {
18002037
                            node_data.style.iter_inline_properties().any(|(_p, conds)| {
17888333
                                conds.as_slice().iter().any(|c|
75155
                                    matches!(c, DynamicSelector::PseudoState(s) if *s == state)
                                )
18000478
                            }) || Self::has_state_props(self.css_props.get_slice(node_idx), state)
7048325
                        };
1469829
                        if has_pseudo(PseudoStateType::Hover)
1394662
                            || has_pseudo(PseudoStateType::Active)
1394662
                            || has_pseudo(PseudoStateType::Focus)
1394586
                            || has_pseudo(PseudoStateType::Dragging)
1394586
                            || has_pseudo(PseudoStateType::DragOver)
                        {
75243
                            need_tag = true; break 'compute_need_tag;
1394586
                        }
                    }
                    // Non-window callbacks
1394586
                    let has_non_window_cb = !node_data.get_callbacks().is_empty()
7920
                        && !node_data.get_callbacks().iter().all(|cb| cb.event.is_window_callback());
1394586
                    if has_non_window_cb { need_tag = true; break 'compute_need_tag; }
                    // Cursor check — read from cached css_props or inline style.
1386666
                    if self.css_props.get_slice(node_idx).iter().any(|p|
656860
                        p.state == azul_css::dynamic_selector::PseudoStateType::Normal
656860
                        && p.prop_type == CssPropertyType::Cursor
1383762
                    ) || node_data.style.iter_inline_properties().any(|(p, _)|
3026026
                        p.get_type() == CssPropertyType::Cursor
                    ) {
45167
                        need_tag = true; break 'compute_need_tag;
1341499
                    }
                    // Overflow scroll check — read from compact tier1
1341499
                    if let Some(cc) = compact_cache.as_ref() {
790740
                        let t1 = cc.tier1_enums[node_idx];
790740
                        let ox = ((t1 >> OVERFLOW_X_SHIFT) & OVERFLOW_MASK) as u8;
790740
                        let oy = ((t1 >> OVERFLOW_Y_SHIFT) & OVERFLOW_MASK) as u8;
                        // 2 = Scroll, 3 = Auto in layout_overflow_to_u8 (new encoding)
790740
                        if ox == 2 || ox == 3 || oy == 2 || oy == 3 {
2015
                            need_tag = true; break 'compute_need_tag;
788725
                        }
550759
                    }
                    // Selectable text check
                    {
                        use crate::dom::NodeType;
1339484
                        let hier = node_hierarchy.as_container()[node_id];
1339484
                        let mut has_text = false;
1339484
                        if let Some(first_child) = hier.first_child_id(node_id) {
571266
                            let mut child_id = Some(first_child);
1289394
                            while let Some(cid) = child_id {
1043785
                                if matches!(node_data_container[cid.index()].get_node_type(), NodeType::Text(_)) {
325657
                                    has_text = true; break;
718128
                                }
718128
                                child_id = node_hierarchy.as_container()[cid].next_sibling_id();
                            }
768218
                        }
1339484
                        if has_text { need_tag = true; break 'compute_need_tag; }
                    }
1013827
                    break 'compute_need_tag;
                }
1661126
                if need_tag {
                    // DETERMINISTIC tag: a pure function of node identity
                    // (node index + 1; 0 stays "no tag"), NOT a global
                    // counter. Tag values are namespaced by tag TYPE
                    // (`TAG_TYPE_DOM_NODE` vs cursor/scrollbar/... — every
                    // consumer matches `tag.1`) and resolved per-DOM, so
                    // per-node determinism is all that is required.
                    //
                    // The old `TagId::unique()` counter made tag numbers an
                    // ALLOCATION ORDER artifact: rebuilding the SAME UI (any
                    // callback returning RefreshDom) produced a fresh tag
                    // map, while the structural-identity display-list cache
                    // (solver3 Step 1.1 — root subtree hash + viewport)
                    // correctly reused the old display list. Map and display
                    // list then disagreed about every tag, and each lookup
                    // that crosses the two — `get_node_hit_test_bounds`, the
                    // WebRender hit-test translation — silently resolved to
                    // nothing: after a hash-identical rebuild, clicks aimed
                    // by node stopped landing (the E2E `double_click` on the
                    // ribbon tab was the visible case). With tags derived
                    // from node identity, a structurally identical tree gets
                    // identical tags, which is exactly the invariant the
                    // display-list cache assumes.
642602
                    Some(TagIdToNodeIdMapping {
642602
                        tag_id: TagId::from_crate_internal(TagId {
642602
                            inner: (node_idx as u64) + 1,
642602
                        }),
642602
                        node_id: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
642602
                        tab_index: tab_index.into(),
642602
                    })
                } else {
1018524
                    None
                }
1661126
            })
72483
            .collect::<Vec<_>>();
72483
        tag_ids
72483
    }
    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
334
    pub fn get_computed_css_style_string(
334
        &self,
334
        node_data: &NodeData,
334
        node_id: &NodeId,
334
        node_state: &StyledNodeState,
334
    ) -> String {
334
        let mut s = String::new();
334
        if let Some(p) = self.get_background_content(node_data, node_id, node_state) {
15
            let _ = write!(s,"background: {};", p.get_css_value_fmt());
319
        }
334
        if let Some(p) = self.get_background_position(node_data, node_id, node_state) {
            let _ = write!(s,"background-position: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_background_size(node_data, node_id, node_state) {
            let _ = write!(s,"background-size: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_background_repeat(node_data, node_id, node_state) {
            let _ = write!(s,"background-repeat: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_font_size(node_data, node_id, node_state) {
111
            let _ = write!(s,"font-size: {};", p.get_css_value_fmt());
323
        }
334
        if let Some(p) = self.get_font_family(node_data, node_id, node_state) {
            let _ = write!(s,"font-family: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_text_color(node_data, node_id, node_state) {
110
            let _ = write!(s,"color: {};", p.get_css_value_fmt());
324
        }
334
        if let Some(p) = self.get_text_align(node_data, node_id, node_state) {
            let _ = write!(s,"text-align: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_line_height(node_data, node_id, node_state) {
            let _ = write!(s,"line-height: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_letter_spacing(node_data, node_id, node_state) {
            let _ = write!(s,"letter-spacing: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_word_spacing(node_data, node_id, node_state) {
            let _ = write!(s,"word-spacing: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_tab_size(node_data, node_id, node_state) {
            let _ = write!(s,"tab-size: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_cursor(node_data, node_id, node_state) {
65
            let _ = write!(s,"cursor: {};", p.get_css_value_fmt());
279
        }
334
        if let Some(p) = self.get_box_shadow_left(node_data, node_id, node_state) {
            let _ = write!(s,
                "-azul-box-shadow-left: {};",
                p.get_css_value_fmt()
            );
334
        }
334
        if let Some(p) = self.get_box_shadow_right(node_data, node_id, node_state) {
            let _ = write!(s,
                "-azul-box-shadow-right: {};",
                p.get_css_value_fmt()
            );
334
        }
334
        if let Some(p) = self.get_box_shadow_top(node_data, node_id, node_state) {
            let _ = write!(s,"-azul-box-shadow-top: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_box_shadow_bottom(node_data, node_id, node_state) {
            let _ = write!(s,
                "-azul-box-shadow-bottom: {};",
                p.get_css_value_fmt()
            );
334
        }
334
        if let Some(p) = self.get_border_top_color(node_data, node_id, node_state) {
            let _ = write!(s,"border-top-color: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_border_left_color(node_data, node_id, node_state) {
            let _ = write!(s,"border-left-color: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_border_right_color(node_data, node_id, node_state) {
            let _ = write!(s,"border-right-color: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_border_bottom_color(node_data, node_id, node_state) {
            let _ = write!(s,"border-bottom-color: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_border_top_style(node_data, node_id, node_state) {
            let _ = write!(s,"border-top-style: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_border_left_style(node_data, node_id, node_state) {
            let _ = write!(s,"border-left-style: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_border_right_style(node_data, node_id, node_state) {
            let _ = write!(s,"border-right-style: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_border_bottom_style(node_data, node_id, node_state) {
            let _ = write!(s,"border-bottom-style: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_border_top_left_radius(node_data, node_id, node_state) {
            let _ = write!(s,
                "border-top-left-radius: {};",
                p.get_css_value_fmt()
            );
334
        }
334
        if let Some(p) = self.get_border_top_right_radius(node_data, node_id, node_state) {
            let _ = write!(s,
                "border-top-right-radius: {};",
                p.get_css_value_fmt()
            );
334
        }
334
        if let Some(p) = self.get_border_bottom_left_radius(node_data, node_id, node_state) {
            let _ = write!(s,
                "border-bottom-left-radius: {};",
                p.get_css_value_fmt()
            );
334
        }
334
        if let Some(p) = self.get_border_bottom_right_radius(node_data, node_id, node_state) {
            let _ = write!(s,
                "border-bottom-right-radius: {};",
                p.get_css_value_fmt()
            );
334
        }
334
        if let Some(p) = self.get_opacity(node_data, node_id, node_state) {
            let _ = write!(s,"opacity: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_transform(node_data, node_id, node_state) {
            let _ = write!(s,"transform: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_transform_origin(node_data, node_id, node_state) {
            let _ = write!(s,"transform-origin: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_perspective_origin(node_data, node_id, node_state) {
            let _ = write!(s,"perspective-origin: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_backface_visibility(node_data, node_id, node_state) {
            let _ = write!(s,"backface-visibility: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_hyphens(node_data, node_id, node_state) {
            let _ = write!(s,"hyphens: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_direction(node_data, node_id, node_state) {
            let _ = write!(s,"direction: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_unicode_bidi(node_data, node_id, node_state) {
            let _ = write!(s,"unicode-bidi: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_text_box_trim(node_data, node_id, node_state) {
            let _ = write!(s,"text-box-trim: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_text_box_edge(node_data, node_id, node_state) {
            let _ = write!(s,"text-box-edge: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_dominant_baseline(node_data, node_id, node_state) {
            let _ = write!(s,"dominant-baseline: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_alignment_baseline(node_data, node_id, node_state) {
            let _ = write!(s,"alignment-baseline: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_baseline_source(node_data, node_id, node_state) {
            let _ = write!(s,"baseline-source: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_line_fit_edge(node_data, node_id, node_state) {
            let _ = write!(s,"line-fit-edge: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_initial_letter_align(node_data, node_id, node_state) {
            let _ = write!(s,"initial-letter-align: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_initial_letter_wrap(node_data, node_id, node_state) {
            let _ = write!(s,"initial-letter-wrap: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_scrollbar_gutter(node_data, node_id, node_state) {
            let _ = write!(s,"scrollbar-gutter: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_overflow_clip_margin(node_data, node_id, node_state) {
            let _ = write!(s,"overflow-clip-margin: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_clip(node_data, node_id, node_state) {
            let _ = write!(s,"clip: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_white_space(node_data, node_id, node_state) {
            let _ = write!(s,"white-space: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_display(node_data, node_id, node_state) {
334
            let _ = write!(s,"display: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_float(node_data, node_id, node_state) {
            let _ = write!(s,"float: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_box_sizing(node_data, node_id, node_state) {
            let _ = write!(s,"box-sizing: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_width(node_data, node_id, node_state) {
16
            let _ = write!(s,"width: {};", p.get_css_value_fmt());
318
        }
334
        if let Some(p) = self.get_height(node_data, node_id, node_state) {
15
            let _ = write!(s,"height: {};", p.get_css_value_fmt());
319
        }
334
        if let Some(p) = self.get_min_width(node_data, node_id, node_state) {
            let _ = write!(s,"min-width: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_min_height(node_data, node_id, node_state) {
            let _ = write!(s,"min-height: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_max_width(node_data, node_id, node_state) {
            let _ = write!(s,"max-width: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_max_height(node_data, node_id, node_state) {
            let _ = write!(s,"max-height: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_position(node_data, node_id, node_state) {
            let _ = write!(s,"position: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_top(node_data, node_id, node_state) {
            let _ = write!(s,"top: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_bottom(node_data, node_id, node_state) {
            let _ = write!(s,"bottom: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_right(node_data, node_id, node_state) {
            let _ = write!(s,"right: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_left(node_data, node_id, node_state) {
            let _ = write!(s,"left: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_padding_top(node_data, node_id, node_state) {
            let _ = write!(s,"padding-top: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_padding_bottom(node_data, node_id, node_state) {
            let _ = write!(s,"padding-bottom: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_padding_left(node_data, node_id, node_state) {
            let _ = write!(s,"padding-left: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_padding_right(node_data, node_id, node_state) {
            let _ = write!(s,"padding-right: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_margin_top(node_data, node_id, node_state) {
26
            let _ = write!(s,"margin-top: {};", p.get_css_value_fmt());
308
        }
334
        if let Some(p) = self.get_margin_bottom(node_data, node_id, node_state) {
26
            let _ = write!(s,"margin-bottom: {};", p.get_css_value_fmt());
308
        }
334
        if let Some(p) = self.get_margin_left(node_data, node_id, node_state) {
16
            let _ = write!(s,"margin-left: {};", p.get_css_value_fmt());
318
        }
334
        if let Some(p) = self.get_margin_right(node_data, node_id, node_state) {
16
            let _ = write!(s,"margin-right: {};", p.get_css_value_fmt());
318
        }
334
        if let Some(p) = self.get_border_top_width(node_data, node_id, node_state) {
            let _ = write!(s,"border-top-width: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_border_left_width(node_data, node_id, node_state) {
            let _ = write!(s,"border-left-width: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_border_right_width(node_data, node_id, node_state) {
            let _ = write!(s,"border-right-width: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_border_bottom_width(node_data, node_id, node_state) {
            let _ = write!(s,"border-bottom-width: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_overflow_x(node_data, node_id, node_state) {
            let _ = write!(s,"overflow-x: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_overflow_y(node_data, node_id, node_state) {
            let _ = write!(s,"overflow-y: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_flex_direction(node_data, node_id, node_state) {
10
            let _ = write!(s,"flex-direction: {};", p.get_css_value_fmt());
324
        }
334
        if let Some(p) = self.get_flex_wrap(node_data, node_id, node_state) {
            let _ = write!(s,"flex-wrap: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_flex_grow(node_data, node_id, node_state) {
            let _ = write!(s,"flex-grow: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_flex_shrink(node_data, node_id, node_state) {
            let _ = write!(s,"flex-shrink: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_justify_content(node_data, node_id, node_state) {
            let _ = write!(s,"justify-content: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_align_items(node_data, node_id, node_state) {
            let _ = write!(s,"align-items: {};", p.get_css_value_fmt());
334
        }
334
        if let Some(p) = self.get_align_content(node_data, node_id, node_state) {
            let _ = write!(s,"align-content: {};", p.get_css_value_fmt());
334
        }
334
        s
334
    }
}
#[repr(C)]
#[derive(Debug, PartialEq, Clone)]
pub struct CssPropertyCachePtr {
    // `ManuallyDrop` so the owned `Box` is freed ONLY by our `Drop` (gated on
    // `run_destructor`), never by drop-glue. The codegen Az wrapper (AzStyledDom)
    // nests an AzCssPropertyCachePtr field whose own `Drop` re-runs
    // `_delete` -> `drop_in_place::<CssPropertyCachePtr>` on the SAME bytes; with a
    // bare `Box` the glue freed it a second time -> double free. Layout is
    // unchanged (one pointer), so the AzCssPropertyCachePtr<->CssPropertyCachePtr
    // FFI transmute stays valid. Matches the GlContextPtr / InstantPtr convention.
    pub ptr: ManuallyDrop<Box<CssPropertyCache>>,
    pub run_destructor: bool,
}
impl CssPropertyCachePtr {
39721
    pub fn new(cache: CssPropertyCache) -> Self {
39721
        Self {
39721
            ptr: ManuallyDrop::new(Box::new(cache)),
39721
            run_destructor: true,
39721
        }
39721
    }
7493
    pub fn downcast_mut(&mut self) -> &mut CssPropertyCache {
7493
        &mut self.ptr
7493
    }
}
impl Drop for CssPropertyCachePtr {
41317
    fn drop(&mut self) {
        // First drop (run_destructor still true) frees the Box and clears the flag in
        // the shared bytes; the codegen's redundant second drop sees false -> no-op.
41317
        if self.run_destructor {
41317
            self.run_destructor = false;
41317
            unsafe {
41317
                ManuallyDrop::drop(&mut self.ptr);
41317
            }
        }
41317
    }
}
/// Generates a mechanical `get_<name>` CSS-property accessor: resolve the property
/// for `(node_data, node_id, node_state)` via `get_property`, then downcast it with
/// the given `as_*` method. Covers the long run of one-line accessors below.
macro_rules! impl_get_prop {
    ($name:ident, $value_ty:ty, $variant:ident, $as_method:ident) => {
9531727
        pub fn $name<'a>(
9531727
            &'a self,
9531727
            node_data: &'a NodeData,
9531727
            node_id: &NodeId,
9531727
            node_state: &StyledNodeState,
9531727
        ) -> Option<&'a $value_ty> {
9531727
            self.get_property(node_data, node_id, node_state, &CssPropertyType::$variant)
9531727
                .and_then(|p| p.$as_method())
9531727
        }
    };
}
impl CssPropertyCache {
39845
    #[must_use] pub fn empty(node_count: usize) -> Self {
39845
        Self {
39845
            node_count,
39845
            retained_author_css: Css::default(),
39845
            user_overridden_properties: Vec::new(),
39845
            dynamic_context: None,
39845

            
39845
            cascaded_props: FlatVecVec::new(node_count),
39845
            css_props: FlatVecVec::new(node_count),
39845

            
39845
            computed_values: Vec::new(),
39845
            compact_cache: None,
39845
            global_css_props: Vec::new(),
39845
            resolved_font_sizes_px: crate::sync::OnceLock::new(),
39845
        }
39845
    }
    /// Clear the lazily-populated font-size cache. Call after any
    /// mutation that could change resolved font-sizes (restyle,
    /// DOM mutation, `append`, etc.). The next
    /// [`crate::styled_dom::StyledDom::resolved_font_size_px`] call
    /// repopulates via a single bottom-up tree walk.
37199
    pub fn invalidate_resolved_font_sizes(&mut self) {
37199
        self.resolved_font_sizes_px = crate::sync::OnceLock::new();
37199
    }
244
    pub fn append(&mut self, other: &mut Self) {
244
        self.user_overridden_properties.append(&mut other.user_overridden_properties);
        // The parent's dynamic context wins; a child subtree styled before
        // composition has no window context of its own worth keeping.
244
        if self.dynamic_context.is_none() {
242
            self.dynamic_context = other.dynamic_context.take();
244
        }
244
        self.cascaded_props.extend_from(&mut other.cascaded_props);
244
        self.css_props.extend_from(&mut other.css_props);
244
        self.computed_values.append(&mut other.computed_values);
244
        self.node_count += other.node_count;
        // Indices shifted — invalidate the font-size cache too.
244
        self.resolved_font_sizes_px = crate::sync::OnceLock::new();
        // Invalidate compact cache since node IDs shifted
244
        self.compact_cache = None;
244
    }
3
    pub fn is_horizontal_overflow_visible(
3
        &self,
3
        node_data: &NodeData,
3
        node_id: &NodeId,
3
        node_state: &StyledNodeState,
3
    ) -> bool {
3
        self.get_overflow_x(node_data, node_id, node_state)
3
            .and_then(|p| p.get_property_or_default())
3
            .unwrap_or_default()
3
            .is_overflow_visible()
3
    }
2
    pub fn is_vertical_overflow_visible(
2
        &self,
2
        node_data: &NodeData,
2
        node_id: &NodeId,
2
        node_state: &StyledNodeState,
2
    ) -> bool {
2
        self.get_overflow_y(node_data, node_id, node_state)
2
            .and_then(|p| p.get_property_or_default())
2
            .unwrap_or_default()
2
            .is_overflow_visible()
2
    }
2
    pub fn is_horizontal_overflow_hidden(
2
        &self,
2
        node_data: &NodeData,
2
        node_id: &NodeId,
2
        node_state: &StyledNodeState,
2
    ) -> bool {
2
        self.get_overflow_x(node_data, node_id, node_state)
2
            .and_then(|p| p.get_property_or_default())
2
            .unwrap_or_default()
2
            .is_overflow_hidden()
2
    }
3
    pub fn is_vertical_overflow_hidden(
3
        &self,
3
        node_data: &NodeData,
3
        node_id: &NodeId,
3
        node_state: &StyledNodeState,
3
    ) -> bool {
3
        self.get_overflow_y(node_data, node_id, node_state)
3
            .and_then(|p| p.get_property_or_default())
3
            .unwrap_or_default()
3
            .is_overflow_hidden()
3
    }
    /// The node's USER OVERRIDE for a property, if any — the runtime layer a
    /// transition writes. Public so the display-list builder's inheritance
    /// walk can consult ancestors (the precomputed inherited tables cannot
    /// see a runtime override on an ancestor).
    #[must_use]
1949568
    pub fn get_user_override(
1949568
        &self,
1949568
        node_id: &NodeId,
1949568
        css_property_type: &CssPropertyType,
1949568
    ) -> Option<&CssProperty> {
1949568
        let v = self.user_overridden_properties.get(node_id.index())?;
140
        v.binary_search_by_key(css_property_type, |(k, _)| *k)
140
            .ok()
140
            .map(|idx| &v[idx].1)
1949568
    }
    /// Does this node DECLARE the property itself (inline style or a matched
    /// stylesheet rule, any pseudo-state)? Inherited values do NOT count —
    /// this is the "inheritance re-roots here" test for the ancestor-override
    /// walk: below a node with its own `color`, an animated ancestor colour
    /// must not leak through.
    #[must_use]
1949567
    pub fn has_own_declaration(
1949567
        &self,
1949567
        node_data: &NodeData,
1949567
        node_id: &NodeId,
1949567
        css_property_type: &CssPropertyType,
1949567
    ) -> bool {
1949567
        if node_data
1949567
            .style
1949567
            .iter_inline_properties()
1949567
            .any(|(p, _)| p.get_type() == *css_property_type)
        {
37873
            return true;
1911694
        }
1911694
        self.css_props
1911694
            .get_slice(node_id.index())
1911694
            .iter()
3098863
            .any(|p| p.prop_type == *css_property_type)
1949567
    }
641338
    pub fn get_text_color_or_default(
641338
        &self,
641338
        node_data: &NodeData,
641338
        node_id: &NodeId,
641338
        node_state: &StyledNodeState,
641338
    ) -> StyleTextColor {
        use azul_css::defaults::DEFAULT_TEXT_COLOR;
641338
        self.get_text_color(node_data, node_id, node_state)
641338
            .and_then(|fs| fs.get_property().copied())
641338
            .unwrap_or(DEFAULT_TEXT_COLOR)
641338
    }
    /// Returns the font family of the node, or the default font family if none is set.
38
    pub fn get_font_id_or_default(
38
        &self,
38
        node_data: &NodeData,
38
        node_id: &NodeId,
38
        node_state: &StyledNodeState,
38
    ) -> StyleFontFamilyVec {
        use azul_css::defaults::DEFAULT_FONT_ID;
38
        let default_font_id = vec![StyleFontFamily::System(AzString::from_const_str(
38
            DEFAULT_FONT_ID,
38
        ))]
38
        .into();
38
        let font_family_opt = self.get_font_family(node_data, node_id, node_state);
38
        font_family_opt
38
            .as_ref()
38
            .and_then(|family| Some(family.get_property()?.clone()))
38
            .unwrap_or(default_font_id)
38
    }
3
    pub fn get_font_size_or_default(
3
        &self,
3
        node_data: &NodeData,
3
        node_id: &NodeId,
3
        node_state: &StyledNodeState,
3
    ) -> StyleFontSize {
        use azul_css::defaults::DEFAULT_FONT_SIZE;
3
        self.get_font_size(node_data, node_id, node_state)
3
            .and_then(|fs| fs.get_property().copied())
3
            .unwrap_or(DEFAULT_FONT_SIZE)
3
    }
2
    pub fn has_border(
2
        &self,
2
        node_data: &NodeData,
2
        node_id: &NodeId,
2
        node_state: &StyledNodeState,
2
    ) -> bool {
2
        self.get_border_left_width(node_data, node_id, node_state)
2
            .is_some()
1
            || self
1
                .get_border_right_width(node_data, node_id, node_state)
1
                .is_some()
1
            || self
1
                .get_border_top_width(node_data, node_id, node_state)
1
                .is_some()
1
            || self
1
                .get_border_bottom_width(node_data, node_id, node_state)
1
                .is_some()
2
    }
2
    pub fn has_box_shadow(
2
        &self,
2
        node_data: &NodeData,
2
        node_id: &NodeId,
2
        node_state: &StyledNodeState,
2
    ) -> bool {
2
        self.get_box_shadow_left(node_data, node_id, node_state)
2
            .is_some()
2
            || self
2
                .get_box_shadow_right(node_data, node_id, node_state)
2
                .is_some()
2
            || self
2
                .get_box_shadow_top(node_data, node_id, node_state)
2
                .is_some()
2
            || self
2
                .get_box_shadow_bottom(node_data, node_id, node_state)
2
                .is_some()
2
    }
9608351
    pub fn get_property<'a>(
9608351
        &'a self,
9608351
        node_data: &'a NodeData,
9608351
        node_id: &NodeId,
9608351
        node_state: &StyledNodeState,
9608351
        css_property_type: &CssPropertyType,
9608351
    ) -> Option<&'a CssProperty> {
        // Thread-local counter of cascade walks, broken down by
        // property type. Drain with `drain_css_prop_counts` (free
        // fn below) when `AZ_PROP_COUNT=1` is set to see which
        // properties dominate the cold layout path.
        //
        // Env check is read ONCE at process start and cached in a
        // `OnceLock<bool>`. Before this, the env check ran per
        // `get_property` call — and the function fires 710k+ times
        // per cold layout on excel.html. `std::env::var_os` takes
        // ~100 ns per call on macOS (env lock + hashmap lookup), so
        // the naive check added ~70 ms of pure noise to every
        // single layout, regardless of whether the env var was set.
        // Using a one-time cached bool removes that overhead.
        //
        // `no_std` builds have no thread-locals / env, so the profiling
        // counter is compiled out entirely.
        #[cfg(feature = "std")]
        {
            static PROP_COUNT_ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
9608351
            let enabled = *PROP_COUNT_ENABLED.get_or_init(crate::profile::cascade_enabled);
9608351
            if enabled {
                // `try_with` (not `with`): the lifted-to-wasm web backend has no
                // real TLS, so `with` would hit `panic_access_error` (the layout
                // path reads CSS props via these getters → would trap). `try_with`
                // returns Err and we skip the profiling-only increment (and its
                // inner Mutex-guarded label table). Desktop behaviour unchanged —
                // when the env var is unset the whole block is gated off anyway.
                let _ = PROP_COUNTS.try_with(|c| {
                    *c.borrow_mut()
                        .entry(Self::css_prop_type_label(css_property_type))
                        .or_insert(0) += 1;
                });
9608351
            }
        }
        // Always use full cascade resolution.
        // Tier 1/2/2b handle layout-hot properties via direct typed getters.
        // This path is only used for paint-time reads (background, shadow, etc.)
9608351
        self.get_property_slow(node_data, node_id, node_state, css_property_type)
9608351
    }
    #[cfg(feature = "std")]
    #[allow(clippy::trivially_copy_pass_by_ref)] // uniform by-ref cascade-API convention (see find_in_stateful)
3
    fn css_prop_type_label(t: &CssPropertyType) -> &'static str {
        // Intern Debug-format labels under a mutex-guarded map so
        // we leak at most one `&'static str` per distinct
        // `CssPropertyType` variant (bounded at ≤ 178 total). Only
        // triggered when `AZ_PROP_COUNT=1`, so zero cost normally.
        use std::sync::{Mutex, OnceLock};
        static TABLE: OnceLock<Mutex<std::collections::HashMap<CssPropertyType, &'static str>>> =
            OnceLock::new();
3
        let m = TABLE.get_or_init(|| Mutex::new(std::collections::HashMap::new()));
3
        let mut g = m.lock().expect("AZ_PROP_COUNT label table poisoned");
3
        if let Some(s) = g.get(t) {
1
            return s;
2
        }
2
        let s: String = std::format!("{t:?}");
2
        let leaked: &'static str = std::boxed::Box::leak(s.into_boxed_str());
2
        g.insert(*t, leaked);
2
        leaked
3
    }
    /// Full cascade resolution for any CSS property type.
    /// Walks all cascade layers: user overrides → inline → stylesheet → cascaded → computed → UA.
    /// Also used by restyle functions that need state-aware lookups.
    #[allow(clippy::trivially_copy_pass_by_ref)] // uniform by-ref cascade-API convention (see find_in_stateful)
    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
9609573
    pub(crate) fn get_property_slow<'a>(
9609573
        &'a self,
9609573
        node_data: &'a NodeData,
9609573
        node_id: &NodeId,
9609573
        node_state: &StyledNodeState,
9609573
        css_property_type: &CssPropertyType,
9609573
    ) -> Option<&'a CssProperty> {
        use azul_css::dynamic_selector::{DynamicSelector, PseudoStateType};
        // Helper: do these conditions identify a rule that applies in `state`
        // under the window's dynamic context? Empty conditions = Normal-only.
        // Otherwise EVERY condition must hold: a pseudo-state condition must
        // equal `state`, and every other condition (viewport/@media, theme,
        // OS, container...) is evaluated against the window-provided
        // `dynamic_context`. With no context yet (a StyledDom no window has
        // adopted), non-pseudo conditions do not apply - the exact behaviour
        // they had before contexts were wired through, so creation-time
        // styling is unchanged.
9609573
        let ctx = self.dynamic_context.as_deref();
9609573
        let matches_pseudo_state = |conds: &azul_css::dynamic_selector::DynamicSelectorVec,
                                    state: PseudoStateType|
5541906
         -> bool {
5541906
            let conditions = conds.as_slice();
5541906
            if conditions.is_empty() {
5077139
                state == PseudoStateType::Normal
            } else {
464767
                conditions.iter().all(|c| match c {
459377
                    DynamicSelector::PseudoState(s) => *s == state,
5390
                    non_pseudo => ctx.is_some_and(|ctx| non_pseudo.matches(ctx)),
464767
                })
            }
5541906
        };
        // First test if there is some user-defined override for the property
9609573
        if let Some(v) = self.user_overridden_properties.get(node_id.index()) {
5021
            if let Ok(idx) = v.binary_search_by_key(css_property_type, |(k, _)| *k) {
263
                return Some(&v[idx].1);
4758
            }
9604552
        }
        // If that fails, see if there is an inline CSS property that matches
        // :focus > :active > :hover > normal (fallback)
9609310
        if node_state.focused {
            // PRIORITY 1: Inline CSS properties (highest priority per CSS spec)
2147
            if let Some(p) = node_data.style.iter_inline_properties().fold(None, |acc, (prop, conds)| {
1292
                if matches_pseudo_state(conds, PseudoStateType::Focus)
342
                    && prop.get_type() == *css_property_type
                {
                    // LAST matching inline declaration wins (CSS source order),
                    // same as the compact builder's later-overwrites-earlier and
                    // get_property_with_context - a widget's merged_style()
                    // appends overrides and relies on exactly this.
342
                    Some(prop)
                } else {
950
                    acc
                }
1292
            }) {
342
                return Some(p);
855
            }
            // PRIORITY 2: CSS stylesheet properties
855
            if let Some(p) = Self::find_in_stateful(
855
                self.css_props.get_slice(node_id.index()),
855
                PseudoStateType::Focus,
855
                css_property_type,
855
            ) {
                return Some(p);
855
            }
            // PRIORITY 3: Cascaded/inherited properties
855
            if let Some(p) = Self::find_in_stateful(
855
                self.cascaded_props.get_slice(node_id.index()),
855
                PseudoStateType::Focus,
855
                css_property_type,
855
            ) {
                return Some(p);
855
            }
9608113
        }
9608968
        if node_state.active {
            // PRIORITY 1: Inline CSS properties (highest priority per CSS spec)
380
            if let Some(p) = node_data.style.iter_inline_properties().fold(None, |acc, (prop, conds)| {
380
                if matches_pseudo_state(conds, PseudoStateType::Active)
95
                    && prop.get_type() == *css_property_type
                {
                    // LAST matching inline declaration wins (CSS source order),
                    // same as the compact builder's later-overwrites-earlier and
                    // get_property_with_context - a widget's merged_style()
                    // appends overrides and relies on exactly this.
95
                    Some(prop)
                } else {
285
                    acc
                }
380
            }) {
95
                return Some(p);
            }
            // PRIORITY 2: CSS stylesheet properties
            if let Some(p) = Self::find_in_stateful(
                self.css_props.get_slice(node_id.index()),
                PseudoStateType::Active,
                css_property_type,
            ) {
                return Some(p);
            }
            // PRIORITY 3: Cascaded/inherited properties
            if let Some(p) = Self::find_in_stateful(
                self.cascaded_props.get_slice(node_id.index()),
                PseudoStateType::Active,
                css_property_type,
            ) {
                return Some(p);
            }
9608873
        }
        // :dragging pseudo-state (higher priority than :hover)
9608873
        if node_state.dragging {
            if let Some(p) = node_data.style.iter_inline_properties().fold(None, |acc, (prop, conds)| {
                if matches_pseudo_state(conds, PseudoStateType::Dragging)
                    && prop.get_type() == *css_property_type
                {
                    // LAST matching inline declaration wins (CSS source order),
                    // same as the compact builder's later-overwrites-earlier and
                    // get_property_with_context - a widget's merged_style()
                    // appends overrides and relies on exactly this.
                    Some(prop)
                } else {
                    acc
                }
            }) {
                return Some(p);
            }
            if let Some(p) = Self::find_in_stateful(
                self.css_props.get_slice(node_id.index()),
                PseudoStateType::Dragging,
                css_property_type,
            ) {
                return Some(p);
            }
            if let Some(p) = Self::find_in_stateful(
                self.cascaded_props.get_slice(node_id.index()),
                PseudoStateType::Dragging,
                css_property_type,
            ) {
                return Some(p);
            }
9608873
        }
        // :drag-over pseudo-state (higher priority than :hover)
9608873
        if node_state.drag_over {
            if let Some(p) = node_data.style.iter_inline_properties().fold(None, |acc, (prop, conds)| {
                if matches_pseudo_state(conds, PseudoStateType::DragOver)
                    && prop.get_type() == *css_property_type
                {
                    // LAST matching inline declaration wins (CSS source order),
                    // same as the compact builder's later-overwrites-earlier and
                    // get_property_with_context - a widget's merged_style()
                    // appends overrides and relies on exactly this.
                    Some(prop)
                } else {
                    acc
                }
            }) {
                return Some(p);
            }
            if let Some(p) = Self::find_in_stateful(
                self.css_props.get_slice(node_id.index()),
                PseudoStateType::DragOver,
                css_property_type,
            ) {
                return Some(p);
            }
            if let Some(p) = Self::find_in_stateful(
                self.cascaded_props.get_slice(node_id.index()),
                PseudoStateType::DragOver,
                css_property_type,
            ) {
                return Some(p);
            }
9608873
        }
9608873
        if node_state.hover {
            // PRIORITY 1: Inline CSS properties (highest priority per CSS spec)
4581
            if let Some(p) = node_data.style.iter_inline_properties().fold(None, |acc, (prop, conds)| {
533
                if matches_pseudo_state(conds, PseudoStateType::Hover)
134
                    && prop.get_type() == *css_property_type
                {
                    // LAST matching inline declaration wins (CSS source order),
                    // same as the compact builder's later-overwrites-earlier and
                    // get_property_with_context - a widget's merged_style()
                    // appends overrides and relies on exactly this.
134
                    Some(prop)
                } else {
399
                    acc
                }
533
            }) {
134
                return Some(p);
4048
            }
            // PRIORITY 2: CSS stylesheet properties
4048
            if let Some(p) = Self::find_in_stateful(
4048
                self.css_props.get_slice(node_id.index()),
4048
                PseudoStateType::Hover,
4048
                css_property_type,
4048
            ) {
                return Some(p);
4048
            }
            // PRIORITY 3: Cascaded/inherited properties
4048
            if let Some(p) = Self::find_in_stateful(
4048
                self.cascaded_props.get_slice(node_id.index()),
4048
                PseudoStateType::Hover,
4048
                css_property_type,
4048
            ) {
                return Some(p);
4048
            }
9604691
        }
        // Normal/fallback properties - always apply as base layer
        // PRIORITY 1: Inline CSS properties (highest priority per CSS spec)
9609461
        if let Some(p) = node_data.style.iter_inline_properties().fold(None, |acc, (prop, conds)| {
5539701
            if matches_pseudo_state(conds, PseudoStateType::Normal)
5078428
                && prop.get_type() == *css_property_type
            {
                // LAST matching inline declaration wins (CSS source order),
                // same as the compact builder's later-overwrites-earlier and
                // get_property_with_context - the widget pattern
                // "display:none + display:flex @media(max-width)" relies on
                // exactly this ordering.
120907
                Some(prop)
            } else {
5418794
                acc
            }
5539701
        }) {
120907
            return Some(p);
9487832
        }
        // PRIORITY 2: CSS stylesheet properties
9487832
        if let Some(p) = Self::find_in_stateful(
9487832
            self.css_props.get_slice(node_id.index()),
9487832
            PseudoStateType::Normal,
9487832
            css_property_type,
9487832
        ) {
293257
            return Some(p);
9194575
        }
        // PRIORITY 2b: Global `*` selector properties (specificity 0,0,0)
        // These are collected once during restyle and apply to all nodes.
        // Lower priority than per-node rules but higher than inheritance/UA.
15933615
        if let Some(p) = self.global_css_props.iter().find(|p| p.get_type() == *css_property_type) {
1
            return Some(p);
9194574
        }
        // PRIORITY 3: Cascaded/inherited properties
9194574
        if let Some(p) = Self::find_in_stateful(
9194574
            self.cascaded_props.get_slice(node_id.index()),
9194574
            PseudoStateType::Normal,
9194574
            css_property_type,
9194574
        ) {
797069
            return Some(p);
8397505
        }
        // Check computed values cache for inherited properties
        // Sorted Vec with binary search
8397505
        if css_property_type.is_inheritable() {
7479362
            if let Some(vec) = self.computed_values.get(node_id.index()) {
7478291
                if let Ok(idx) = vec.binary_search_by_key(css_property_type, |(k, _)| *k) {
95288
                    return Some(&vec[idx].1.property);
7383003
                }
1071
            }
918143
        }
        // User-agent stylesheet fallback (lowest precedence)
        // Check if the node type has a default value for this property
8302217
        crate::ua_css::get_ua_property(&node_data.node_type, *css_property_type)
9609573
    }
    /// Get a CSS property using `DynamicSelectorContext` for evaluation.
    ///
    /// This is the new API that supports @media queries, @container queries,
    /// OS-specific styles, and all pseudo-states via `CssPropertyWithConditions`.
    ///
    /// The evaluation follows "last wins" semantics - properties are evaluated
    /// in reverse order and the first matching property wins.
    #[allow(clippy::trivially_copy_pass_by_ref)] // uniform by-ref cascade-API convention (see find_in_stateful)
2
    pub(crate) fn get_property_with_context<'a>(
2
        &'a self,
2
        node_data: &'a NodeData,
2
        node_id: &NodeId,
2
        context: &DynamicSelectorContext,
2
        css_property_type: &CssPropertyType,
2
    ) -> Option<&'a CssProperty> {
        // First test if there is some user-defined override for the property
2
        if let Some(v) = self.user_overridden_properties.get(node_id.index()) {
            if let Ok(idx) = v.binary_search_by_key(css_property_type, |(k, _)| *k) {
                return Some(&v[idx].1);
            }
2
        }
        // Check inline CSS properties with DynamicSelectorContext evaluation.
        // Iterate in REVERSE order across the flat (prop, conds) view —
        // "last found wins" semantics, replacing the old Focus > Active >
        // Hover > Normal priority chain.
        // "last found wins": scan the flat (prop, conds) view forward and keep the
        // last match (iter_inline_properties is not DoubleEndedIterator, so this
        // replaces an earlier collect-then-rev-find_map).
2
        let mut last_inline = None;
2
        for (prop, conds) in node_data.style.iter_inline_properties() {
2
            let conditions_match = conds.as_slice().iter().all(|c| c.matches(context));
2
            if prop.get_type() == *css_property_type && conditions_match {
1
                last_inline = Some(prop);
1
            }
        }
2
        if let Some(prop) = last_inline {
1
            return Some(prop);
1
        }
        // Fall back to CSS file and cascaded properties
1
        let legacy_state = StyledNodeState::from_pseudo_state_flags(&context.pseudo_state);
1
        if let Some(p) = self.get_property(node_data, node_id, &legacy_state, css_property_type) {
            return Some(p);
1
        }
1
        None
2
    }
    /// Check if any properties with conditions would change between two contexts.
    /// This is used for re-layout detection on viewport/container resize.
5
    pub(crate) fn check_properties_changed(
5
        node_data: &NodeData,
5
        old_context: &DynamicSelectorContext,
5
        new_context: &DynamicSelectorContext,
5
    ) -> bool {
5
        for (_prop, conds) in node_data.style.iter_inline_properties() {
4
            let was_active = conds.as_slice().iter().all(|c| c.matches(old_context));
4
            let is_active = conds.as_slice().iter().all(|c| c.matches(new_context));
4
            if was_active != is_active {
2
                return true;
2
            }
        }
3
        false
5
    }
    /// Check if any layout-affecting properties would change between two contexts.
    /// This is a more targeted check for re-layout detection.
2
    pub(crate) fn check_layout_properties_changed(
2
        node_data: &NodeData,
2
        old_context: &DynamicSelectorContext,
2
        new_context: &DynamicSelectorContext,
2
    ) -> bool {
2
        for (prop, conds) in node_data.style.iter_inline_properties() {
            // Skip non-layout-affecting properties
2
            if !prop.get_type().can_trigger_relayout() {
1
                continue;
1
            }
1
            let was_active = conds.as_slice().iter().all(|c| c.matches(old_context));
1
            let is_active = conds.as_slice().iter().all(|c| c.matches(new_context));
1
            if was_active != is_active {
1
                return true;
            }
        }
1
        false
2
    }
    impl_get_prop!(get_background_content, StyleBackgroundContentVecValue, BackgroundContent, as_background_content);
    impl_get_prop!(get_hyphens, StyleHyphensValue, Hyphens, as_hyphens);
    impl_get_prop!(get_word_break, StyleWordBreakValue, WordBreak, as_word_break);
    impl_get_prop!(get_overflow_wrap, StyleOverflowWrapValue, OverflowWrap, as_overflow_wrap);
    impl_get_prop!(get_line_break, StyleLineBreakValue, LineBreak, as_line_break);
    impl_get_prop!(get_text_align_last, StyleTextAlignLastValue, TextAlignLast, as_text_align_last);
    impl_get_prop!(get_text_transform, StyleTextTransformValue, TextTransform, as_text_transform);
    impl_get_prop!(get_object_fit, StyleObjectFitValue, ObjectFit, as_object_fit);
    impl_get_prop!(get_text_overflow, StyleTextOverflowValue, TextOverflow, as_text_overflow);
    impl_get_prop!(get_text_orientation, StyleTextOrientationValue, TextOrientation, as_text_orientation);
    impl_get_prop!(get_object_position, StyleObjectPositionValue, ObjectPosition, as_object_position);
    impl_get_prop!(get_aspect_ratio, StyleAspectRatioValue, AspectRatio, as_aspect_ratio);
    impl_get_prop!(get_direction, StyleDirectionValue, Direction, as_direction);
    impl_get_prop!(get_unicode_bidi, StyleUnicodeBidiValue, UnicodeBidi, as_unicode_bidi);
    impl_get_prop!(get_text_box_trim, StyleTextBoxTrimValue, TextBoxTrim, as_text_box_trim);
    impl_get_prop!(get_text_box_edge, StyleTextBoxEdgeValue, TextBoxEdge, as_text_box_edge);
    impl_get_prop!(get_dominant_baseline, StyleDominantBaselineValue, DominantBaseline, as_dominant_baseline);
    impl_get_prop!(get_alignment_baseline, StyleAlignmentBaselineValue, AlignmentBaseline, as_alignment_baseline);
    impl_get_prop!(get_baseline_source, StyleBaselineSourceValue, BaselineSource, as_baseline_source);
    impl_get_prop!(get_line_fit_edge, StyleLineFitEdgeValue, LineFitEdge, as_line_fit_edge);
    impl_get_prop!(get_initial_letter_align, StyleInitialLetterAlignValue, InitialLetterAlign, as_initial_letter_align);
    impl_get_prop!(get_initial_letter_wrap, StyleInitialLetterWrapValue, InitialLetterWrap, as_initial_letter_wrap);
    impl_get_prop!(get_scrollbar_gutter, StyleScrollbarGutterValue, ScrollbarGutter, as_scrollbar_gutter);
    impl_get_prop!(get_overflow_clip_margin, StyleOverflowClipMarginValue, OverflowClipMargin, as_overflow_clip_margin);
    impl_get_prop!(get_clip, StyleClipRectValue, Clip, as_clip);
    impl_get_prop!(get_white_space, StyleWhiteSpaceValue, WhiteSpace, as_white_space);
    impl_get_prop!(get_background_position, StyleBackgroundPositionVecValue, BackgroundPosition, as_background_position);
    impl_get_prop!(get_background_size, StyleBackgroundSizeVecValue, BackgroundSize, as_background_size);
    impl_get_prop!(get_background_repeat, StyleBackgroundRepeatVecValue, BackgroundRepeat, as_background_repeat);
    impl_get_prop!(get_font_size, StyleFontSizeValue, FontSize, as_font_size);
    impl_get_prop!(get_font_family, StyleFontFamilyVecValue, FontFamily, as_font_family);
    impl_get_prop!(get_font_weight, StyleFontWeightValue, FontWeight, as_font_weight);
    impl_get_prop!(get_font_style, StyleFontStyleValue, FontStyle, as_font_style);
    impl_get_prop!(get_text_color, StyleTextColorValue, TextColor, as_text_color);
    impl_get_prop!(get_text_indent, StyleTextIndentValue, TextIndent, as_text_indent);
    impl_get_prop!(get_initial_letter, StyleInitialLetterValue, InitialLetter, as_initial_letter);
    impl_get_prop!(get_line_clamp, StyleLineClampValue, LineClamp, as_line_clamp);
    impl_get_prop!(get_hanging_punctuation, StyleHangingPunctuationValue, HangingPunctuation, as_hanging_punctuation);
    impl_get_prop!(get_text_combine_upright, StyleTextCombineUprightValue, TextCombineUpright, as_text_combine_upright);
    impl_get_prop!(get_exclusion_margin, StyleExclusionMarginValue, ExclusionMargin, as_exclusion_margin);
    impl_get_prop!(get_hyphenation_language, StyleHyphenationLanguageValue, HyphenationLanguage, as_hyphenation_language);
    impl_get_prop!(get_caret_color, CaretColorValue, CaretColor, as_caret_color);
    impl_get_prop!(get_caret_width, CaretWidthValue, CaretWidth, as_caret_width);
    impl_get_prop!(get_caret_animation_duration, CaretAnimationDurationValue, CaretAnimationDuration, as_caret_animation_duration);
    impl_get_prop!(get_selection_background_color, SelectionBackgroundColorValue, SelectionBackgroundColor, as_selection_background_color);
    impl_get_prop!(get_selection_color, SelectionColorValue, SelectionColor, as_selection_color);
    impl_get_prop!(get_selection_radius, SelectionRadiusValue, SelectionRadius, as_selection_radius);
    impl_get_prop!(get_text_justify, LayoutTextJustifyValue, TextJustify, as_text_justify);
    impl_get_prop!(get_z_index, LayoutZIndexValue, ZIndex, as_z_index);
    impl_get_prop!(get_flex_basis, LayoutFlexBasisValue, FlexBasis, as_flex_basis);
    impl_get_prop!(get_column_gap, LayoutColumnGapValue, ColumnGap, as_column_gap);
    impl_get_prop!(get_row_gap, LayoutRowGapValue, RowGap, as_row_gap);
    impl_get_prop!(get_grid_template_columns, LayoutGridTemplateColumnsValue, GridTemplateColumns, as_grid_template_columns);
    impl_get_prop!(get_grid_template_rows, LayoutGridTemplateRowsValue, GridTemplateRows, as_grid_template_rows);
    impl_get_prop!(get_grid_auto_columns, LayoutGridAutoColumnsValue, GridAutoColumns, as_grid_auto_columns);
    impl_get_prop!(get_grid_auto_rows, LayoutGridAutoRowsValue, GridAutoRows, as_grid_auto_rows);
    impl_get_prop!(get_grid_column, LayoutGridColumnValue, GridColumn, as_grid_column);
    impl_get_prop!(get_grid_row, LayoutGridRowValue, GridRow, as_grid_row);
    impl_get_prop!(get_grid_auto_flow, LayoutGridAutoFlowValue, GridAutoFlow, as_grid_auto_flow);
    impl_get_prop!(get_justify_self, LayoutJustifySelfValue, JustifySelf, as_justify_self);
    impl_get_prop!(get_justify_items, LayoutJustifyItemsValue, JustifyItems, as_justify_items);
    impl_get_prop!(get_gap, LayoutGapValue, Gap, as_gap);
    /// Method for getting grid-gap property
    #[allow(clippy::trivially_copy_pass_by_ref)] // uniform by-ref cascade-API convention (see find_in_stateful)
2
    pub(crate) fn get_grid_gap<'a>(
2
        &'a self,
2
        node_data: &'a NodeData,
2
        node_id: &NodeId,
2
        node_state: &StyledNodeState,
2
    ) -> Option<&'a LayoutGapValue> {
2
        self.get_property(node_data, node_id, node_state, &CssPropertyType::GridGap)
2
            .and_then(|p| p.as_grid_gap())
2
    }
    impl_get_prop!(get_align_self, LayoutAlignSelfValue, AlignSelf, as_align_self);
    impl_get_prop!(get_font, StyleFontValue, Font, as_font);
    impl_get_prop!(get_writing_mode, LayoutWritingModeValue, WritingMode, as_writing_mode);
    impl_get_prop!(get_clear, LayoutClearValue, Clear, as_clear);
    impl_get_prop!(get_shape_outside, ShapeOutsideValue, ShapeOutside, as_shape_outside);
    impl_get_prop!(get_shape_inside, ShapeInsideValue, ShapeInside, as_shape_inside);
    impl_get_prop!(get_clip_path, ClipPathValue, ClipPath, as_clip_path);
    /// Method for getting scrollbar track background
44
    pub fn get_scrollbar_track<'a>(
44
        &'a self,
44
        node_data: &'a NodeData,
44
        node_id: &NodeId,
44
        node_state: &StyledNodeState,
44
    ) -> Option<&'a StyleBackgroundContentValue> {
44
        self.get_property(node_data, node_id, node_state, &CssPropertyType::ScrollbarTrack)
44
            .and_then(|p| p.as_scrollbar_track())
44
    }
    /// Method for getting scrollbar thumb background
45
    pub fn get_scrollbar_thumb<'a>(
45
        &'a self,
45
        node_data: &'a NodeData,
45
        node_id: &NodeId,
45
        node_state: &StyledNodeState,
45
    ) -> Option<&'a StyleBackgroundContentValue> {
45
        self.get_property(node_data, node_id, node_state, &CssPropertyType::ScrollbarThumb)
45
            .and_then(|p| p.as_scrollbar_thumb())
45
    }
    /// Method for getting scrollbar button background
44
    pub fn get_scrollbar_button<'a>(
44
        &'a self,
44
        node_data: &'a NodeData,
44
        node_id: &NodeId,
44
        node_state: &StyledNodeState,
44
    ) -> Option<&'a StyleBackgroundContentValue> {
44
        self.get_property(node_data, node_id, node_state, &CssPropertyType::ScrollbarButton)
44
            .and_then(|p| p.as_scrollbar_button())
44
    }
    /// Method for getting scrollbar corner background
44
    pub fn get_scrollbar_corner<'a>(
44
        &'a self,
44
        node_data: &'a NodeData,
44
        node_id: &NodeId,
44
        node_state: &StyledNodeState,
44
    ) -> Option<&'a StyleBackgroundContentValue> {
44
        self.get_property(node_data, node_id, node_state, &CssPropertyType::ScrollbarCorner)
44
            .and_then(|p| p.as_scrollbar_corner())
44
    }
    /// Method for getting scrollbar resizer background
1
    pub fn get_scrollbar_resizer<'a>(
1
        &'a self,
1
        node_data: &'a NodeData,
1
        node_id: &NodeId,
1
        node_state: &StyledNodeState,
1
    ) -> Option<&'a StyleBackgroundContentValue> {
1
        self.get_property(node_data, node_id, node_state, &CssPropertyType::ScrollbarResizer)
1
            .and_then(|p| p.as_scrollbar_resizer())
1
    }
    impl_get_prop!(get_scrollbar_width, LayoutScrollbarWidthValue, ScrollbarWidth, as_scrollbar_width);
    impl_get_prop!(get_scrollbar_color, StyleScrollbarColorValue, ScrollbarColor, as_scrollbar_color);
    impl_get_prop!(get_scrollbar_visibility, ScrollbarVisibilityModeValue, ScrollbarVisibility, as_scrollbar_visibility);
    impl_get_prop!(get_scrollbar_fade_delay, ScrollbarFadeDelayValue, ScrollbarFadeDelay, as_scrollbar_fade_delay);
    impl_get_prop!(get_scrollbar_fade_duration, ScrollbarFadeDurationValue, ScrollbarFadeDuration, as_scrollbar_fade_duration);
    impl_get_prop!(get_visibility, StyleVisibilityValue, Visibility, as_visibility);
    impl_get_prop!(get_break_before, PageBreakValue, BreakBefore, as_break_before);
    impl_get_prop!(get_break_after, PageBreakValue, BreakAfter, as_break_after);
    impl_get_prop!(get_break_inside, BreakInsideValue, BreakInside, as_break_inside);
    impl_get_prop!(get_orphans, OrphansValue, Orphans, as_orphans);
    impl_get_prop!(get_widows, WidowsValue, Widows, as_widows);
    impl_get_prop!(get_box_decoration_break, BoxDecorationBreakValue, BoxDecorationBreak, as_box_decoration_break);
    impl_get_prop!(get_column_count, ColumnCountValue, ColumnCount, as_column_count);
    impl_get_prop!(get_column_width, ColumnWidthValue, ColumnWidth, as_column_width);
    impl_get_prop!(get_column_span, ColumnSpanValue, ColumnSpan, as_column_span);
    impl_get_prop!(get_column_fill, ColumnFillValue, ColumnFill, as_column_fill);
    impl_get_prop!(get_column_rule_width, ColumnRuleWidthValue, ColumnRuleWidth, as_column_rule_width);
    impl_get_prop!(get_column_rule_style, ColumnRuleStyleValue, ColumnRuleStyle, as_column_rule_style);
    impl_get_prop!(get_column_rule_color, ColumnRuleColorValue, ColumnRuleColor, as_column_rule_color);
    impl_get_prop!(get_flow_into, FlowIntoValue, FlowInto, as_flow_into);
    impl_get_prop!(get_flow_from, FlowFromValue, FlowFrom, as_flow_from);
    impl_get_prop!(get_shape_margin, ShapeMarginValue, ShapeMargin, as_shape_margin);
    impl_get_prop!(get_shape_image_threshold, ShapeImageThresholdValue, ShapeImageThreshold, as_shape_image_threshold);
    impl_get_prop!(get_content, ContentValue, Content, as_content);
    impl_get_prop!(get_counter_reset, CounterResetValue, CounterReset, as_counter_reset);
    impl_get_prop!(get_counter_increment, CounterIncrementValue, CounterIncrement, as_counter_increment);
    impl_get_prop!(get_string_set, StringSetValue, StringSet, as_string_set);
    impl_get_prop!(get_text_align, StyleTextAlignValue, TextAlign, as_text_align);
    impl_get_prop!(get_user_select, StyleUserSelectValue, UserSelect, as_user_select);
    impl_get_prop!(get_text_decoration, StyleTextDecorationValue, TextDecoration, as_text_decoration);
    impl_get_prop!(get_vertical_align, StyleVerticalAlignValue, VerticalAlign, as_vertical_align);
    impl_get_prop!(get_line_height, StyleLineHeightValue, LineHeight, as_line_height);
    impl_get_prop!(get_letter_spacing, StyleLetterSpacingValue, LetterSpacing, as_letter_spacing);
    impl_get_prop!(get_word_spacing, StyleWordSpacingValue, WordSpacing, as_word_spacing);
    impl_get_prop!(get_tab_size, StyleTabSizeValue, TabSize, as_tab_size);
    impl_get_prop!(get_cursor, StyleCursorValue, Cursor, as_cursor);
    impl_get_prop!(get_box_shadow_left, StyleBoxShadowValue, BoxShadowLeft, as_box_shadow_left);
    impl_get_prop!(get_box_shadow_right, StyleBoxShadowValue, BoxShadowRight, as_box_shadow_right);
    impl_get_prop!(get_box_shadow_top, StyleBoxShadowValue, BoxShadowTop, as_box_shadow_top);
    impl_get_prop!(get_box_shadow_bottom, StyleBoxShadowValue, BoxShadowBottom, as_box_shadow_bottom);
    impl_get_prop!(get_border_top_color, StyleBorderTopColorValue, BorderTopColor, as_border_top_color);
    impl_get_prop!(get_border_left_color, StyleBorderLeftColorValue, BorderLeftColor, as_border_left_color);
    impl_get_prop!(get_border_right_color, StyleBorderRightColorValue, BorderRightColor, as_border_right_color);
    impl_get_prop!(get_border_bottom_color, StyleBorderBottomColorValue, BorderBottomColor, as_border_bottom_color);
    impl_get_prop!(get_border_top_style, StyleBorderTopStyleValue, BorderTopStyle, as_border_top_style);
    impl_get_prop!(get_border_left_style, StyleBorderLeftStyleValue, BorderLeftStyle, as_border_left_style);
    impl_get_prop!(get_border_right_style, StyleBorderRightStyleValue, BorderRightStyle, as_border_right_style);
    impl_get_prop!(get_border_bottom_style, StyleBorderBottomStyleValue, BorderBottomStyle, as_border_bottom_style);
    impl_get_prop!(get_border_top_left_radius, StyleBorderTopLeftRadiusValue, BorderTopLeftRadius, as_border_top_left_radius);
    impl_get_prop!(get_border_top_right_radius, StyleBorderTopRightRadiusValue, BorderTopRightRadius, as_border_top_right_radius);
    impl_get_prop!(get_border_bottom_left_radius, StyleBorderBottomLeftRadiusValue, BorderBottomLeftRadius, as_border_bottom_left_radius);
    impl_get_prop!(get_border_bottom_right_radius, StyleBorderBottomRightRadiusValue, BorderBottomRightRadius, as_border_bottom_right_radius);
    impl_get_prop!(get_opacity, StyleOpacityValue, Opacity, as_opacity);
    impl_get_prop!(get_transform, StyleTransformVecValue, Transform, as_transform);
    impl_get_prop!(get_transform_origin, StyleTransformOriginValue, TransformOrigin, as_transform_origin);
    impl_get_prop!(get_perspective_origin, StylePerspectiveOriginValue, PerspectiveOrigin, as_perspective_origin);
    impl_get_prop!(get_backface_visibility, StyleBackfaceVisibilityValue, BackfaceVisibility, as_backface_visibility);
    impl_get_prop!(get_display, LayoutDisplayValue, Display, as_display);
    impl_get_prop!(get_float, LayoutFloatValue, Float, as_float);
    impl_get_prop!(get_box_sizing, LayoutBoxSizingValue, BoxSizing, as_box_sizing);
    impl_get_prop!(get_width, LayoutWidthValue, Width, as_width);
    impl_get_prop!(get_height, LayoutHeightValue, Height, as_height);
    impl_get_prop!(get_min_width, LayoutMinWidthValue, MinWidth, as_min_width);
    impl_get_prop!(get_min_height, LayoutMinHeightValue, MinHeight, as_min_height);
    impl_get_prop!(get_max_width, LayoutMaxWidthValue, MaxWidth, as_max_width);
    impl_get_prop!(get_max_height, LayoutMaxHeightValue, MaxHeight, as_max_height);
    impl_get_prop!(get_position, LayoutPositionValue, Position, as_position);
    impl_get_prop!(get_top, LayoutTopValue, Top, as_top);
    impl_get_prop!(get_bottom, LayoutInsetBottomValue, Bottom, as_bottom);
    impl_get_prop!(get_right, LayoutRightValue, Right, as_right);
    impl_get_prop!(get_left, LayoutLeftValue, Left, as_left);
    impl_get_prop!(get_padding_top, LayoutPaddingTopValue, PaddingTop, as_padding_top);
    impl_get_prop!(get_padding_bottom, LayoutPaddingBottomValue, PaddingBottom, as_padding_bottom);
    impl_get_prop!(get_padding_left, LayoutPaddingLeftValue, PaddingLeft, as_padding_left);
    impl_get_prop!(get_padding_right, LayoutPaddingRightValue, PaddingRight, as_padding_right);
    impl_get_prop!(get_margin_top, LayoutMarginTopValue, MarginTop, as_margin_top);
    impl_get_prop!(get_margin_bottom, LayoutMarginBottomValue, MarginBottom, as_margin_bottom);
    impl_get_prop!(get_margin_left, LayoutMarginLeftValue, MarginLeft, as_margin_left);
    impl_get_prop!(get_margin_right, LayoutMarginRightValue, MarginRight, as_margin_right);
    impl_get_prop!(get_border_top_width, LayoutBorderTopWidthValue, BorderTopWidth, as_border_top_width);
    impl_get_prop!(get_border_left_width, LayoutBorderLeftWidthValue, BorderLeftWidth, as_border_left_width);
    impl_get_prop!(get_border_right_width, LayoutBorderRightWidthValue, BorderRightWidth, as_border_right_width);
    impl_get_prop!(get_border_bottom_width, LayoutBorderBottomWidthValue, BorderBottomWidth, as_border_bottom_width);
    impl_get_prop!(get_overflow_x, LayoutOverflowValue, OverflowX, as_overflow_x);
    impl_get_prop!(get_overflow_y, LayoutOverflowValue, OverflowY, as_overflow_y);
    impl_get_prop!(get_overflow_block, LayoutOverflowValue, OverflowBlock, as_overflow_block);
    impl_get_prop!(get_overflow_inline, LayoutOverflowValue, OverflowInline, as_overflow_inline);
    impl_get_prop!(get_flex_direction, LayoutFlexDirectionValue, FlexDirection, as_flex_direction);
    impl_get_prop!(get_flex_wrap, LayoutFlexWrapValue, FlexWrap, as_flex_wrap);
    impl_get_prop!(get_flex_grow, LayoutFlexGrowValue, FlexGrow, as_flex_grow);
    impl_get_prop!(get_flex_shrink, LayoutFlexShrinkValue, FlexShrink, as_flex_shrink);
    impl_get_prop!(get_justify_content, LayoutJustifyContentValue, JustifyContent, as_justify_content);
    impl_get_prop!(get_align_items, LayoutAlignItemsValue, AlignItems, as_align_items);
    impl_get_prop!(get_align_content, LayoutAlignContentValue, AlignContent, as_align_content);
    impl_get_prop!(get_mix_blend_mode, StyleMixBlendModeValue, MixBlendMode, as_mix_blend_mode);
    impl_get_prop!(get_filter, StyleFilterVecValue, Filter, as_filter);
    impl_get_prop!(get_backdrop_filter, StyleFilterVecValue, BackdropFilter, as_backdrop_filter);
    impl_get_prop!(get_text_shadow, StyleBoxShadowValue, TextShadow, as_text_shadow);
    impl_get_prop!(get_list_style_type, StyleListStyleTypeValue, ListStyleType, as_list_style_type);
    impl_get_prop!(get_list_style_position, StyleListStylePositionValue, ListStylePosition, as_list_style_position);
    impl_get_prop!(get_table_layout, LayoutTableLayoutValue, TableLayout, as_table_layout);
    impl_get_prop!(get_border_collapse, StyleBorderCollapseValue, BorderCollapse, as_border_collapse);
    impl_get_prop!(get_border_spacing, LayoutBorderSpacingValue, BorderSpacing, as_border_spacing);
    impl_get_prop!(get_caption_side, StyleCaptionSideValue, CaptionSide, as_caption_side);
    impl_get_prop!(get_empty_cells, StyleEmptyCellsValue, EmptyCells, as_empty_cells);
    // Width calculation methods
17
    pub fn calc_width(
17
        &self,
17
        node_data: &NodeData,
17
        node_id: &NodeId,
17
        styled_node_state: &StyledNodeState,
17
        reference_width: f32,
17
    ) -> f32 {
17
        self.get_width(node_data, node_id, styled_node_state)
17
            .and_then(|w| match w.get_property()? {
12
                LayoutWidth::Px(px) => Some(px.to_pixels_internal(
12
                    reference_width,
12
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
12
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
12
                )),
1
                _ => Some(0.0), // min-content/max-content not resolved here
14
            })
17
            .unwrap_or(0.0)
17
    }
3
    pub fn calc_min_width(
3
        &self,
3
        node_data: &NodeData,
3
        node_id: &NodeId,
3
        styled_node_state: &StyledNodeState,
3
        reference_width: f32,
3
    ) -> f32 {
3
        self.get_min_width(node_data, node_id, styled_node_state)
3
            .and_then(|w| {
2
                Some(w.get_property()?.inner.to_pixels_internal(
2
                    reference_width,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
2
            })
3
            .unwrap_or(0.0)
3
    }
3
    pub fn calc_max_width(
3
        &self,
3
        node_data: &NodeData,
3
        node_id: &NodeId,
3
        styled_node_state: &StyledNodeState,
3
        reference_width: f32,
3
    ) -> Option<f32> {
3
        self.get_max_width(node_data, node_id, styled_node_state)
3
            .and_then(|w| {
2
                Some(w.get_property()?.inner.to_pixels_internal(
2
                    reference_width,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
2
            })
3
    }
    // Height calculation methods
3
    pub fn calc_height(
3
        &self,
3
        node_data: &NodeData,
3
        node_id: &NodeId,
3
        styled_node_state: &StyledNodeState,
3
        reference_height: f32,
3
    ) -> f32 {
3
        self.get_height(node_data, node_id, styled_node_state)
3
            .and_then(|h| match h.get_property()? {
2
                LayoutHeight::Px(px) => Some(px.to_pixels_internal(
2
                    reference_height,
2
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
2
                )),
                _ => Some(0.0), // min-content/max-content not resolved here
2
            })
3
            .unwrap_or(0.0)
3
    }
1
    pub fn calc_min_height(
1
        &self,
1
        node_data: &NodeData,
1
        node_id: &NodeId,
1
        styled_node_state: &StyledNodeState,
1
        reference_height: f32,
1
    ) -> f32 {
1
        self.get_min_height(node_data, node_id, styled_node_state)
1
            .and_then(|h| {
                Some(h.get_property()?.inner.to_pixels_internal(
                    reference_height,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
            })
1
            .unwrap_or(0.0)
1
    }
2
    pub fn calc_max_height(
2
        &self,
2
        node_data: &NodeData,
2
        node_id: &NodeId,
2
        styled_node_state: &StyledNodeState,
2
        reference_height: f32,
2
    ) -> Option<f32> {
2
        self.get_max_height(node_data, node_id, styled_node_state)
2
            .and_then(|h| {
                Some(h.get_property()?.inner.to_pixels_internal(
                    reference_height,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
            })
2
    }
    // Position calculation methods
2
    pub fn calc_left(
2
        &self,
2
        node_data: &NodeData,
2
        node_id: &NodeId,
2
        styled_node_state: &StyledNodeState,
2
        reference_width: f32,
2
    ) -> Option<f32> {
2
        self.get_left(node_data, node_id, styled_node_state)
2
            .and_then(|l| {
1
                Some(l.get_property()?.inner.to_pixels_internal(
1
                    reference_width,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
1
            })
2
    }
2
    pub fn calc_right(
2
        &self,
2
        node_data: &NodeData,
2
        node_id: &NodeId,
2
        styled_node_state: &StyledNodeState,
2
        reference_width: f32,
2
    ) -> Option<f32> {
2
        self.get_right(node_data, node_id, styled_node_state)
2
            .and_then(|r| {
1
                Some(r.get_property()?.inner.to_pixels_internal(
1
                    reference_width,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
1
            })
2
    }
2
    pub fn calc_top(
2
        &self,
2
        node_data: &NodeData,
2
        node_id: &NodeId,
2
        styled_node_state: &StyledNodeState,
2
        reference_height: f32,
2
    ) -> Option<f32> {
2
        self.get_top(node_data, node_id, styled_node_state)
2
            .and_then(|t| {
1
                Some(t.get_property()?.inner.to_pixels_internal(
1
                    reference_height,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
1
            })
2
    }
2
    pub fn calc_bottom(
2
        &self,
2
        node_data: &NodeData,
2
        node_id: &NodeId,
2
        styled_node_state: &StyledNodeState,
2
        reference_height: f32,
2
    ) -> Option<f32> {
2
        self.get_bottom(node_data, node_id, styled_node_state)
2
            .and_then(|b| {
1
                Some(b.get_property()?.inner.to_pixels_internal(
1
                    reference_height,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
1
            })
2
    }
    // Border calculation methods
2
    pub fn calc_border_left_width(
2
        &self,
2
        node_data: &NodeData,
2
        node_id: &NodeId,
2
        styled_node_state: &StyledNodeState,
2
        reference_width: f32,
2
    ) -> f32 {
2
        self.get_border_left_width(node_data, node_id, styled_node_state)
2
            .and_then(|b| {
1
                Some(b.get_property()?.inner.to_pixels_internal(
1
                    reference_width,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
1
            })
2
            .unwrap_or(0.0)
2
    }
1
    pub fn calc_border_right_width(
1
        &self,
1
        node_data: &NodeData,
1
        node_id: &NodeId,
1
        styled_node_state: &StyledNodeState,
1
        reference_width: f32,
1
    ) -> f32 {
1
        self.get_border_right_width(node_data, node_id, styled_node_state)
1
            .and_then(|b| {
                Some(b.get_property()?.inner.to_pixels_internal(
                    reference_width,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
            })
1
            .unwrap_or(0.0)
1
    }
1
    pub fn calc_border_top_width(
1
        &self,
1
        node_data: &NodeData,
1
        node_id: &NodeId,
1
        styled_node_state: &StyledNodeState,
1
        reference_height: f32,
1
    ) -> f32 {
1
        self.get_border_top_width(node_data, node_id, styled_node_state)
1
            .and_then(|b| {
                Some(b.get_property()?.inner.to_pixels_internal(
                    reference_height,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
            })
1
            .unwrap_or(0.0)
1
    }
1
    pub fn calc_border_bottom_width(
1
        &self,
1
        node_data: &NodeData,
1
        node_id: &NodeId,
1
        styled_node_state: &StyledNodeState,
1
        reference_height: f32,
1
    ) -> f32 {
1
        self.get_border_bottom_width(node_data, node_id, styled_node_state)
1
            .and_then(|b| {
                Some(b.get_property()?.inner.to_pixels_internal(
                    reference_height,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
            })
1
            .unwrap_or(0.0)
1
    }
    // Padding calculation methods
4
    pub fn calc_padding_left(
4
        &self,
4
        node_data: &NodeData,
4
        node_id: &NodeId,
4
        styled_node_state: &StyledNodeState,
4
        reference_width: f32,
4
    ) -> f32 {
4
        self.get_padding_left(node_data, node_id, styled_node_state)
4
            .and_then(|p| {
3
                Some(p.get_property()?.inner.to_pixels_internal(
3
                    reference_width,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
3
            })
4
            .unwrap_or(0.0)
4
    }
1
    pub fn calc_padding_right(
1
        &self,
1
        node_data: &NodeData,
1
        node_id: &NodeId,
1
        styled_node_state: &StyledNodeState,
1
        reference_width: f32,
1
    ) -> f32 {
1
        self.get_padding_right(node_data, node_id, styled_node_state)
1
            .and_then(|p| {
                Some(p.get_property()?.inner.to_pixels_internal(
                    reference_width,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
            })
1
            .unwrap_or(0.0)
1
    }
2
    pub fn calc_padding_top(
2
        &self,
2
        node_data: &NodeData,
2
        node_id: &NodeId,
2
        styled_node_state: &StyledNodeState,
2
        reference_height: f32,
2
    ) -> f32 {
2
        self.get_padding_top(node_data, node_id, styled_node_state)
2
            .and_then(|p| {
                Some(p.get_property()?.inner.to_pixels_internal(
                    reference_height,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
            })
2
            .unwrap_or(0.0)
2
    }
1
    pub fn calc_padding_bottom(
1
        &self,
1
        node_data: &NodeData,
1
        node_id: &NodeId,
1
        styled_node_state: &StyledNodeState,
1
        reference_height: f32,
1
    ) -> f32 {
1
        self.get_padding_bottom(node_data, node_id, styled_node_state)
1
            .and_then(|p| {
                Some(p.get_property()?.inner.to_pixels_internal(
                    reference_height,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
            })
1
            .unwrap_or(0.0)
1
    }
    // Margin calculation methods
1
    pub fn calc_margin_left(
1
        &self,
1
        node_data: &NodeData,
1
        node_id: &NodeId,
1
        styled_node_state: &StyledNodeState,
1
        reference_width: f32,
1
    ) -> f32 {
1
        self.get_margin_left(node_data, node_id, styled_node_state)
1
            .and_then(|m| {
                Some(m.get_property()?.inner.to_pixels_internal(
                    reference_width,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
            })
1
            .unwrap_or(0.0)
1
    }
1
    pub fn calc_margin_right(
1
        &self,
1
        node_data: &NodeData,
1
        node_id: &NodeId,
1
        styled_node_state: &StyledNodeState,
1
        reference_width: f32,
1
    ) -> f32 {
1
        self.get_margin_right(node_data, node_id, styled_node_state)
1
            .and_then(|m| {
                Some(m.get_property()?.inner.to_pixels_internal(
                    reference_width,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
            })
1
            .unwrap_or(0.0)
1
    }
3
    pub fn calc_margin_top(
3
        &self,
3
        node_data: &NodeData,
3
        node_id: &NodeId,
3
        styled_node_state: &StyledNodeState,
3
        reference_height: f32,
3
    ) -> f32 {
3
        self.get_margin_top(node_data, node_id, styled_node_state)
3
            .and_then(|m| {
2
                Some(m.get_property()?.inner.to_pixels_internal(
2
                    reference_height,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
2
            })
3
            .unwrap_or(0.0)
3
    }
1
    pub fn calc_margin_bottom(
1
        &self,
1
        node_data: &NodeData,
1
        node_id: &NodeId,
1
        styled_node_state: &StyledNodeState,
1
        reference_height: f32,
1
    ) -> f32 {
1
        self.get_margin_bottom(node_data, node_id, styled_node_state)
1
            .and_then(|m| {
                Some(m.get_property()?.inner.to_pixels_internal(
                    reference_height,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                ))
            })
1
            .unwrap_or(0.0)
1
    }
    #[allow(clippy::too_many_lines)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
3729836
    fn resolve_property_dependency(
3729836
        target_property: &CssProperty,
3729836
        reference_property: &CssProperty,
3729836
    ) -> Option<CssProperty> {
        // wildcard import: this big property-dispatch match references the full set
        // of layout property types; enumerating them all is unmaintainable.
        #[allow(clippy::wildcard_imports)]
        use azul_css::{
            css::CssPropertyValue,
            props::{
                basic::{font::StyleFontSize, length::SizeMetric, pixel::PixelValue},
                layout::*,
                style::{SelectionRadius, StyleLetterSpacing, StyleWordSpacing},
            },
        };
        // Extract PixelValue from various property types (returns owned value)
4297607
        let get_pixel_value = |prop: &CssProperty| -> Option<PixelValue> {
4297607
            match prop {
642958
                CssProperty::FontSize(val) => val.get_property().map(|v| v.inner),
66
                CssProperty::LetterSpacing(val) => val.get_property().map(|v| v.inner),
22
                CssProperty::WordSpacing(val) => val.get_property().map(|v| v.inner),
59079
                CssProperty::PaddingLeft(val) => val.get_property().map(|v| v.inner),
58391
                CssProperty::PaddingRight(val) => val.get_property().map(|v| v.inner),
87596
                CssProperty::PaddingTop(val) => val.get_property().map(|v| v.inner),
59711
                CssProperty::PaddingBottom(val) => val.get_property().map(|v| v.inner),
23852
                CssProperty::MarginLeft(val) => val.get_property().map(|v| v.inner),
5350
                CssProperty::MarginRight(val) => val.get_property().map(|v| v.inner),
80228
                CssProperty::MarginTop(val) => val.get_property().map(|v| v.inner),
111898
                CssProperty::MarginBottom(val) => val.get_property().map(|v| v.inner),
6237
                CssProperty::MinWidth(val) => val.get_property().map(|v| v.inner),
154
                CssProperty::MinHeight(val) => val.get_property().map(|v| v.inner),
                CssProperty::MaxWidth(val) => val.get_property().map(|v| v.inner),
                CssProperty::MaxHeight(val) => val.get_property().map(|v| v.inner),
                CssProperty::SelectionRadius(val) => val.get_property().map(|v| v.inner),
3162065
                _ => None,
            }
4297607
        };
3729836
        let target_pixel_value = get_pixel_value(target_property)?;
567771
        let reference_pixel_value = get_pixel_value(reference_property)?;
        // Convert reference to absolute pixels first
567770
        let reference_px = match reference_pixel_value.metric {
567768
            SizeMetric::Px => reference_pixel_value.number.get(),
1
            SizeMetric::Pt => reference_pixel_value.number.get() * PT_TO_PX,
            SizeMetric::In => reference_pixel_value.number.get() * IN_TO_PX,
            SizeMetric::Cm => reference_pixel_value.number.get() * CM_TO_PX,
            SizeMetric::Mm => reference_pixel_value.number.get() * MM_TO_PX,
            // Reference can't be relative (em/rem/%) or viewport-relative.
            SizeMetric::Em
            | SizeMetric::Rem
            | SizeMetric::Percent
            | SizeMetric::Vw
            | SizeMetric::Vh
            | SizeMetric::Vmin
1
            | SizeMetric::Vmax => return None,
        };
        // Resolve target based on reference
567769
        let resolved_px = match target_pixel_value.metric {
441940
            SizeMetric::Px => target_pixel_value.number.get(),
            SizeMetric::Pt => target_pixel_value.number.get() * PT_TO_PX,
            SizeMetric::In => target_pixel_value.number.get() * IN_TO_PX,
            SizeMetric::Cm => target_pixel_value.number.get() * CM_TO_PX,
            SizeMetric::Mm => target_pixel_value.number.get() * MM_TO_PX,
            // em/rem both scale by reference (rem uses reference as root font-size).
125713
            SizeMetric::Em | SizeMetric::Rem => target_pixel_value.number.get() * reference_px,
115
            SizeMetric::Percent => target_pixel_value.number.get() / 100.0 * reference_px,
            // Need viewport context
1
            SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => return None,
        };
        // Create a new property with the resolved value
567768
        let resolved_pixel_value = PixelValue::px(resolved_px);
567768
        match target_property {
75184
            CssProperty::FontSize(_) => Some(CssProperty::FontSize(CssPropertyValue::Exact(
75184
                StyleFontSize {
75184
                    inner: resolved_pixel_value,
75184
                },
75184
            ))),
66
            CssProperty::LetterSpacing(_) => Some(CssProperty::LetterSpacing(
66
                CssPropertyValue::Exact(StyleLetterSpacing {
66
                    inner: resolved_pixel_value,
66
                }),
66
            )),
22
            CssProperty::WordSpacing(_) => Some(CssProperty::WordSpacing(CssPropertyValue::Exact(
22
                StyleWordSpacing {
22
                    inner: resolved_pixel_value,
22
                },
22
            ))),
59079
            CssProperty::PaddingLeft(_) => Some(CssProperty::PaddingLeft(CssPropertyValue::Exact(
59079
                LayoutPaddingLeft {
59079
                    inner: resolved_pixel_value,
59079
                },
59079
            ))),
58391
            CssProperty::PaddingRight(_) => Some(CssProperty::PaddingRight(
58391
                CssPropertyValue::Exact(LayoutPaddingRight {
58391
                    inner: resolved_pixel_value,
58391
                }),
58391
            )),
87596
            CssProperty::PaddingTop(_) => Some(CssProperty::PaddingTop(CssPropertyValue::Exact(
87596
                LayoutPaddingTop {
87596
                    inner: resolved_pixel_value,
87596
                },
87596
            ))),
59711
            CssProperty::PaddingBottom(_) => Some(CssProperty::PaddingBottom(
59711
                CssPropertyValue::Exact(LayoutPaddingBottom {
59711
                    inner: resolved_pixel_value,
59711
                }),
59711
            )),
23852
            CssProperty::MarginLeft(_) => Some(CssProperty::MarginLeft(CssPropertyValue::Exact(
23852
                LayoutMarginLeft {
23852
                    inner: resolved_pixel_value,
23852
                },
23852
            ))),
5350
            CssProperty::MarginRight(_) => Some(CssProperty::MarginRight(CssPropertyValue::Exact(
5350
                LayoutMarginRight {
5350
                    inner: resolved_pixel_value,
5350
                },
5350
            ))),
80228
            CssProperty::MarginTop(_) => Some(CssProperty::MarginTop(CssPropertyValue::Exact(
80228
                LayoutMarginTop {
80228
                    inner: resolved_pixel_value,
80228
                },
80228
            ))),
111898
            CssProperty::MarginBottom(_) => Some(CssProperty::MarginBottom(
111898
                CssPropertyValue::Exact(LayoutMarginBottom {
111898
                    inner: resolved_pixel_value,
111898
                }),
111898
            )),
6237
            CssProperty::MinWidth(_) => Some(CssProperty::MinWidth(CssPropertyValue::Exact(
6237
                LayoutMinWidth {
6237
                    inner: resolved_pixel_value,
6237
                },
6237
            ))),
154
            CssProperty::MinHeight(_) => Some(CssProperty::MinHeight(CssPropertyValue::Exact(
154
                LayoutMinHeight {
154
                    inner: resolved_pixel_value,
154
                },
154
            ))),
            CssProperty::MaxWidth(_) => Some(CssProperty::MaxWidth(CssPropertyValue::Exact(
                LayoutMaxWidth {
                    inner: resolved_pixel_value,
                },
            ))),
            CssProperty::MaxHeight(_) => Some(CssProperty::MaxHeight(CssPropertyValue::Exact(
                LayoutMaxHeight {
                    inner: resolved_pixel_value,
                },
            ))),
            CssProperty::SelectionRadius(_) => Some(CssProperty::SelectionRadius(
                CssPropertyValue::Exact(SelectionRadius {
                    inner: resolved_pixel_value,
                }),
            )),
            _ => None,
        }
3729836
    }
    /// Applies user-agent (UA) CSS properties to the cascade before inheritance.
    ///
    /// UA CSS has the lowest priority in the cascade, so it should only be applied
    /// if the node doesn't already have the property from inline styles or author CSS.
    ///
    /// This is critical for text nodes: UA CSS properties (like font-weight: bold for H1)
    /// must be in the cascade maps so they can be inherited by child text nodes.
    ///
    /// Uses a bitset per node to avoid O(n²) scanning of property vecs.
    #[allow(clippy::too_many_lines)] // cohesive single-pass walker; splitting adds state-threading
35505
    pub fn apply_ua_css(&mut self, node_data: &[NodeData]) {
        use azul_css::props::property::CssPropertyType;
        use azul_css::dynamic_selector::PseudoStateType;
35505
        let node_count = node_data.len();
35505
        if node_count == 0 {
67
            return;
35438
        }
        // Build a bitset per node: which CssPropertyType values are already set (Normal state).
        // CssPropertyType has ~178 variants, so we need [u128; 2] per node (256 bits).
35438
        let mut prop_set: Vec<[u128; 2]> = vec![[0u128; 2]; node_count];
        // Mark properties from css_props (author CSS, Normal state)
730041
        for (node_idx, props) in self.css_props.iter_node_slices() {
1073351
            for p in props {
343310
                if p.state == PseudoStateType::Normal {
342449
                    let d = p.prop_type as u16 as usize;
342449
                    if d < 128 {
335421
                        prop_set[node_idx][0] |= 1u128 << d;
335881
                    } else {
7028
                        prop_set[node_idx][1] |= 1u128 << (d - 128);
7028
                    }
861
                }
            }
        }
        // Mark properties from cascaded_props (Normal state)
730045
        for (node_idx, props) in self.cascaded_props.iter_node_slices() {
1829141
            for p in props {
1099096
                if p.state == PseudoStateType::Normal {
1081034
                    let d = p.prop_type as u16 as usize;
1081034
                    if d < 128 {
1080781
                        prop_set[node_idx][0] |= 1u128 << d;
1080781
                    } else {
253
                        prop_set[node_idx][1] |= 1u128 << (d - 128);
253
                    }
18062
                }
            }
        }
        // Mark properties from inline CSS (NodeData.style, unconditional = Normal)
730045
        for (node_idx, node) in node_data.iter().enumerate() {
3398337
            for (prop, conds) in node.style.iter_inline_properties() {
3388842
                let is_normal = conds.as_slice().is_empty();
3388842
                if is_normal {
3203607
                    let d = prop.get_type() as u16 as usize;
3203607
                    if d < 128 {
3001683
                        prop_set[node_idx][0] |= 1u128 << d;
3001683
                    } else {
201924
                        prop_set[node_idx][1] |= 1u128 << (d - 128);
201924
                    }
185235
                }
            }
        }
        // Mark properties from the GLOBAL `*` bucket. A `* { margin: 0 }`
        // reset is author CSS and must beat UA defaults on every ELEMENT
        // (origin beats specificity), but it is stored once globally rather
        // than per node, so the per-node marking above never saw it - the UA
        // body margin (8px) survived the classic reset and every page using
        // it rendered shifted against the browser reference. Text nodes are
        // exempt: `*` matches elements only (the compact builder makes the
        // same distinction), and UA defaults for text nodes must stay.
35438
        if !self.global_css_props.is_empty() {
1872
            let mut global_bits = [0u128; 2];
16399
            for p in &self.global_css_props {
14527
                let d = p.get_type() as u16 as usize;
14527
                if d < 128 {
14527
                    global_bits[0] |= 1u128 << d;
14527
                } else {
                    global_bits[1] |= 1u128 << (d - 128);
                }
            }
15827
            for (node_idx, node) in node_data.iter().enumerate() {
15827
                if !node.is_text_node() {
9472
                    prop_set[node_idx][0] |= global_bits[0];
9472
                    prop_set[node_idx][1] |= global_bits[1];
9472
                }
            }
33566
        }
        // All UA property types that get_ua_property() may return Some for.
        // MUST stay in sync with compact.rs::UA_PROPERTY_TYPES: a UA property
        // present in one list but not the other makes the two cascade paths
        // disagree about the computed value (this bit the VirtualView
        // overflow default).
35438
        let property_types = [
35438
            CssPropertyType::Display,
35438
            CssPropertyType::OverflowX,
35438
            CssPropertyType::OverflowY,
35438
            CssPropertyType::Width,
35438
            CssPropertyType::Height,
35438
            CssPropertyType::FontSize,
35438
            CssPropertyType::FontWeight,
35438
            CssPropertyType::FontFamily,
35438
            CssPropertyType::MarginTop,
35438
            CssPropertyType::MarginBottom,
35438
            CssPropertyType::MarginLeft,
35438
            CssPropertyType::MarginRight,
35438
            CssPropertyType::PaddingTop,
35438
            CssPropertyType::PaddingBottom,
35438
            CssPropertyType::PaddingLeft,
35438
            CssPropertyType::PaddingRight,
35438
            CssPropertyType::BorderTopStyle,
35438
            CssPropertyType::BorderTopWidth,
35438
            CssPropertyType::BorderTopColor,
35438
            CssPropertyType::BreakInside,
35438
            CssPropertyType::BreakAfter,
35438
            CssPropertyType::ListStyleType,
35438
            CssPropertyType::CounterReset,
35438
            CssPropertyType::TextDecoration,
35438
            CssPropertyType::TextAlign,
35438
            CssPropertyType::VerticalAlign,
35438
            CssPropertyType::Cursor,
35438
        ];
        // Apply UA CSS: only insert for property types not yet set (bitset check = O(1))
730045
        for (node_index, node) in node_data.iter().enumerate() {
730045
            let node_type = &node.node_type;
20441260
            for prop_type in &property_types {
                // Check bitset: if already set, skip entirely
19711215
                let d = *prop_type as u16 as usize;
19711215
                let has_prop = if d < 128 {
16791035
                    (prop_set[node_index][0] & (1u128 << d)) != 0
                } else {
2920180
                    (prop_set[node_index][1] & (1u128 << (d - 128))) != 0
                };
19711215
                if has_prop {
2196895
                    continue;
17514320
                }
                // Check if UA CSS defines this property for this node type
17514320
                if let Some(ua_prop) = crate::ua_css::get_ua_property(node_type, *prop_type) {
1013972
                    self.cascaded_props.push_to(node_index, StatefulCssProperty {
1013972
                        state: PseudoStateType::Normal,
1013972
                        prop_type: *prop_type,
1013972
                        property: ua_prop.clone(),
1013972
                    });
                    // Mark as set in the bitset (prevent duplicate insertion for same node)
1013972
                    if d < 128 {
1009932
                        prop_set[node_index][0] |= 1u128 << d;
1009932
                    } else {
4040
                        prop_set[node_index][1] |= 1u128 << (d - 128);
4040
                    }
16500348
                }
            }
        }
35505
    }
    /// Sort `cascaded_props` by (state, `prop_type`) and flatten into contiguous memory.
    /// Must be called after `apply_ua_css()` which adds entries to `cascaded_props`.
1
    pub fn sort_cascaded_props(&mut self) {
10
        self.cascaded_props.sort_each_and_flatten(|p| (p.state, p.prop_type));
1
    }
    /// Compute inherited values for all nodes in the DOM tree.
    ///
    /// Implements CSS inheritance: walk tree depth-first, apply cascade priority
    /// (inherited → cascaded → css → inline → user), create dependency chains for
    /// relative values. Call `apply_ua_css()` before this function.
36229
    pub fn compute_inherited_values(
36229
        &mut self,
36229
        node_hierarchy: &[NodeHierarchyItem],
36229
        node_data: &[NodeData],
36229
    ) -> Vec<NodeId> {
36229
        if self.computed_values.len() < node_hierarchy.len() {
35362
            self.computed_values.resize(node_hierarchy.len(), Vec::new());
35362
        }
36229
        node_hierarchy
36229
            .iter()
36229
            .enumerate()
732445
            .filter_map(|(node_index, hierarchy_item)| {
732445
                let node_id = NodeId::new(node_index);
732445
                let parent_id = hierarchy_item.parent_id();
732445
                let parent_computed: Option<Vec<(CssPropertyType, CssPropertyWithOrigin)>> =
732445
                    parent_id.and_then(|pid| self.computed_values.get(pid.index()).cloned());
732445
                let mut ctx = InheritanceContext {
732445
                    node_id,
732445
                    parent_id,
732445
                    computed_values: Vec::new(),
732445
                };
                // Step 1: Inherit from parent
732445
                if let Some(ref parent_values) = parent_computed {
696283
                    Self::inherit_from_parent(&mut ctx, parent_values);
696283
                }
                // Steps 2-5: Apply cascade in priority order
732445
                self.apply_cascade_properties(
732445
                    &mut ctx,
732445
                    node_id,
732445
                    parent_computed.as_ref(),
732445
                    node_data,
732445
                    node_index,
                );
                // Check for changes and store
732445
                let changed = self.store_if_changed(&ctx);
732445
                changed.then_some(node_id)
732445
            })
36229
            .collect()
36229
    }
    /// Inherit inheritable properties from parent node
696283
    fn inherit_from_parent(
696283
        ctx: &mut InheritanceContext,
696283
        parent_values: &[(CssPropertyType, CssPropertyWithOrigin)],
696283
    ) {
1496259
        for (prop_type, prop_with_origin) in
6823310
            parent_values.iter().filter(|(pt, _)| pt.is_inheritable())
        {
1496259
            let entry = (*prop_type, CssPropertyWithOrigin {
1496259
                property: prop_with_origin.property.clone(),
1496259
                origin: CssPropertyOrigin::Inherited,
1496259
            });
            // Insert into sorted vec
1496259
            match ctx.computed_values.binary_search_by_key(prop_type, |(k, _)| *k) {
                Ok(idx) => ctx.computed_values[idx] = entry,
1496259
                Err(idx) => ctx.computed_values.insert(idx, entry),
            }
        }
696283
    }
    /// Apply all cascade properties in priority order
732445
    fn apply_cascade_properties(
732445
        &self,
732445
        ctx: &mut InheritanceContext,
732445
        node_id: NodeId,
732445
        parent_computed: Option<&Vec<(CssPropertyType, CssPropertyWithOrigin)>>,
732445
        node_data: &[NodeData],
732445
        node_index: usize,
732445
    ) {
        // Step 2: Cascaded properties (UA CSS)
        {
732445
            let cascaded_slice = self.cascaded_props.get_slice(node_id.index());
2847659
            for p in cascaded_slice {
2115214
                if p.state == azul_css::dynamic_selector::PseudoStateType::Normal
2097152
                    && Self::should_apply_cascaded(&ctx.computed_values, p.prop_type, &p.property) {
2097152
                        Self::process_property(ctx, &p.property, parent_computed);
2097152
                    }
            }
        }
        // Step 3: CSS properties (stylesheets)
        {
732445
            let css_slice = self.css_props.get_slice(node_id.index());
1075850
            for p in css_slice {
343405
                if p.state == azul_css::dynamic_selector::PseudoStateType::Normal {
342544
                    Self::process_property(ctx, &p.property, parent_computed);
342544
                }
            }
        }
        // Step 4: Inline CSS properties
3400737
        for (prop, conds) in node_data[node_index].style.iter_inline_properties() {
            // Only apply unconditional (normal) properties
3389815
            if conds.as_slice().is_empty() {
3204580
                Self::process_property(ctx, prop, parent_computed);
3204580
            }
        }
        // Step 5: User-overridden properties
732445
        if let Some(user_props) = self.user_overridden_properties.get(node_id.index()) {
14
            for (_, prop) in user_props {
1
                Self::process_property(ctx, prop, parent_computed);
1
            }
732432
        }
732445
    }
    /// Check if a cascaded property should be applied.
    ///
    /// A cascaded (UA / author-selector) value applies unless the node has
    /// already set its OWN value (`origin == Own`), which wins per the cascade.
    /// An `Inherited` placeholder must NOT block it — including a relative
    /// `font-size`: an earlier version skipped a cascaded relative font-size when
    /// an inherited value existed, which silently dropped `<h1>`'s UA
    /// `font-size: 2em` (and every heading), leaving headings at their parent's
    /// size. That was wrong: `resolve_font_size_property` resolves the `em`
    /// against the *parent's* font-size, not the inherited value, so there is no
    /// double-scaling — the cascaded relative size must apply and overwrite the
    /// inherited entry.
2097157
    fn should_apply_cascaded(
2097157
        computed: &[(CssPropertyType, CssPropertyWithOrigin)],
2097157
        prop_type: CssPropertyType,
2097157
        _prop: &CssProperty,
2097157
    ) -> bool {
2097157
        computed
2097157
            .binary_search_by_key(&prop_type, |(k, _)| *k)
2097157
            .map_or(true, |idx| computed[idx].1.origin == CssPropertyOrigin::Inherited)
2097157
    }
    /// Process a single property: resolve and store
5644277
    fn process_property(
5644277
        ctx: &mut InheritanceContext,
5644277
        prop: &CssProperty,
5644277
        parent_computed: Option<&Vec<(CssPropertyType, CssPropertyWithOrigin)>>,
5644277
    ) {
5644277
        let prop_type = prop.get_type();
5644277
        let resolved = if prop_type == CssPropertyType::FontSize {
187716
            Self::resolve_font_size_property(prop, parent_computed)
        } else {
5456561
            Self::resolve_other_property(prop, &ctx.computed_values)
        };
5644277
        let entry = (prop_type, CssPropertyWithOrigin {
5644277
            property: resolved,
5644277
            origin: CssPropertyOrigin::Own,
5644277
        });
5644277
        match ctx.computed_values.binary_search_by_key(&prop_type, |(k, _)| *k) {
1229157
            Ok(idx) => ctx.computed_values[idx] = entry,
4415120
            Err(idx) => ctx.computed_values.insert(idx, entry),
        }
5644277
    }
    /// Resolve font-size property (uses parent's font-size as reference)
187716
    fn resolve_font_size_property(
187716
        prop: &CssProperty,
187716
        parent_computed: Option<&Vec<(CssPropertyType, CssPropertyWithOrigin)>>,
187716
    ) -> CssProperty {
187716
        let parent_font_size = parent_computed
187716
            .and_then(|p| {
182470
                p.binary_search_by_key(&CssPropertyType::FontSize, |(k, _)| *k)
182470
                    .ok()
182470
                    .map(|idx| &p[idx].1)
182470
            });
187716
        parent_font_size.map_or_else(|| Self::resolve_font_size_to_pixels(
112535
                prop,
                azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
75181
            ), |pfs| Self::resolve_property_dependency(prop, &pfs.property).unwrap_or_else(
                || {
                    Self::resolve_font_size_to_pixels(
                        prop,
                        azul_css::props::basic::pixel::DEFAULT_FONT_SIZE,
                    )
                },
            ))
187716
    }
    /// Resolve other properties (uses current node's font-size as reference)
5456561
    fn resolve_other_property(
5456561
        prop: &CssProperty,
5456561
        computed: &[(CssPropertyType, CssPropertyWithOrigin)],
5456561
    ) -> CssProperty {
5456561
        computed
5456561
            .binary_search_by_key(&CssPropertyType::FontSize, |(k, _)| *k)
5456561
            .ok()
5456561
            .and_then(|idx| Self::resolve_property_dependency(prop, &computed[idx].1.property))
5456561
            .unwrap_or_else(|| prop.clone())
5456561
    }
    /// Convert font-size to absolute pixels
112547
    fn resolve_font_size_to_pixels(prop: &CssProperty, reference_px: f32) -> CssProperty {
        use azul_css::{
            css::CssPropertyValue,
            props::basic::{font::StyleFontSize, length::SizeMetric, pixel::PixelValue},
        };
112547
        let CssProperty::FontSize(css_val) = prop else {
1
            return prop.clone();
        };
112546
        let Some(font_size) = css_val.get_property() else {
1
            return prop.clone();
        };
112545
        let resolved_px = match font_size.inner.metric {
112329
            SizeMetric::Px => font_size.inner.number.get(),
1
            SizeMetric::Pt => font_size.inner.number.get() * PT_TO_PX,
            SizeMetric::In => font_size.inner.number.get() * IN_TO_PX,
            SizeMetric::Cm => font_size.inner.number.get() * CM_TO_PX,
            SizeMetric::Mm => font_size.inner.number.get() * MM_TO_PX,
201
            SizeMetric::Em => font_size.inner.number.get() * reference_px,
            SizeMetric::Rem => {
1
                font_size.inner.number.get() * azul_css::props::basic::pixel::DEFAULT_FONT_SIZE
            }
1
            SizeMetric::Percent => font_size.inner.number.get() / 100.0 * reference_px,
            SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax => {
12
                return prop.clone();
            }
        };
112533
        CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize {
112533
            inner: PixelValue::px(resolved_px),
112533
        }))
112547
    }
    /// Check if font-size has relative unit (em, rem, %)
7
    fn has_relative_font_size_unit(prop: &CssProperty) -> bool {
        use azul_css::props::basic::length::SizeMetric;
7
        let CssProperty::FontSize(css_val) = prop else {
1
            return false;
        };
6
        css_val
6
            .get_property()
6
            .is_some_and(|fs| {
2
                matches!(
5
                    fs.inner.metric,
                    SizeMetric::Em | SizeMetric::Rem | SizeMetric::Percent
                )
5
            })
7
    }
    /// Store computed values if changed, returns true if values were updated
732445
    fn store_if_changed(&mut self, ctx: &InheritanceContext) -> bool {
732445
        let values_changed = self
732445
            .computed_values
732445
            .get(ctx.node_id.index()) != Some(&ctx.computed_values);
732445
        self.computed_values[ctx.node_id.index()].clone_from(&ctx.computed_values);
732445
        values_changed
732445
    }
}
/// Context for computing inherited values for a single node
struct InheritanceContext {
    node_id: NodeId,
    parent_id: Option<NodeId>,
    computed_values: Vec<(CssPropertyType, CssPropertyWithOrigin)>,
}
impl CssPropertyCache {
    /// Clear the entire compact cache. Call after major DOM changes.
1
    pub(crate) fn invalidate_resolved_cache(&mut self) {
1
        self.compact_cache = None;
1
    }
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::too_many_lines)]
mod autotest_generated {
    use azul_css::{
        css::CssPropertyValue,
        dynamic_selector::{
            CssPropertyWithConditions, DynamicSelector, DynamicSelectorContext, PseudoStateType,
        },
        props::{
            basic::{length::SizeMetric, pixel::PixelValue},
            layout::{
                LayoutFlexBasis, LayoutInsetBottom, LayoutLeft, LayoutMarginTop, LayoutMaxWidth,
                LayoutMinWidth, LayoutOverflow, LayoutPaddingLeft, LayoutRight, LayoutTop,
            },
            style::LayoutBorderLeftWidth,
        },
    };
    use super::*;
    // ---------------------------------------------------------------------
    // helpers
    // ---------------------------------------------------------------------
    /// Approximate float compare — every value here round-trips through
    /// `FloatValue`'s fixed-point (1/1000) encoding.
    fn close(a: f32, b: f32) -> bool {
        (a - b).abs() < 0.01
    }
    fn n0() -> NodeId {
        NodeId::new(0)
    }
    fn normal() -> StyledNodeState {
        StyledNodeState::default()
    }
    /// A `<div>` carrying `props` as unconditional (Normal-state) inline CSS.
    fn div_with(props: Vec<CssProperty>) -> NodeData {
        let mut nd = NodeData::create_div();
        for property in props {
            nd.add_css_property(CssPropertyWithConditions {
                property,
                apply_if: Vec::new().into(),
            });
        }
        nd
    }
    /// A `<div>` carrying `props` gated on a single pseudo-state.
    fn div_with_pseudo(props: Vec<CssProperty>, state: PseudoStateType) -> NodeData {
        let mut nd = NodeData::create_div();
        for property in props {
            nd.add_css_property(CssPropertyWithConditions {
                property,
                apply_if: vec![DynamicSelector::PseudoState(state)].into(),
            });
        }
        nd
    }
    fn width_px(v: f32) -> CssProperty {
        CssProperty::Width(CssPropertyValue::Exact(LayoutWidth::Px(PixelValue::px(v))))
    }
    fn width_pct(v: f32) -> CssProperty {
        CssProperty::Width(CssPropertyValue::Exact(LayoutWidth::Px(PixelValue::percent(
            v,
        ))))
    }
    fn font_size(pv: PixelValue) -> CssProperty {
        CssProperty::FontSize(CssPropertyValue::Exact(StyleFontSize { inner: pv }))
    }
    /// Pull `(metric, number)` back out of a `CssProperty::FontSize`.
    fn font_size_parts(p: &CssProperty) -> Option<(SizeMetric, f32)> {
        match p {
            CssProperty::FontSize(v) => v
                .get_property()
                .map(|fs| (fs.inner.metric, fs.inner.number.get())),
            _ => None,
        }
    }
    fn stateful(state: PseudoStateType, property: CssProperty) -> StatefulCssProperty {
        StatefulCssProperty {
            state,
            prop_type: property.get_type(),
            property,
        }
    }
    // =====================================================================
    // FlatVecVec — construction / getters / predicates
    // =====================================================================
    #[test]
    fn flatvecvec_new_zero_is_empty() {
        let f = FlatVecVec::<i32>::new(0);
        assert_eq!(f.len(), 0);
        assert!(f.is_empty());
        // Quirk worth pinning: with no build slots at all, `is_flattened()` is
        // vacuously true (`build.is_empty()`), even though `flatten()` never ran.
        assert!(f.is_flattened());
        assert!(f.get_slice(0).is_empty());
    }
    #[test]
    fn flatvecvec_new_invariants_hold() {
        let f = FlatVecVec::<i32>::new(3);
        assert_eq!(f.len(), 3);
        assert!(!f.is_empty());
        assert!(!f.is_flattened(), "fresh multi-slot vec is in build phase");
        assert_eq!(f.build_get(0), Some(&Vec::new()));
        assert_eq!(f.build_get(2), Some(&Vec::new()));
        assert_eq!(f.build_get(3), None, "one past the end");
        assert_eq!(f.build_get(usize::MAX), None);
        assert!(f.get_slice(0).is_empty());
    }
    #[test]
    fn flatvecvec_default_is_neutral() {
        let f = FlatVecVec::<i32>::default();
        assert_eq!(f.len(), 0);
        assert!(f.is_empty());
        assert_eq!(f.build_get(0), None);
        assert!(f.get_slice(0).is_empty());
    }
    #[test]
    fn flatvecvec_get_slice_out_of_bounds_is_empty_in_both_phases() {
        let mut f = FlatVecVec::<i32>::new(2);
        f.push_to(0, 7);
        // build phase
        assert_eq!(f.get_slice(0), &[7]);
        assert!(f.get_slice(2).is_empty());
        assert!(f.get_slice(usize::MAX).is_empty());
        f.flatten();
        // read phase — same out-of-bounds contract, still no panic
        assert_eq!(f.get_slice(0), &[7]);
        assert!(f.get_slice(2).is_empty());
        assert!(f.get_slice(usize::MAX).is_empty());
    }
    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn flatvecvec_push_to_out_of_bounds_panics() {
        // Documented in `push_to`: "Panics if ... node_index >= len()".
        let mut f = FlatVecVec::<i32>::new(1);
        f.push_to(1, 0);
    }
    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn flatvecvec_push_to_after_flatten_panics() {
        // Documented in `push_to`: "Panics if already flattened".
        let mut f = FlatVecVec::<i32>::new(1);
        f.flatten();
        f.push_to(0, 0);
    }
    #[test]
    #[should_panic(expected = "index out of bounds")]
    fn flatvecvec_build_mut_out_of_bounds_panics() {
        let mut f = FlatVecVec::<i32>::new(1);
        let _ = f.build_mut(usize::MAX);
    }
    #[test]
    fn flatvecvec_build_iter_mut_visits_every_slot() {
        let mut f = FlatVecVec::<i32>::new(3);
        f.push_to(0, 1);
        f.push_to(2, 2);
        let mut visited = 0;
        for v in f.build_iter_mut() {
            visited += 1;
            v.clear();
        }
        assert_eq!(visited, 3);
        assert!(f.get_slice(0).is_empty());
        assert!(f.get_slice(2).is_empty());
    }
    #[test]
    fn flatvecvec_build_get_returns_none_once_flattened() {
        let mut f = FlatVecVec::<i32>::new(1);
        f.push_to(0, 5);
        f.flatten();
        // Doc: "During read phase, returns None (use `get_slice` instead)."
        assert_eq!(f.build_get(0), None);
        assert_eq!(f.get_slice(0), &[5]);
    }
    // =====================================================================
    // FlatVecVec — heap_bytes (numeric)
    // =====================================================================
    #[test]
    fn flatvecvec_heap_bytes_zero_and_empty() {
        let f = FlatVecVec::<i32>::default();
        assert_eq!(f.heap_bytes(0), 0, "empty vec, zero element size");
        // All three capacities are 0, so even a nonsensical MAX element size
        // multiplies out to 0 rather than overflowing.
        assert_eq!(f.heap_bytes(usize::MAX), 0);
        assert_eq!(f.heap_bytes(size_of::<i32>()), 0);
    }
    #[test]
    fn flatvecvec_heap_bytes_counts_build_and_flat_storage() {
        let mut f = FlatVecVec::<i32>::new(4);
        // Build-phase slots cost at least the outer Vec headers, even at a
        // per-element size of 0.
        assert!(f.heap_bytes(0) >= 4 * size_of::<Vec<i32>>());
        f.push_to(0, 1);
        f.push_to(0, 2);
        let build_bytes = f.heap_bytes(size_of::<i32>());
        assert!(build_bytes > 0);
        f.flatten();
        // Flat storage accounts for the 2 elements + the 4-entry offset table.
        let flat_bytes = f.heap_bytes(size_of::<i32>());
        assert!(flat_bytes >= 2 * size_of::<i32>() + 4 * size_of::<(u32, u32)>());
    }
    // =====================================================================
    // FlatVecVec — flatten / sort_each_and_flatten
    // =====================================================================
    #[test]
    fn flatvecvec_sort_each_and_flatten_keeps_last_of_equal_keys() {
        // CSS cascade rule: among equal keys, later source order wins.
        let mut f = FlatVecVec::<(i32, i32)>::new(1);
        f.push_to(0, (1, 10));
        f.push_to(0, (1, 20)); // same key, pushed later => must win
        f.push_to(0, (0, 30));
        f.sort_each_and_flatten(|p| p.0);
        assert!(f.is_flattened());
        assert_eq!(f.get_slice(0), &[(0, 30), (1, 20)]);
    }
    #[test]
    fn flatvecvec_sort_each_and_flatten_on_empty_slots() {
        let mut f = FlatVecVec::<i32>::new(3);
        f.push_to(1, 42);
        f.sort_each_and_flatten(|v| *v);
        assert_eq!(f.len(), 3);
        assert!(f.get_slice(0).is_empty());
        assert_eq!(f.get_slice(1), &[42]);
        assert!(f.get_slice(2).is_empty());
    }
    #[test]
    fn flatvecvec_sort_each_and_flatten_on_zero_nodes_does_not_panic() {
        let mut f = FlatVecVec::<i32>::new(0);
        f.sort_each_and_flatten(|v| *v);
        assert_eq!(f.len(), 0);
        assert!(f.get_slice(0).is_empty());
    }
    #[test]
    fn flatvecvec_flatten_does_not_deduplicate() {
        let mut f = FlatVecVec::<i32>::new(2);
        f.push_to(0, 5);
        f.push_to(0, 5);
        f.push_to(1, 9);
        f.flatten();
        assert!(f.is_flattened());
        assert_eq!(f.get_slice(0), &[5, 5], "flatten() must not dedup");
        assert_eq!(f.get_slice(1), &[9]);
    }
    // =====================================================================
    // FlatVecVec — retain
    // =====================================================================
    #[test]
    fn flatvecvec_retain_before_flatten_is_a_noop() {
        // Doc: "Must be called after flatten." Before that it must not silently
        // corrupt the build-phase data — it early-returns.
        let mut f = FlatVecVec::<i32>::new(1);
        f.push_to(0, 1);
        f.push_to(0, 2);
        f.retain(|_| false);
        assert_eq!(f.get_slice(0), &[1, 2], "build-phase data left untouched");
    }
    #[test]
    fn flatvecvec_retain_preserves_per_node_order() {
        let mut f = FlatVecVec::<i32>::new(2);
        for v in [1, 2, 3, 4] {
            f.push_to(0, v);
        }
        f.push_to(1, 5);
        f.flatten();
        f.retain(|v| v % 2 == 0);
        assert_eq!(f.get_slice(0), &[2, 4]);
        assert!(f.get_slice(1).is_empty());
        assert_eq!(f.len(), 2, "node slots survive an empty retain");
    }
    #[test]
    fn flatvecvec_retain_dropping_everything_leaves_empty_slices() {
        let mut f = FlatVecVec::<i32>::new(2);
        f.push_to(0, 1);
        f.push_to(1, 2);
        f.flatten();
        f.retain(|_| false);
        assert_eq!(f.len(), 2);
        assert!(f.get_slice(0).is_empty());
        assert!(f.get_slice(1).is_empty());
    }
    #[test]
    fn flatvecvec_retain_with_node_index_sees_owning_node() {
        let mut f = FlatVecVec::<i32>::new(3);
        f.push_to(0, 10);
        f.push_to(1, 11);
        f.push_to(2, 12);
        f.flatten();
        f.retain_with_node_index(|idx, _| idx == 1);
        assert!(f.get_slice(0).is_empty());
        assert_eq!(f.get_slice(1), &[11]);
        assert!(f.get_slice(2).is_empty());
    }
    #[test]
    fn flatvecvec_retain_with_node_index_before_flatten_is_a_noop() {
        let mut f = FlatVecVec::<i32>::new(1);
        f.push_to(0, 1);
        f.retain_with_node_index(|_, _| false);
        assert_eq!(f.get_slice(0), &[1]);
    }
    // =====================================================================
    // FlatVecVec — iteration / extend_from
    // =====================================================================
    #[test]
    fn flatvecvec_iter_node_slices_covers_all_nodes_in_both_phases() {
        let mut f = FlatVecVec::<i32>::new(3);
        f.push_to(1, 7);
        let build: Vec<(usize, Vec<i32>)> = f
            .iter_node_slices()
            .map(|(i, s)| (i, s.to_vec()))
            .collect();
        assert_eq!(build, vec![(0, vec![]), (1, vec![7]), (2, vec![])]);
        f.flatten();
        let flat: Vec<(usize, Vec<i32>)> = f
            .iter_node_slices()
            .map(|(i, s)| (i, s.to_vec()))
            .collect();
        assert_eq!(flat, build, "iteration is phase-independent");
    }
    #[test]
    fn flatvecvec_iter_node_slices_on_empty_yields_nothing() {
        let f = FlatVecVec::<i32>::new(0);
        assert_eq!(f.iter_node_slices().count(), 0);
    }
    #[test]
    fn flatvecvec_extend_from_both_in_build_phase() {
        let mut a = FlatVecVec::<i32>::new(1);
        a.push_to(0, 1);
        let mut b = FlatVecVec::<i32>::new(2);
        b.push_to(0, 2);
        b.push_to(1, 3);
        a.extend_from(&mut b);
        assert_eq!(a.len(), 3);
        assert_eq!(a.get_slice(0), &[1]);
        assert_eq!(a.get_slice(1), &[2]);
        assert_eq!(a.get_slice(2), &[3]);
        assert_eq!(b.len(), 0, "other is drained");
    }
    #[test]
    fn flatvecvec_extend_from_both_flattened_rebases_offsets() {
        let mut a = FlatVecVec::<i32>::new(2);
        a.push_to(0, 1);
        a.push_to(1, 2);
        a.flatten();
        let mut b = FlatVecVec::<i32>::new(2);
        b.push_to(0, 3);
        b.push_to(1, 4);
        b.flatten();
        a.extend_from(&mut b);
        assert_eq!(a.len(), 4);
        assert_eq!(a.get_slice(0), &[1]);
        assert_eq!(a.get_slice(1), &[2]);
        assert_eq!(a.get_slice(2), &[3], "offsets rebased onto a's flat data");
        assert_eq!(a.get_slice(3), &[4]);
    }
    #[test]
    fn flatvecvec_extend_from_across_phases_discards_self_flat_data() {
        // Doc precondition: "Both must be in build phase, or both must be
        // flattened." This pins what a violation actually does today — the
        // flattened side's items are dropped on the floor rather than merged.
        let mut a = FlatVecVec::<i32>::new(1);
        a.push_to(0, 1);
        a.flatten();
        let mut b = FlatVecVec::<i32>::new(1);
        b.push_to(0, 2);
        a.extend_from(&mut b); // no panic...
        assert_eq!(a.len(), 1);
        assert_eq!(
            a.get_slice(0),
            &[2],
            "a's own flattened item (1) is silently lost"
        );
    }
    #[test]
    fn flatvecvec_eq_within_the_same_phase() {
        let mut a = FlatVecVec::<i32>::new(1);
        a.push_to(0, 1);
        let mut b = FlatVecVec::<i32>::new(1);
        b.push_to(0, 1);
        assert_eq!(a, b);
        b.push_to(0, 2);
        assert_ne!(a, b);
        a.flatten();
        // (a and c are both flattened below — equality is only meaningful
        // between two caches in the same phase)
        let mut c = FlatVecVec::<i32>::new(1);
        c.push_to(0, 1);
        c.flatten();
        assert_eq!(a, c);
    }
    // =====================================================================
    // CssPropertyCacheBreakdown
    // =====================================================================
    #[test]
    fn breakdown_total_bytes_sums_subfields_and_excludes_node_count() {
        let b = CssPropertyCacheBreakdown {
            node_count: 999_999,
            cascaded_props_bytes: 1,
            css_props_bytes: 2,
            computed_values_bytes: 4,
            user_overridden_bytes: 8,
            global_css_props_bytes: 16,
            compact_cache_bytes: 32,
            resolved_font_sizes_bytes: 64,
        };
        assert_eq!(b.total_bytes(), 127, "node_count is not a byte count");
    }
    #[test]
    fn breakdown_total_bytes_default_is_zero_and_max_single_field_does_not_overflow() {
        assert_eq!(CssPropertyCacheBreakdown::default().total_bytes(), 0);
        let b = CssPropertyCacheBreakdown {
            cascaded_props_bytes: usize::MAX,
            ..Default::default()
        };
        assert_eq!(b.total_bytes(), usize::MAX);
    }
    // =====================================================================
    // CssPropertyCache — construction / memory / append
    // =====================================================================
    #[test]
    fn cache_empty_zero_is_neutral() {
        let c = CssPropertyCache::empty(0);
        assert_eq!(c.node_count, 0);
        assert!(c.css_props.is_empty());
        assert!(c.cascaded_props.is_empty());
        assert!(c.computed_values.is_empty());
        assert!(c.user_overridden_properties.is_empty());
        assert!(c.global_css_props.is_empty());
        assert!(c.compact_cache.is_none());
        let b = c.memory_breakdown();
        assert_eq!(b.node_count, 0);
        assert_eq!(b.total_bytes(), 0, "a zero-node cache retains no heap");
    }
    #[test]
    fn cache_empty_invariants_hold() {
        let c = CssPropertyCache::empty(7);
        assert_eq!(c.node_count, 7);
        assert_eq!(c.css_props.len(), 7);
        assert_eq!(c.cascaded_props.len(), 7);
        assert!(!c.css_props.is_flattened(), "starts in build phase");
        assert!(c.compact_cache.is_none());
        let b = c.memory_breakdown();
        assert_eq!(b.node_count, 7);
        assert!(b.total_bytes() > 0);
        assert_eq!(b.compact_cache_bytes, 0);
        assert_eq!(b.resolved_font_sizes_bytes, 0);
    }
    #[test]
    fn cache_invalidate_resolved_font_sizes_clears_the_once_lock() {
        let mut c = CssPropertyCache::empty(1);
        assert!(c.resolved_font_sizes_px.set(vec![16.0]).is_ok());
        assert!(c.resolved_font_sizes_px.get().is_some());
        c.invalidate_resolved_font_sizes();
        assert!(
            c.resolved_font_sizes_px.get().is_none(),
            "next read must recompute"
        );
        // and it can be re-populated afterwards
        assert!(c.resolved_font_sizes_px.set(vec![12.0]).is_ok());
    }
    #[test]
    fn cache_append_sums_nodes_and_invalidates_derived_caches() {
        let mut a = CssPropertyCache::empty(2);
        let mut b = CssPropertyCache::empty(3);
        assert!(a.resolved_font_sizes_px.set(vec![16.0, 16.0]).is_ok());
        a.append(&mut b);
        assert_eq!(a.node_count, 5);
        assert_eq!(a.css_props.len(), 5);
        assert_eq!(a.cascaded_props.len(), 5);
        assert!(
            a.resolved_font_sizes_px.get().is_none(),
            "node indices shifted"
        );
        assert!(a.compact_cache.is_none());
    }
    #[test]
    fn cache_append_of_empty_cache_is_a_noop_on_node_count() {
        let mut a = CssPropertyCache::empty(2);
        let mut b = CssPropertyCache::empty(0);
        a.append(&mut b);
        assert_eq!(a.node_count, 2);
        assert_eq!(a.css_props.len(), 2);
    }
    #[test]
    fn cache_invalidate_resolved_cache_drops_compact_cache() {
        let mut c = CssPropertyCache::empty(1);
        c.invalidate_resolved_cache();
        assert!(c.compact_cache.is_none());
    }
    #[test]
    fn cache_ptr_new_and_downcast_roundtrip() {
        let mut p = CssPropertyCachePtr::new(CssPropertyCache::empty(4));
        assert!(p.run_destructor);
        assert_eq!(p.downcast_mut().node_count, 4);
        p.downcast_mut().node_count = 9;
        assert_eq!(p.downcast_mut().node_count, 9, "downcast_mut aliases the box");
    }
    // =====================================================================
    // Predicates (overflow / border / box-shadow)
    // =====================================================================
    #[test]
    fn overflow_predicates_default_to_visible_for_a_bare_div() {
        let c = CssPropertyCache::empty(1);
        let nd = NodeData::create_div();
        assert!(c.is_horizontal_overflow_visible(&nd, &n0(), &normal()));
        assert!(c.is_vertical_overflow_visible(&nd, &n0(), &normal()));
        assert!(!c.is_horizontal_overflow_hidden(&nd, &n0(), &normal()));
        assert!(!c.is_vertical_overflow_hidden(&nd, &n0(), &normal()));
    }
    #[test]
    fn overflow_predicates_are_per_axis() {
        let c = CssPropertyCache::empty(1);
        let nd = div_with(vec![CssProperty::OverflowX(CssPropertyValue::Exact(
            LayoutOverflow::Hidden,
        ))]);
        assert!(c.is_horizontal_overflow_hidden(&nd, &n0(), &normal()));
        assert!(!c.is_horizontal_overflow_visible(&nd, &n0(), &normal()));
        // the Y axis must be untouched
        assert!(!c.is_vertical_overflow_hidden(&nd, &n0(), &normal()));
        assert!(c.is_vertical_overflow_visible(&nd, &n0(), &normal()));
    }
    #[test]
    fn overflow_predicates_do_not_panic_on_an_out_of_range_node_id() {
        let c = CssPropertyCache::empty(0);
        let nd = NodeData::create_div();
        let far = NodeId::new(999_999);
        assert!(c.is_horizontal_overflow_visible(&nd, &far, &normal()));
        assert!(!c.is_vertical_overflow_hidden(&nd, &far, &normal()));
    }
    #[test]
    fn has_border_false_without_and_true_with_a_border_width() {
        let c = CssPropertyCache::empty(1);
        assert!(!c.has_border(&NodeData::create_div(), &n0(), &normal()));
        let bordered = div_with(vec![CssProperty::BorderLeftWidth(CssPropertyValue::Exact(
            LayoutBorderLeftWidth {
                inner: PixelValue::px(2.0),
            },
        ))]);
        assert!(c.has_border(&bordered, &n0(), &normal()));
    }
    #[test]
    fn has_box_shadow_false_for_a_bare_div() {
        let c = CssPropertyCache::empty(1);
        assert!(!c.has_box_shadow(&NodeData::create_div(), &n0(), &normal()));
        // out-of-range node id must not panic either
        assert!(!c.has_box_shadow(&NodeData::create_div(), &NodeId::new(500), &normal()));
    }
    // =====================================================================
    // `*_or_default` getters
    // =====================================================================
    #[test]
    fn or_default_getters_fall_back_to_the_css_defaults() {
        let c = CssPropertyCache::empty(1);
        let nd = NodeData::create_div();
        assert_eq!(
            c.get_font_size_or_default(&nd, &n0(), &normal()),
            azul_css::defaults::DEFAULT_FONT_SIZE
        );
        assert_eq!(
            c.get_text_color_or_default(&nd, &n0(), &normal()),
            azul_css::defaults::DEFAULT_TEXT_COLOR
        );
        let fams = c.get_font_id_or_default(&nd, &n0(), &normal());
        assert_eq!(fams.as_ref().len(), 1);
        match &fams.as_ref()[0] {
            StyleFontFamily::System(s) => {
                assert_eq!(s.as_str(), azul_css::defaults::DEFAULT_FONT_ID);
            }
            other => panic!("expected the default System font family, got {other:?}"),
        }
    }
    #[test]
    fn get_font_size_or_default_prefers_the_inline_value() {
        let c = CssPropertyCache::empty(1);
        let nd = div_with(vec![font_size(PixelValue::px(42.0))]);
        let fs = c.get_font_size_or_default(&nd, &n0(), &normal());
        assert!(close(fs.inner.number.get(), 42.0));
        assert_eq!(fs.inner.metric, SizeMetric::Px);
    }
    #[test]
    fn or_default_getters_survive_an_out_of_range_node_id() {
        let c = CssPropertyCache::empty(0);
        let nd = NodeData::create_div();
        let far = NodeId::new(usize::MAX / 2);
        assert_eq!(
            c.get_font_size_or_default(&nd, &far, &normal()),
            azul_css::defaults::DEFAULT_FONT_SIZE
        );
        assert_eq!(c.get_font_id_or_default(&nd, &far, &normal()).as_ref().len(), 1);
    }
    // =====================================================================
    // calc_* (numeric: zero / negative / NaN / inf / saturation)
    // =====================================================================
    #[test]
    fn calc_width_is_zero_when_unset() {
        let c = CssPropertyCache::empty(1);
        let nd = NodeData::create_div();
        assert_eq!(c.calc_width(&nd, &n0(), &normal(), 800.0), 0.0);
        assert_eq!(c.calc_width(&nd, &n0(), &normal(), 0.0), 0.0);
        assert_eq!(c.calc_height(&nd, &n0(), &normal(), f32::NAN), 0.0);
    }
    #[test]
    fn calc_width_resolves_px_and_percent() {
        let c = CssPropertyCache::empty(1);
        let px = div_with(vec![width_px(100.0)]);
        assert!(close(c.calc_width(&px, &n0(), &normal(), 800.0), 100.0));
        // px must ignore the reference entirely
        assert!(close(c.calc_width(&px, &n0(), &normal(), 0.0), 100.0));
        let pct = div_with(vec![width_pct(50.0)]);
        assert!(close(c.calc_width(&pct, &n0(), &normal(), 800.0), 400.0));
        assert!(close(c.calc_width(&pct, &n0(), &normal(), 0.0), 0.0));
    }
    #[test]
    fn calc_width_with_a_negative_reference_is_negative_not_clamped() {
        let c = CssPropertyCache::empty(1);
        let pct = div_with(vec![width_pct(50.0)]);
        assert!(close(c.calc_width(&pct, &n0(), &normal(), -800.0), -400.0));
    }
    #[test]
    fn calc_width_with_nan_and_infinite_references_is_defined() {
        let c = CssPropertyCache::empty(1);
        let pct = div_with(vec![width_pct(50.0)]);
        assert!(c.calc_width(&pct, &n0(), &normal(), f32::NAN).is_nan());
        assert_eq!(
            c.calc_width(&pct, &n0(), &normal(), f32::INFINITY),
            f32::INFINITY
        );
        assert_eq!(
            c.calc_width(&pct, &n0(), &normal(), f32::NEG_INFINITY),
            f32::NEG_INFINITY
        );
    }
    #[test]
    fn calc_width_saturates_non_finite_pixel_values_at_construction() {
        let c = CssPropertyCache::empty(1);
        // PixelValue stores a fixed-point isize, so `as isize` saturates:
        // NaN => 0, +inf => isize::MAX, -inf => isize::MIN. Nothing panics and
        // nothing leaks a NaN into layout.
        let nan = div_with(vec![width_px(f32::NAN)]);
        assert_eq!(c.calc_width(&nan, &n0(), &normal(), 800.0), 0.0);
        let inf = div_with(vec![width_px(f32::INFINITY)]);
        let got = c.calc_width(&inf, &n0(), &normal(), 800.0);
        assert!(got.is_finite() && got > 0.0, "saturated, got {got}");
        let neg_inf = div_with(vec![width_px(f32::NEG_INFINITY)]);
        let got = c.calc_width(&neg_inf, &n0(), &normal(), 800.0);
        assert!(got.is_finite() && got < 0.0, "saturated, got {got}");
        let huge = div_with(vec![width_px(f32::MAX)]);
        assert!(c.calc_width(&huge, &n0(), &normal(), 800.0).is_finite());
    }
    #[test]
    fn calc_width_of_auto_and_intrinsic_keywords_is_zero() {
        let c = CssPropertyCache::empty(1);
        let auto = div_with(vec![CssProperty::Width(CssPropertyValue::Auto)]);
        assert_eq!(c.calc_width(&auto, &n0(), &normal(), 800.0), 0.0);
        // min-content/max-content are not resolvable here; documented as 0.0.
        let min_content = div_with(vec![CssProperty::Width(CssPropertyValue::Exact(
            LayoutWidth::MinContent,
        ))]);
        assert_eq!(c.calc_width(&min_content, &n0(), &normal(), 800.0), 0.0);
    }
    #[test]
    fn calc_height_mirrors_calc_width() {
        let c = CssPropertyCache::empty(1);
        let nd = div_with(vec![CssProperty::Height(CssPropertyValue::Exact(
            LayoutHeight::Px(PixelValue::percent(25.0)),
        ))]);
        assert!(close(c.calc_height(&nd, &n0(), &normal(), 400.0), 100.0));
        assert!(c.calc_height(&nd, &n0(), &normal(), f32::NAN).is_nan());
    }
    #[test]
    fn calc_min_width_defaults_to_zero_and_max_width_defaults_to_none() {
        let c = CssPropertyCache::empty(1);
        let nd = NodeData::create_div();
        assert_eq!(c.calc_min_width(&nd, &n0(), &normal(), 800.0), 0.0);
        assert_eq!(c.calc_min_height(&nd, &n0(), &normal(), 600.0), 0.0);
        assert_eq!(c.calc_max_width(&nd, &n0(), &normal(), 800.0), None);
        assert_eq!(c.calc_max_height(&nd, &n0(), &normal(), 600.0), None);
    }
    #[test]
    fn calc_min_max_width_resolve_percentages_and_propagate_nan() {
        let c = CssPropertyCache::empty(1);
        let nd = div_with(vec![
            CssProperty::MinWidth(CssPropertyValue::Exact(LayoutMinWidth {
                inner: PixelValue::percent(10.0),
            })),
            CssProperty::MaxWidth(CssPropertyValue::Exact(LayoutMaxWidth {
                inner: PixelValue::percent(90.0),
            })),
        ]);
        assert!(close(c.calc_min_width(&nd, &n0(), &normal(), 1000.0), 100.0));
        assert!(close(
            c.calc_max_width(&nd, &n0(), &normal(), 1000.0).unwrap(),
            900.0
        ));
        assert!(c.calc_min_width(&nd, &n0(), &normal(), f32::NAN).is_nan());
        assert!(c
            .calc_max_width(&nd, &n0(), &normal(), f32::NAN)
            .unwrap()
            .is_nan());
    }
    #[test]
    fn calc_inset_getters_are_none_when_unset_and_some_when_set() {
        let c = CssPropertyCache::empty(1);
        let bare = NodeData::create_div();
        assert_eq!(c.calc_left(&bare, &n0(), &normal(), 800.0), None);
        assert_eq!(c.calc_right(&bare, &n0(), &normal(), 800.0), None);
        assert_eq!(c.calc_top(&bare, &n0(), &normal(), 600.0), None);
        assert_eq!(c.calc_bottom(&bare, &n0(), &normal(), 600.0), None);
        let inset = div_with(vec![
            CssProperty::Left(CssPropertyValue::Exact(LayoutLeft {
                inner: PixelValue::px(5.0),
            })),
            CssProperty::Right(CssPropertyValue::Exact(LayoutRight {
                inner: PixelValue::percent(10.0),
            })),
            CssProperty::Top(CssPropertyValue::Exact(LayoutTop {
                inner: PixelValue::px(-7.0),
            })),
            CssProperty::Bottom(CssPropertyValue::Exact(LayoutInsetBottom {
                inner: PixelValue::px(0.0),
            })),
        ]);
        assert!(close(c.calc_left(&inset, &n0(), &normal(), 800.0).unwrap(), 5.0));
        assert!(close(
            c.calc_right(&inset, &n0(), &normal(), 800.0).unwrap(),
            80.0
        ));
        assert!(close(
            c.calc_top(&inset, &n0(), &normal(), 600.0).unwrap(),
            -7.0
        ));
        assert_eq!(c.calc_bottom(&inset, &n0(), &normal(), 600.0), Some(0.0));
    }
    #[test]
    fn calc_padding_margin_border_default_to_zero() {
        let c = CssPropertyCache::empty(1);
        let nd = NodeData::create_div();
        assert_eq!(c.calc_padding_left(&nd, &n0(), &normal(), 800.0), 0.0);
        assert_eq!(c.calc_padding_right(&nd, &n0(), &normal(), 800.0), 0.0);
        assert_eq!(c.calc_padding_top(&nd, &n0(), &normal(), 600.0), 0.0);
        assert_eq!(c.calc_padding_bottom(&nd, &n0(), &normal(), 600.0), 0.0);
        assert_eq!(c.calc_margin_left(&nd, &n0(), &normal(), 800.0), 0.0);
        assert_eq!(c.calc_margin_right(&nd, &n0(), &normal(), 800.0), 0.0);
        assert_eq!(c.calc_margin_top(&nd, &n0(), &normal(), 600.0), 0.0);
        assert_eq!(c.calc_margin_bottom(&nd, &n0(), &normal(), 600.0), 0.0);
        assert_eq!(c.calc_border_left_width(&nd, &n0(), &normal(), 800.0), 0.0);
        assert_eq!(c.calc_border_right_width(&nd, &n0(), &normal(), 800.0), 0.0);
        assert_eq!(c.calc_border_top_width(&nd, &n0(), &normal(), 600.0), 0.0);
        assert_eq!(c.calc_border_bottom_width(&nd, &n0(), &normal(), 600.0), 0.0);
    }
    #[test]
    fn calc_padding_em_uses_the_default_font_size_not_the_reference() {
        // `calc_*` passes DEFAULT_FONT_SIZE (16px) as both em and rem resolvers,
        // so an em padding must be invariant under the reference width.
        let c = CssPropertyCache::empty(1);
        let nd = div_with(vec![CssProperty::PaddingLeft(CssPropertyValue::Exact(
            LayoutPaddingLeft {
                inner: PixelValue::em(2.0),
            },
        ))]);
        assert!(close(c.calc_padding_left(&nd, &n0(), &normal(), 800.0), 32.0));
        assert!(close(c.calc_padding_left(&nd, &n0(), &normal(), 0.0), 32.0));
        assert!(close(
            c.calc_padding_left(&nd, &n0(), &normal(), f32::NAN),
            32.0
        ));
    }
    #[test]
    fn calc_margin_and_border_resolve_px_and_percent() {
        let c = CssPropertyCache::empty(1);
        let nd = div_with(vec![
            CssProperty::MarginTop(CssPropertyValue::Exact(LayoutMarginTop {
                inner: PixelValue::percent(50.0),
            })),
            CssProperty::BorderLeftWidth(CssPropertyValue::Exact(LayoutBorderLeftWidth {
                inner: PixelValue::px(3.0),
            })),
        ]);
        assert!(close(c.calc_margin_top(&nd, &n0(), &normal(), 200.0), 100.0));
        assert!(close(
            c.calc_border_left_width(&nd, &n0(), &normal(), 800.0),
            3.0
        ));
        assert!(c.calc_margin_top(&nd, &n0(), &normal(), f32::NAN).is_nan());
    }
    #[test]
    fn calc_getters_do_not_panic_on_an_out_of_range_node_id() {
        let c = CssPropertyCache::empty(0);
        let nd = NodeData::create_div();
        let far = NodeId::new(usize::MAX / 2);
        assert_eq!(c.calc_width(&nd, &far, &normal(), 800.0), 0.0);
        assert_eq!(c.calc_max_height(&nd, &far, &normal(), 600.0), None);
        assert_eq!(c.calc_padding_top(&nd, &far, &normal(), f32::INFINITY), 0.0);
    }
    // =====================================================================
    // property_needs_slow_path_after_compact
    // =====================================================================
    #[test]
    fn slow_path_only_needed_for_non_px_pixel_values() {
        // px round-trips through the compact cache => no slow path
        assert!(!property_needs_slow_path_after_compact(&width_px(10.0)));
        // % encodes to SENTINEL => must survive the prune
        assert!(property_needs_slow_path_after_compact(&width_pct(50.0)));
        assert!(!property_needs_slow_path_after_compact(&CssProperty::Height(
            CssPropertyValue::Exact(LayoutHeight::Px(PixelValue::px(1.0)))
        )));
        assert!(property_needs_slow_path_after_compact(&CssProperty::Height(
            CssPropertyValue::Exact(LayoutHeight::Px(PixelValue::em(1.0)))
        )));
    }
    #[test]
    fn slow_path_covers_the_plain_pixelvalue_wrappers() {
        assert!(property_needs_slow_path_after_compact(&font_size(
            PixelValue::rem(2.0)
        )));
        assert!(!property_needs_slow_path_after_compact(&font_size(
            PixelValue::px(16.0)
        )));
        assert!(property_needs_slow_path_after_compact(
            &CssProperty::MinWidth(CssPropertyValue::Exact(LayoutMinWidth {
                inner: PixelValue::percent(10.0),
            }))
        ));
        assert!(!property_needs_slow_path_after_compact(
            &CssProperty::PaddingLeft(CssPropertyValue::Exact(LayoutPaddingLeft {
                inner: PixelValue::px(4.0),
            }))
        ));
    }
    #[test]
    fn slow_path_handles_flex_basis_and_non_pixel_properties() {
        assert!(property_needs_slow_path_after_compact(
            &CssProperty::FlexBasis(CssPropertyValue::Exact(LayoutFlexBasis::Exact(
                PixelValue::percent(50.0)
            )))
        ));
        assert!(!property_needs_slow_path_after_compact(
            &CssProperty::FlexBasis(CssPropertyValue::Exact(LayoutFlexBasis::Auto))
        ));
        // Non-Exact keywords and non-pixel properties never need the slow path.
        assert!(!property_needs_slow_path_after_compact(&CssProperty::Width(
            CssPropertyValue::Auto
        )));
        assert!(!property_needs_slow_path_after_compact(
            &CssProperty::const_none(CssPropertyType::Display)
        ));
        assert!(!property_needs_slow_path_after_compact(
            &CssProperty::const_none(CssPropertyType::BackgroundContent)
        ));
    }
    // =====================================================================
    // clone_inheritable_property (round-trip)
    // =====================================================================
    #[test]
    fn clone_inheritable_property_round_trips_heap_and_pod_variants() {
        // The whole point of this hand-rolled clone is that it must be
        // byte-equivalent to the derived Clone on native.
        let font_family = CssProperty::FontFamily(CssPropertyValue::Exact(
            vec![StyleFontFamily::System(AzString::from_const_str("serif"))].into(),
        ));
        assert_eq!(clone_inheritable_property(&font_family), font_family);
        for p in [
            CssProperty::const_none(CssPropertyType::Cursor),
            CssProperty::const_none(CssPropertyType::TextColor),
            CssProperty::const_none(CssPropertyType::BackgroundContent),
            CssProperty::const_none(CssPropertyType::Transform),
            CssProperty::const_none(CssPropertyType::Content),
            width_px(3.0),
            font_size(PixelValue::em(1.5)),
        ] {
            assert_eq!(clone_inheritable_property(&p), p, "clone must be identity");
            assert_eq!(clone_inheritable_property(&p).get_type(), p.get_type());
        }
    }
    // =====================================================================
    // find_in_stateful / has_state_props / prop_types_for_state
    // =====================================================================
    fn sorted_stateful_fixture() -> Vec<StatefulCssProperty> {
        let mut v = vec![
            stateful(PseudoStateType::Normal, width_px(1.0)),
            stateful(
                PseudoStateType::Normal,
                CssProperty::const_none(CssPropertyType::Display),
            ),
            stateful(PseudoStateType::Hover, width_px(2.0)),
        ];
        // The lookup helpers require (state, prop_type) sort order.
        v.sort_by_key(|p| (p.state, p.prop_type));
        v
    }
    #[test]
    fn find_in_stateful_on_an_empty_slice_is_none() {
        assert!(CssPropertyCache::find_in_stateful(
            &[],
            PseudoStateType::Normal,
            &CssPropertyType::Width
        )
        .is_none());
    }
    #[test]
    fn find_in_stateful_is_keyed_on_both_state_and_prop_type() {
        let v = sorted_stateful_fixture();
        let normal_width =
            CssPropertyCache::find_in_stateful(&v, PseudoStateType::Normal, &CssPropertyType::Width)
                .expect("normal width present");
        assert_eq!(normal_width.get_type(), CssPropertyType::Width);
        let hover_width =
            CssPropertyCache::find_in_stateful(&v, PseudoStateType::Hover, &CssPropertyType::Width)
                .expect("hover width present");
        // same prop type, different state => a different entry
        assert_ne!(normal_width, hover_width);
        // present prop type, absent state
        assert!(CssPropertyCache::find_in_stateful(
            &v,
            PseudoStateType::Focus,
            &CssPropertyType::Width
        )
        .is_none());
        // present state, absent prop type
        assert!(CssPropertyCache::find_in_stateful(
            &v,
            PseudoStateType::Hover,
            &CssPropertyType::Display
        )
        .is_none());
    }
    #[test]
    fn has_state_props_true_false_and_edges() {
        let v = sorted_stateful_fixture();
        assert!(CssPropertyCache::has_state_props(&v, PseudoStateType::Normal));
        assert!(CssPropertyCache::has_state_props(&v, PseudoStateType::Hover));
        assert!(!CssPropertyCache::has_state_props(&v, PseudoStateType::Focus));
        // empty slice: deterministic false, no partition_point OOB read
        assert!(!CssPropertyCache::has_state_props(
            &[],
            PseudoStateType::Normal
        ));
    }
    #[test]
    fn prop_types_for_state_filters_by_state() {
        let v = sorted_stateful_fixture();
        let mut normal: Vec<CssPropertyType> =
            CssPropertyCache::prop_types_for_state(&v, PseudoStateType::Normal)
                .copied()
                .collect();
        normal.sort_unstable();
        assert_eq!(normal.len(), 2);
        assert!(normal.contains(&CssPropertyType::Width));
        assert!(normal.contains(&CssPropertyType::Display));
        let hover: Vec<CssPropertyType> =
            CssPropertyCache::prop_types_for_state(&v, PseudoStateType::Hover)
                .copied()
                .collect();
        assert_eq!(hover, vec![CssPropertyType::Width]);
        assert_eq!(
            CssPropertyCache::prop_types_for_state(&v, PseudoStateType::Active).count(),
            0
        );
        assert_eq!(
            CssPropertyCache::prop_types_for_state(&[], PseudoStateType::Normal).count(),
            0
        );
    }
    // =====================================================================
    // font-size resolution (numeric)
    // =====================================================================
    #[test]
    fn resolve_font_size_to_pixels_converts_absolute_units() {
        let px = CssPropertyCache::resolve_font_size_to_pixels(&font_size(PixelValue::px(20.0)), 10.0);
        let (metric, n) = font_size_parts(&px).unwrap();
        assert_eq!(metric, SizeMetric::Px);
        assert!(close(n, 20.0));
        let pt = CssPropertyCache::resolve_font_size_to_pixels(&font_size(PixelValue::pt(12.0)), 10.0);
        assert!(close(font_size_parts(&pt).unwrap().1, 12.0 * PT_TO_PX));
    }
    #[test]
    fn resolve_font_size_to_pixels_em_scales_by_reference_but_rem_does_not() {
        let em =
            CssPropertyCache::resolve_font_size_to_pixels(&font_size(PixelValue::em(2.0)), 10.0);
        assert!(close(font_size_parts(&em).unwrap().1, 20.0));
        // rem deliberately ignores the reference and uses DEFAULT_FONT_SIZE (16).
        let rem =
            CssPropertyCache::resolve_font_size_to_pixels(&font_size(PixelValue::rem(2.0)), 10.0);
        assert!(close(font_size_parts(&rem).unwrap().1, 32.0));
        let pct = CssPropertyCache::resolve_font_size_to_pixels(
            &font_size(PixelValue::percent(50.0)),
            10.0,
        );
        assert!(close(font_size_parts(&pct).unwrap().1, 5.0));
    }
    #[test]
    fn resolve_font_size_to_pixels_with_nan_and_infinite_references() {
        // NaN * anything => NaN => saturates to 0 in the fixed-point encoding.
        let nan =
            CssPropertyCache::resolve_font_size_to_pixels(&font_size(PixelValue::em(2.0)), f32::NAN);
        let (metric, n) = font_size_parts(&nan).unwrap();
        assert_eq!(metric, SizeMetric::Px);
        assert_eq!(n, 0.0, "NaN must not escape into the cascade");
        let inf = CssPropertyCache::resolve_font_size_to_pixels(
            &font_size(PixelValue::em(2.0)),
            f32::INFINITY,
        );
        let n = font_size_parts(&inf).unwrap().1;
        assert!(n.is_finite() && n > 0.0, "saturated, got {n}");
        let zero =
            CssPropertyCache::resolve_font_size_to_pixels(&font_size(PixelValue::em(2.0)), 0.0);
        assert_eq!(font_size_parts(&zero).unwrap().1, 0.0);
        let neg =
            CssPropertyCache::resolve_font_size_to_pixels(&font_size(PixelValue::em(2.0)), -10.0);
        assert!(close(font_size_parts(&neg).unwrap().1, -20.0));
    }
    #[test]
    fn resolve_font_size_to_pixels_passes_through_unresolvable_inputs() {
        // viewport units need a viewport => returned unchanged
        let vw = font_size(PixelValue::from_metric(SizeMetric::Vw, 10.0));
        assert_eq!(CssPropertyCache::resolve_font_size_to_pixels(&vw, 16.0), vw);
        // a non-font-size property is returned verbatim
        let w = width_px(10.0);
        assert_eq!(CssPropertyCache::resolve_font_size_to_pixels(&w, 16.0), w);
        // a keyword (non-Exact) font-size has no PixelValue to convert
        let inherit = CssProperty::FontSize(CssPropertyValue::Inherit);
        assert_eq!(
            CssPropertyCache::resolve_font_size_to_pixels(&inherit, 16.0),
            inherit
        );
    }
    #[test]
    fn has_relative_font_size_unit_true_false_and_edges() {
        assert!(CssPropertyCache::has_relative_font_size_unit(&font_size(
            PixelValue::em(1.0)
        )));
        assert!(CssPropertyCache::has_relative_font_size_unit(&font_size(
            PixelValue::rem(1.0)
        )));
        assert!(CssPropertyCache::has_relative_font_size_unit(&font_size(
            PixelValue::percent(100.0)
        )));
        assert!(!CssPropertyCache::has_relative_font_size_unit(&font_size(
            PixelValue::px(16.0)
        )));
        assert!(!CssPropertyCache::has_relative_font_size_unit(&font_size(
            PixelValue::pt(12.0)
        )));
        // keyword font-size and non-font-size properties are not "relative"
        assert!(!CssPropertyCache::has_relative_font_size_unit(
            &CssProperty::FontSize(CssPropertyValue::Auto)
        ));
        assert!(!CssPropertyCache::has_relative_font_size_unit(&width_px(1.0)));
    }
    // =====================================================================
    // resolve_property_dependency
    // =====================================================================
    #[test]
    fn resolve_property_dependency_scales_relative_targets_by_an_absolute_reference() {
        let reference = font_size(PixelValue::px(10.0));
        let em = CssPropertyCache::resolve_property_dependency(
            &font_size(PixelValue::em(2.0)),
            &reference,
        )
        .expect("em resolves against an absolute reference");
        assert!(close(font_size_parts(&em).unwrap().1, 20.0));
        let pct = CssPropertyCache::resolve_property_dependency(
            &font_size(PixelValue::percent(50.0)),
            &reference,
        )
        .expect("percent resolves");
        assert!(close(font_size_parts(&pct).unwrap().1, 5.0));
        // The reference itself may be in any absolute unit.
        let pt_ref = font_size(PixelValue::pt(10.0));
        let em2 =
            CssPropertyCache::resolve_property_dependency(&font_size(PixelValue::em(2.0)), &pt_ref)
                .expect("pt reference is absolute");
        assert!(close(
            font_size_parts(&em2).unwrap().1,
            2.0 * 10.0 * PT_TO_PX
        ));
    }
    #[test]
    fn resolve_property_dependency_rewrites_the_target_variant_in_place() {
        let reference = font_size(PixelValue::px(10.0));
        let padding = CssProperty::PaddingLeft(CssPropertyValue::Exact(LayoutPaddingLeft {
            inner: PixelValue::em(3.0),
        }));
        let out = CssPropertyCache::resolve_property_dependency(&padding, &reference)
            .expect("padding is a supported target");
        match out {
            CssProperty::PaddingLeft(v) => {
                let inner = v.get_property().unwrap().inner;
                assert_eq!(inner.metric, SizeMetric::Px);
                assert!(close(inner.number.get(), 30.0));
            }
            other => panic!("variant must be preserved, got {other:?}"),
        }
    }
    #[test]
    fn resolve_property_dependency_returns_none_for_unresolvable_inputs() {
        let abs = font_size(PixelValue::px(10.0));
        // a relative reference cannot anchor anything
        assert!(CssPropertyCache::resolve_property_dependency(
            &font_size(PixelValue::em(2.0)),
            &font_size(PixelValue::em(2.0))
        )
        .is_none());
        // viewport-unit target needs a viewport
        assert!(CssPropertyCache::resolve_property_dependency(
            &font_size(PixelValue::from_metric(SizeMetric::Vh, 5.0)),
            &abs
        )
        .is_none());
        // unsupported target type (no PixelValue to extract)
        assert!(CssPropertyCache::resolve_property_dependency(&width_px(5.0), &abs).is_none());
        // unsupported reference type
        assert!(CssPropertyCache::resolve_property_dependency(
            &font_size(PixelValue::em(2.0)),
            &width_px(5.0)
        )
        .is_none());
        // keyword (non-Exact) target
        assert!(CssPropertyCache::resolve_property_dependency(
            &CssProperty::FontSize(CssPropertyValue::Inherit),
            &abs
        )
        .is_none());
    }
    // =====================================================================
    // should_apply_cascaded
    // =====================================================================
    #[test]
    fn should_apply_cascaded_respects_origin_and_relative_font_sizes() {
        let own = |p: CssProperty| {
            vec![(
                p.get_type(),
                CssPropertyWithOrigin {
                    property: p,
                    origin: CssPropertyOrigin::Own,
                },
            )]
        };
        let inherited = |p: CssProperty| {
            vec![(
                p.get_type(),
                CssPropertyWithOrigin {
                    property: p,
                    origin: CssPropertyOrigin::Inherited,
                },
            )]
        };
        // nothing computed yet => apply
        assert!(CssPropertyCache::should_apply_cascaded(
            &[],
            CssPropertyType::Width,
            &width_px(1.0)
        ));
        // the node already set it itself => the UA/cascaded value must not win
        assert!(!CssPropertyCache::should_apply_cascaded(
            &own(width_px(2.0)),
            CssPropertyType::Width,
            &width_px(1.0)
        ));
        // an inherited value is weaker than a cascaded one => apply
        assert!(CssPropertyCache::should_apply_cascaded(
            &inherited(width_px(2.0)),
            CssPropertyType::Width,
            &width_px(1.0)
        ));
        // A cascaded (UA/author) font-size — relative OR absolute — overrides an
        // inherited value: it is the node's own declared size (e.g. <h1>'s UA
        // `font-size: 2em`), and `resolve_font_size_property` resolves the `em`
        // against the parent's size, so there is no double-scaling.
        let inherited_fs = inherited(font_size(PixelValue::px(20.0)));
        assert!(CssPropertyCache::should_apply_cascaded(
            &inherited_fs,
            CssPropertyType::FontSize,
            &font_size(PixelValue::em(2.0))
        ));
        assert!(CssPropertyCache::should_apply_cascaded(
            &inherited_fs,
            CssPropertyType::FontSize,
            &font_size(PixelValue::px(12.0))
        ));
    }
    // =====================================================================
    // get_property / get_property_slow (cascade layering)
    // =====================================================================
    #[test]
    fn get_property_finds_an_inline_normal_property() {
        let c = CssPropertyCache::empty(1);
        let nd = div_with(vec![width_px(100.0)]);
        let got = c
            .get_property(&nd, &n0(), &normal(), &CssPropertyType::Width)
            .expect("inline width");
        assert_eq!(*got, width_px(100.0));
    }
    #[test]
    fn get_property_ignores_pseudo_state_props_unless_the_state_is_active() {
        let c = CssPropertyCache::empty(1);
        let nd = div_with_pseudo(vec![width_px(100.0)], PseudoStateType::Hover);
        assert!(
            c.get_property(&nd, &n0(), &normal(), &CssPropertyType::Width)
                .is_none(),
            ":hover width must not leak into the Normal state"
        );
        let hovered = StyledNodeState {
            hover: true,
            ..StyledNodeState::default()
        };
        assert_eq!(
            c.get_property(&nd, &n0(), &hovered, &CssPropertyType::Width),
            Some(&width_px(100.0))
        );
    }
    #[test]
    fn get_property_user_override_beats_inline_and_stylesheet() {
        let mut c = CssPropertyCache::empty(1);
        c.user_overridden_properties
            .push(vec![(CssPropertyType::Width, width_px(1.0))]);
        c.css_props
            .push_to(0, stateful(PseudoStateType::Normal, width_px(2.0)));
        c.css_props.sort_each_and_flatten(|p| (p.state, p.prop_type));
        let nd = div_with(vec![width_px(3.0)]);
        assert_eq!(
            c.get_property(&nd, &n0(), &normal(), &CssPropertyType::Width),
            Some(&width_px(1.0)),
            "user override is the top cascade layer"
        );
    }
    #[test]
    fn get_property_falls_back_through_stylesheet_global_cascaded_then_ua() {
        let nd = NodeData::create_div();
        // stylesheet layer
        let mut c = CssPropertyCache::empty(1);
        c.css_props
            .push_to(0, stateful(PseudoStateType::Normal, width_px(2.0)));
        c.css_props.sort_each_and_flatten(|p| (p.state, p.prop_type));
        assert_eq!(
            c.get_property(&nd, &n0(), &normal(), &CssPropertyType::Width),
            Some(&width_px(2.0))
        );
        // `*` global layer (below per-node rules)
        let mut c = CssPropertyCache::empty(1);
        c.global_css_props.push(width_px(4.0));
        assert_eq!(
            c.get_property(&nd, &n0(), &normal(), &CssPropertyType::Width),
            Some(&width_px(4.0))
        );
        // cascaded (inherited/UA) layer
        let mut c = CssPropertyCache::empty(1);
        c.cascaded_props
            .push_to(0, stateful(PseudoStateType::Normal, width_px(5.0)));
        c.cascaded_props
            .sort_each_and_flatten(|p| (p.state, p.prop_type));
        assert_eq!(
            c.get_property(&nd, &n0(), &normal(), &CssPropertyType::Width),
            Some(&width_px(5.0))
        );
        // UA fallback: a <div> has no UA width, but it does have `display: block`
        let c = CssPropertyCache::empty(1);
        assert!(c
            .get_property(&nd, &n0(), &normal(), &CssPropertyType::Width)
            .is_none());
        assert!(c
            .get_property(&nd, &n0(), &normal(), &CssPropertyType::Display)
            .is_some());
    }
    #[test]
    fn get_property_on_an_out_of_range_node_id_falls_through_to_ua_css() {
        let c = CssPropertyCache::empty(0);
        let nd = NodeData::create_div();
        let far = NodeId::new(usize::MAX / 2);
        assert!(c
            .get_property(&nd, &far, &normal(), &CssPropertyType::Width)
            .is_none());
        assert!(
            c.get_property(&nd, &far, &normal(), &CssPropertyType::Display)
                .is_some(),
            "UA CSS is node-type-keyed, not index-keyed"
        );
    }
    #[test]
    fn get_property_with_context_matches_pseudo_state_conditions() {
        let c = CssPropertyCache::empty(1);
        let nd = div_with_pseudo(vec![width_px(100.0)], PseudoStateType::Hover);
        let plain = DynamicSelectorContext::default();
        assert!(c
            .get_property_with_context(&nd, &n0(), &plain, &CssPropertyType::Width)
            .is_none());
        let mut hovered = DynamicSelectorContext::default();
        hovered.pseudo_state.hover = true;
        assert_eq!(
            c.get_property_with_context(&nd, &n0(), &hovered, &CssPropertyType::Width),
            Some(&width_px(100.0))
        );
    }
    #[test]
    fn check_properties_changed_only_fires_when_a_condition_flips() {
        let plain = DynamicSelectorContext::default();
        let mut hovered = DynamicSelectorContext::default();
        hovered.pseudo_state.hover = true;
        // unconditional props never "change" between contexts
        let unconditional = div_with(vec![width_px(1.0)]);
        assert!(!CssPropertyCache::check_properties_changed(
            &unconditional,
            &plain,
            &hovered
        ));
        let conditional = div_with_pseudo(vec![width_px(1.0)], PseudoStateType::Hover);
        assert!(CssPropertyCache::check_properties_changed(
            &conditional,
            &plain,
            &hovered
        ));
        assert!(
            !CssPropertyCache::check_properties_changed(&conditional, &plain, &plain),
            "identical contexts can never differ"
        );
        // a node with no inline style at all
        assert!(!CssPropertyCache::check_properties_changed(
            &NodeData::create_div(),
            &plain,
            &hovered
        ));
    }
    #[test]
    fn check_layout_properties_changed_ignores_non_layout_properties() {
        let plain = DynamicSelectorContext::default();
        let mut hovered = DynamicSelectorContext::default();
        hovered.pseudo_state.hover = true;
        let layout = div_with_pseudo(vec![width_px(1.0)], PseudoStateType::Hover);
        assert!(CssPropertyCache::check_layout_properties_changed(
            &layout, &plain, &hovered
        ));
        assert!(CssPropertyType::Width.can_trigger_relayout());
        // A paint-only property flipping must not force a relayout.
        let paint = div_with_pseudo(
            vec![CssProperty::const_none(CssPropertyType::BackgroundContent)],
            PseudoStateType::Hover,
        );
        assert!(!CssPropertyType::BackgroundContent.can_trigger_relayout());
        assert!(!CssPropertyCache::check_layout_properties_changed(
            &paint, &plain, &hovered
        ));
        // ...though the generic check still sees it
        assert!(CssPropertyCache::check_properties_changed(
            &paint, &plain, &hovered
        ));
    }
    // =====================================================================
    // grid-gap / scrollbar getters
    // =====================================================================
    #[test]
    fn grid_gap_and_scrollbar_getters_are_none_on_a_bare_div() {
        let c = CssPropertyCache::empty(1);
        let nd = NodeData::create_div();
        assert!(c.get_grid_gap(&nd, &n0(), &normal()).is_none());
        assert!(c.get_scrollbar_track(&nd, &n0(), &normal()).is_none());
        assert!(c.get_scrollbar_thumb(&nd, &n0(), &normal()).is_none());
        assert!(c.get_scrollbar_button(&nd, &n0(), &normal()).is_none());
        assert!(c.get_scrollbar_corner(&nd, &n0(), &normal()).is_none());
        assert!(c.get_scrollbar_resizer(&nd, &n0(), &normal()).is_none());
        // and on an out-of-range node id
        let far = NodeId::new(4_242);
        assert!(c.get_grid_gap(&nd, &far, &normal()).is_none());
        assert!(c.get_scrollbar_thumb(&nd, &far, &normal()).is_none());
    }
    // =====================================================================
    // get_computed_css_style_string
    // =====================================================================
    #[test]
    fn computed_css_style_string_serializes_set_properties() {
        let c = CssPropertyCache::empty(1);
        // A bare <div> still gets `display: block` from the UA sheet.
        let s = c.get_computed_css_style_string(&NodeData::create_div(), &n0(), &normal());
        assert!(s.contains("display:"), "got {s:?}");
        let styled = div_with(vec![width_px(100.0), font_size(PixelValue::px(12.0))]);
        let s = c.get_computed_css_style_string(&styled, &n0(), &normal());
        assert!(s.contains("width:"), "got {s:?}");
        assert!(s.contains("font-size:"), "got {s:?}");
        assert!(s.ends_with(';'), "each declaration is terminated: {s:?}");
    }
    #[test]
    fn computed_css_style_string_does_not_panic_on_an_out_of_range_node_id() {
        let c = CssPropertyCache::empty(0);
        let s = c.get_computed_css_style_string(
            &NodeData::create_div(),
            &NodeId::new(usize::MAX / 2),
            &normal(),
        );
        assert!(s.contains("display:"));
    }
    // =====================================================================
    // apply_ua_css / sort_cascaded_props / prune_compact_normal_props
    // =====================================================================
    #[test]
    fn apply_ua_css_inserts_ua_properties_into_cascaded_props() {
        let nodes = vec![NodeData::create_div()];
        let mut c = CssPropertyCache::empty(1);
        c.apply_ua_css(&nodes);
        let props = c.cascaded_props.build_get(0).expect("build phase");
        assert!(
            props
                .iter()
                .any(|p| p.prop_type == CssPropertyType::Display
                    && p.state == PseudoStateType::Normal),
            "UA `div {{ display: block }}` must land in the cascade"
        );
    }
    #[test]
    fn apply_ua_css_does_not_override_an_existing_inline_property() {
        let nodes = vec![div_with(vec![CssProperty::const_none(
            CssPropertyType::Display,
        )])];
        let mut c = CssPropertyCache::empty(1);
        c.apply_ua_css(&nodes);
        let props = c.cascaded_props.build_get(0).expect("build phase");
        assert!(
            !props.iter().any(|p| p.prop_type == CssPropertyType::Display),
            "UA CSS is the weakest layer and must not clobber inline"
        );
    }
    #[test]
    fn apply_ua_css_on_zero_nodes_returns_early() {
        let mut c = CssPropertyCache::empty(0);
        c.apply_ua_css(&[]);
        assert_eq!(c.cascaded_props.len(), 0);
    }
    #[test]
    fn sort_cascaded_props_flattens_and_orders_by_state_then_type() {
        let mut c = CssPropertyCache::empty(1);
        c.cascaded_props
            .push_to(0, stateful(PseudoStateType::Hover, width_px(1.0)));
        c.cascaded_props.push_to(
            0,
            stateful(
                PseudoStateType::Normal,
                CssProperty::const_none(CssPropertyType::Display),
            ),
        );
        c.cascaded_props
            .push_to(0, stateful(PseudoStateType::Normal, width_px(2.0)));
        c.sort_cascaded_props();
        assert!(c.cascaded_props.is_flattened());
        let slice = c.cascaded_props.get_slice(0);
        assert_eq!(slice.len(), 3);
        let keys: Vec<_> = slice.iter().map(|p| (p.state, p.prop_type)).collect();
        let mut sorted = keys.clone();
        sorted.sort_unstable();
        assert_eq!(keys, sorted, "binary_search lookups require sort order");
    }
    #[test]
    fn prune_compact_normal_props_keeps_what_the_slow_path_still_needs() {
        let mut c = CssPropertyCache::empty(1);
        // Normal + compact-encoded + fully representable => droppable
        c.cascaded_props.push_to(
            0,
            stateful(
                PseudoStateType::Normal,
                CssProperty::const_none(CssPropertyType::Display),
            ),
        );
        // Normal + compact-encoded but SENTINEL-encoded (%) => must survive
        c.cascaded_props
            .push_to(0, stateful(PseudoStateType::Normal, width_pct(50.0)));
        // Normal + no compact encoding at all => must survive
        c.cascaded_props.push_to(
            0,
            stateful(
                PseudoStateType::Normal,
                CssProperty::const_none(CssPropertyType::BackgroundContent),
            ),
        );
        // non-Normal => always survives
        c.cascaded_props.push_to(
            0,
            stateful(
                PseudoStateType::Hover,
                CssProperty::const_none(CssPropertyType::Display),
            ),
        );
        c.prune_compact_normal_props();
        let kept: Vec<(PseudoStateType, CssPropertyType)> = c
            .cascaded_props
            .get_slice(0)
            .iter()
            .map(|p| (p.state, p.prop_type))
            .collect();
        assert!(
            !kept.contains(&(PseudoStateType::Normal, CssPropertyType::Display)),
            "the compact cache is authoritative for this one"
        );
        assert!(kept.contains(&(PseudoStateType::Normal, CssPropertyType::Width)));
        assert!(kept.contains(&(PseudoStateType::Normal, CssPropertyType::BackgroundContent)));
        assert!(kept.contains(&(PseudoStateType::Hover, CssPropertyType::Display)));
        assert_eq!(kept.len(), 3);
    }
    #[test]
    fn prune_compact_normal_props_on_an_empty_cache_does_not_panic() {
        let mut c = CssPropertyCache::empty(0);
        c.prune_compact_normal_props();
        assert_eq!(c.cascaded_props.len(), 0);
        let mut c = CssPropertyCache::empty(3);
        c.prune_compact_normal_props();
        assert_eq!(c.cascaded_props.len(), 3);
        assert!(c.cascaded_props.get_slice(0).is_empty());
    }
    // =====================================================================
    // compute_inherited_values
    // =====================================================================
    /// `[root, child]`, child's parent = root (the hierarchy uses 1-based ids).
    fn two_node_hierarchy() -> Vec<NodeHierarchyItem> {
        vec![
            NodeHierarchyItem {
                parent: 0,
                previous_sibling: 0,
                next_sibling: 0,
                last_child: 2,
            },
            NodeHierarchyItem {
                parent: 1,
                previous_sibling: 0,
                next_sibling: 0,
                last_child: 0,
            },
        ]
    }
    #[test]
    fn compute_inherited_values_propagates_font_size_to_children() {
        let hierarchy = two_node_hierarchy();
        assert_eq!(hierarchy[1].parent_id(), Some(NodeId::new(0)));
        let nodes = vec![
            div_with(vec![font_size(PixelValue::px(20.0))]),
            NodeData::create_div(),
        ];
        let mut c = CssPropertyCache::empty(2);
        let changed = c.compute_inherited_values(&hierarchy, &nodes);
        assert_eq!(c.computed_values.len(), 2);
        assert_eq!(changed.len(), 2, "both nodes gained a computed value");
        let (t, v) = &c.computed_values[1][0];
        assert_eq!(*t, CssPropertyType::FontSize);
        assert_eq!(v.origin, CssPropertyOrigin::Inherited);
        assert!(close(font_size_parts(&v.property).unwrap().1, 20.0));
        // the parent's own value keeps the Own origin
        assert_eq!(c.computed_values[0][0].1.origin, CssPropertyOrigin::Own);
    }
    #[test]
    fn compute_inherited_values_resolves_a_child_em_against_the_parent_px() {
        let hierarchy = two_node_hierarchy();
        let nodes = vec![
            div_with(vec![font_size(PixelValue::px(20.0))]),
            div_with(vec![font_size(PixelValue::em(2.0))]),
        ];
        let mut c = CssPropertyCache::empty(2);
        c.compute_inherited_values(&hierarchy, &nodes);
        let (t, v) = &c.computed_values[1][0];
        assert_eq!(*t, CssPropertyType::FontSize);
        assert_eq!(v.origin, CssPropertyOrigin::Own);
        let (metric, n) = font_size_parts(&v.property).unwrap();
        assert_eq!(metric, SizeMetric::Px, "resolved to absolute px");
        assert!(close(n, 40.0), "2em of the parent's 20px, got {n}");
    }
    #[test]
    fn compute_inherited_values_is_idempotent_on_a_second_run() {
        let hierarchy = two_node_hierarchy();
        let nodes = vec![
            div_with(vec![font_size(PixelValue::px(20.0))]),
            NodeData::create_div(),
        ];
        let mut c = CssPropertyCache::empty(2);
        assert_eq!(c.compute_inherited_values(&hierarchy, &nodes).len(), 2);
        assert!(
            c.compute_inherited_values(&hierarchy, &nodes).is_empty(),
            "nothing changed the second time around"
        );
    }
    #[test]
    fn compute_inherited_values_on_an_empty_tree_does_not_panic() {
        let mut c = CssPropertyCache::empty(0);
        assert!(c.compute_inherited_values(&[], &[]).is_empty());
        assert!(c.computed_values.is_empty());
    }
    // =====================================================================
    // restyle / generate_tag_ids
    // =====================================================================
    fn one_node_scaffold() -> (NodeHierarchyItemVec, NodeDataContainer<CascadeInfo>) {
        (
            vec![NodeHierarchyItem::zeroed()].into(),
            NodeDataContainer::new(vec![CascadeInfo {
                index_in_parent: 0,
                is_last_child: true,
            }]),
        )
    }
    #[test]
    fn restyle_with_an_empty_stylesheet_flattens_and_yields_no_tags() {
        let (hierarchy, cascade) = one_node_scaffold();
        let nodes = NodeDataContainer::new(vec![NodeData::create_div()]);
        let non_leaf: ParentWithNodeDepthVec = Vec::new().into();
        let mut css = Css::empty();
        let mut c = CssPropertyCache::empty(1);
        let tags = c.restyle(
            &mut css,
            &nodes.as_ref(),
            &hierarchy,
            &non_leaf,
            &cascade.as_ref(),
        );
        assert!(tags.is_empty(), "a plain div needs no hit-test tag");
        assert!(
            c.css_props.is_flattened(),
            "restyle must leave css_props in read phase"
        );
        assert!(c.resolved_font_sizes_px.get().is_none());
    }
    #[test]
    fn generate_tag_ids_skips_inert_nodes_and_tags_interactive_ones() {
        let (hierarchy, _) = one_node_scaffold();
        let inert = NodeDataContainer::new(vec![NodeData::create_div()]);
        let c = CssPropertyCache::empty(1);
        assert!(c.generate_tag_ids(&inert.as_ref(), &hierarchy).is_empty());
        // an inline :hover rule makes the node hit-testable
        let hoverable = NodeDataContainer::new(vec![div_with_pseudo(
            vec![width_px(1.0)],
            PseudoStateType::Hover,
        )]);
        let tags = c.generate_tag_ids(&hoverable.as_ref(), &hierarchy);
        assert_eq!(tags.len(), 1);
        assert_eq!(
            tags[0].node_id.into_crate_internal(),
            Some(NodeId::new(0))
        );
    }
    #[test]
    fn generate_tag_ids_tags_a_node_with_a_cursor_declaration() {
        let (hierarchy, _) = one_node_scaffold();
        let nodes = NodeDataContainer::new(vec![div_with(vec![CssProperty::const_none(
            CssPropertyType::Cursor,
        )])]);
        let c = CssPropertyCache::empty(1);
        assert_eq!(c.generate_tag_ids(&nodes.as_ref(), &hierarchy).len(), 1);
    }
    #[test]
    fn generate_tag_ids_on_an_empty_dom_yields_nothing() {
        let nodes: NodeDataContainer<NodeData> = NodeDataContainer::new(Vec::new());
        let hierarchy: NodeHierarchyItemVec = Vec::new().into();
        let c = CssPropertyCache::empty(0);
        assert!(c.generate_tag_ids(&nodes.as_ref(), &hierarchy).is_empty());
    }
    // =====================================================================
    // std-gated profiling helpers
    // =====================================================================
    #[cfg(feature = "std")]
    #[test]
    fn css_prop_type_label_is_interned_and_distinct_per_variant() {
        let a = CssPropertyCache::css_prop_type_label(&CssPropertyType::Width);
        let b = CssPropertyCache::css_prop_type_label(&CssPropertyType::Width);
        assert!(!a.is_empty());
        assert_eq!(
            a.as_ptr(),
            b.as_ptr(),
            "the label table must leak at most one &'static str per variant"
        );
        let other = CssPropertyCache::css_prop_type_label(&CssPropertyType::Height);
        assert_ne!(a, other);
    }
    #[cfg(feature = "std")]
    #[test]
    fn drain_css_prop_counts_is_sorted_descending_and_drains() {
        // The counter is thread-local and only records when AZ_PROP_COUNT=1, so
        // the contract to pin here is "never panics, and drains".
        let first = drain_css_prop_counts();
        for w in first.windows(2) {
            assert!(w[0].1 >= w[1].1, "counts must be sorted descending");
        }
        assert!(
            drain_css_prop_counts().is_empty(),
            "a drained counter comes back empty"
        );
    }
}