1
// +spec:box-model:b3a79e - box assigned same styles as generating element; getters read from styled DOM per node
2
//! Centralized CSS property getters for the layout solver pipeline
3

            
4
use azul_core::{
5
    dom::{NodeId, NodeType},
6
    geom::LogicalSize,
7
    id::NodeId as CoreNodeId,
8
    styled_dom::{StyledDom, StyledNodeState},
9
};
10
use azul_css::{
11
    css::CssPropertyValue,
12
    props::{
13
        basic::{
14
            font::{StyleFontFamily, StyleFontFamilyVec, StyleFontStyle, StyleFontWeight},
15
            pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
16
            time::CssDuration,
17
            ColorU, PhysicalSize, PixelValue, PropertyContext, ResolutionContext,
18
        },
19
        layout::{
20
            grid::GridTemplateAreas, BoxDecorationBreak, BreakInside, LayoutAlignContent,
21
            LayoutAlignItems, LayoutBoxSizing, LayoutClear, LayoutDisplay, LayoutFlexDirection,
22
            LayoutFlexWrap, LayoutFloat, LayoutHeight, LayoutJustifyContent, LayoutOverflow,
23
            LayoutPosition, LayoutWidth, LayoutWritingMode, Orphans, PageBreak,
24
            StyleOverflowClipMargin, StyleScrollbarGutter, Widows,
25
        },
26
        property::{
27
            CssProperty, CssPropertyType, LayoutAlignContentValue, LayoutAlignItemsValue,
28
            LayoutAlignSelfValue, LayoutFlexBasisValue, LayoutFlexDirectionValue,
29
            LayoutFlexGrowValue, LayoutFlexShrinkValue, LayoutFlexWrapValue, LayoutGapValue,
30
            LayoutGridAutoColumnsValue, LayoutGridAutoFlowValue, LayoutGridAutoRowsValue,
31
            LayoutGridColumnValue, LayoutGridRowValue, LayoutGridTemplateColumnsValue,
32
            LayoutGridTemplateRowsValue, LayoutJustifyContentValue, LayoutJustifyItemsValue,
33
            LayoutJustifySelfValue,
34
        },
35
        style::{
36
            border_radius::StyleBorderRadius,
37
            lists::{StyleListStylePosition, StyleListStyleType},
38
            StyleAlignmentBaseline, StyleBaselineSource, StyleDirection, StyleDominantBaseline,
39
            StyleInitialLetterAlign, StyleLineFitEdge,
40
            StyleInitialLetterWrap, StyleTextAlign, StyleTextBoxEdge, StyleTextBoxTrim,
41
            StyleUnicodeBidi, StyleUserSelect, StyleVerticalAlign, StyleVisibility,
42
            StyleWhiteSpace,
43
        },
44
    },
45
};
46

            
47
use crate::{
48
    font_traits::{ParsedFontTrait, StyleProperties},
49
    solver3::{
50
        display_list::{BorderRadius, PhysicalSizeImport},
51
        layout_tree::LayoutNode,
52
        scrollbar::ScrollbarRequirements,
53
    },
54
};
55

            
56
const DEFAULT_EM_SIZE: f32 = 16.0;
57
const DEFAULT_CARET_WIDTH_PX: f32 = 2.0;
58
// ONE authority for the default blink period: the manager's constant.
59
// This and CURSOR_BLINK_INTERVAL_MS disagreeing (500 vs 530) is what made
60
// the "which default wins" question unanswerable — do not fork it again.
61
#[allow(clippy::cast_possible_truncation)]
62
const DEFAULT_CARET_BLINK_MS: u32 = crate::managers::text_edit::CURSOR_BLINK_INTERVAL_MS as u32;
63
const DEFAULT_TAB_SIZE: f32 = 8.0;
64
const SCROLLBAR_WIDTH_THIN: f32 = 8.0;
65
const SCROLLBAR_WIDTH_AUTO: f32 = 12.0;
66
const SCROLLBAR_HOVER_EXPAND_PX: f32 = 4.0;
67
const THUMB_HOVER_LIGHTEN: u8 = 30;
68
const THUMB_HOVER_ALPHA_ADD: u8 = 40;
69
const THUMB_ACTIVE_DARKEN: u8 = 15;
70

            
71
// Font-size resolution helper functions
72

            
73
/// Helper function to get element's computed font-size.
74
///
75
/// **Memoised** for the common `Normal` pseudo-state: the first
76
/// call on a given `StyledDom` populates
77
/// `css_property_cache.ptr.resolved_font_sizes_px` via a single
78
/// bottom-up DOM walk (N cascade walks total, stored as
79
/// `Vec<f32>`); every subsequent call is a single Vec index.
80
/// Non-normal state falls through to [`resolve_font_size_slow`].
81
///
82
/// Motivation: `AZ_PROP_COUNT=1` measured 329 629 `font-size`
83
/// cascade walks per cold layout on excel.html (~730 per node).
84
/// With this cache that collapses to ~500 total (one per node,
85
/// once), and subsequent layouts hit the Vec directly.
86
///
87
/// The semantics of the slow path are preserved exactly: the
88
/// `compute_all_font_sizes_px` walker mirrors the original's
89
/// `computed_values` → cascade → `DEFAULT_FONT_SIZE` ordering,
90
/// so rendered pixels are byte-identical.
91
2770818
#[must_use] pub fn get_element_font_size(
92
2770818
    styled_dom: &StyledDom,
93
2770818
    dom_id: NodeId,
94
2770818
    node_state: &StyledNodeState,
95
2770818
) -> f32 {
96
    // M12.7 FIX: the OnceLock-cached fast path
97
    // (`is_normal → resolved_font_sizes_px.get_or_init(|| compute_all_font_sizes_px) →
98
    // sizes.get`) MIS-LIFTS to wasm — it diverges (create_node_from_dom never returns →
99
    // empty LayoutTree → 0 rects). PROVEN by isolation: skipping it lets
100
    // get_element_font_size reach + return via resolve_font_size_slow, and
101
    // create_resolution_context completes (sub-step 1→4). resolve_font_size_slow is the
102
    // same resolution unmemoized (correct), so we always use it. (Native desktop is
103
    // unaffected in correctness; it loses the per-DOM memoization — a minor perf cost
104
    // only on the lifted web path's small DOMs. The cache-block lift bug — likely the
105
    // compute_all_font_sizes_px closure's control/FP — is documented for a later remill
106
    // fix that can restore the fast path.)
107
2770818
    let _ = compute_all_font_sizes_px; // referenced so other callers / native keep it
108
2770818
    resolve_font_size_slow(styled_dom, dom_id, node_state)
109
2770818
}
110

            
111
/// Bottom-up single-pass resolve of every node's font-size.
112
/// Parents are computed before children (DFS pre-order invariant
113
/// on `NodeId::index()`), so `em` inherits via the parent's
114
/// already-stored pixel value. `rem` reads from `sizes[0]` once
115
/// the root is populated (the root's own size resolves via the
116
/// `computed_values` short-circuit if set, otherwise DEFAULT).
117
///
118
/// Preserves the original resolution order exactly:
119
///
120
/// 1. `computed_values` binary search → if `FontSize` is pre-
121
///    resolved to a px value, use that.
122
/// 2. Full cascade via `cache.get_font_size(...)`; if an explicit
123
///    value is present, resolve with context.
124
/// 3. `DEFAULT_FONT_SIZE` fallback — NOT `parent_font_size`,
125
///    because the `computed_values` short-circuit at step 1 is
126
///    the cascade's inheritance channel (pre-populated for every
127
///    inheriting node).
128
fn compute_all_font_sizes_px(styled_dom: &StyledDom) -> Vec<f32> {
129
    use azul_css::props::{
130
        basic::length::SizeMetric,
131
        property::{CssProperty, CssPropertyType},
132
    };
133

            
134
    let n = styled_dom.node_data.len();
135
    let mut sizes = alloc::vec![DEFAULT_FONT_SIZE; n];
136
    if n == 0 {
137
        return sizes;
138
    }
139

            
140
    let data_container = styled_dom.node_data.as_container();
141
    let state_container = styled_dom.styled_nodes.as_container();
142
    let hierarchy = styled_dom.node_hierarchy.as_container();
143
    let cache = &styled_dom.css_property_cache.ptr;
144

            
145
    for idx in 0..n {
146
        let dom_id = NodeId::new(idx);
147

            
148
        // Step 1: computed_values short-circuit (matches original).
149
        if let Some(vec) = cache.computed_values.get(idx) {
150
            if let Ok(cv_idx) = vec.binary_search_by_key(&CssPropertyType::FontSize, |(k, _)| *k) {
151
                if let CssProperty::FontSize(css_val) = &vec[cv_idx].1.property {
152
                    if let Some(fs) = css_val.get_property() {
153
                        if fs.inner.metric == SizeMetric::Px {
154
                            sizes[idx] = fs.inner.number.get();
155
                            continue;
156
                        }
157
                    }
158
                }
159
            }
160
        }
161

            
162
        // Step 2: full cascade walk.
163
        let parent_font_size = hierarchy
164
            .get(dom_id)
165
            .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id)
166
            .map_or(DEFAULT_FONT_SIZE, |p| sizes[p.index()]);
167
        let root_font_size = sizes[0];
168

            
169
        let Some(node_data) = data_container.internal.get(idx) else {
170
            sizes[idx] = DEFAULT_FONT_SIZE;
171
            continue;
172
        };
173
        let Some(styled) = state_container.internal.get(idx) else {
174
            sizes[idx] = DEFAULT_FONT_SIZE;
175
            continue;
176
        };
177
        let node_state = &styled.styled_node_state;
178

            
179
        // Step 2.5: compact cache fast path — avoids a full cascade walk
180
        // per node. The build-time pass has already resolved em/% to px,
181
        // so the raw u32 here is the final pixel value when set.
182
        let mut fast_fs: Option<f32> = None;
183
        let mut compact_said_inherit = false;
184
        if node_state.is_normal() {
185
            if let Some(ref cc) = cache.compact_cache {
186
                let raw = cc.get_font_size_raw(idx);
187
                if raw == azul_css::compact_cache::U32_SENTINEL
188
                    || raw == azul_css::compact_cache::U32_INHERIT
189
                    || raw == azul_css::compact_cache::U32_INITIAL
190
                {
191
                    compact_said_inherit = true;
192
                } else if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
193
                    // Already-resolved pixel value (em/% eliminated during build).
194
                    if pv.metric == SizeMetric::Px {
195
                        fast_fs = Some(pv.number.get());
196
                    } else {
197
                        // Shouldn't normally happen post-resolve, but fall through safely.
198
                        let context = ResolutionContext {
199
                            vertical_writing_mode: false,
200
                            element_font_size: DEFAULT_FONT_SIZE,
201
                            parent_font_size,
202
                            root_font_size,
203
                            containing_block_size: PhysicalSize::new(0.0, 0.0),
204
                            element_size: None,
205
                            viewport_size: PhysicalSize::new(0.0, 0.0),
206
                        };
207
                        fast_fs =
208
                            Some(pv.resolve_with_context(&context, PropertyContext::FontSize));
209
                    }
210
                }
211
            }
212
        }
213
        if let Some(fs) = fast_fs {
214
            sizes[idx] = fs;
215
            continue;
216
        }
217
        if compact_said_inherit {
218
            sizes[idx] = parent_font_size;
219
            continue;
220
        }
221

            
222
        let resolved = cache
223
            .get_font_size(node_data, &dom_id, node_state)
224
            .and_then(|v| v.get_property().copied())
225
            .map(|v| {
226
                let context = ResolutionContext {
227
                    vertical_writing_mode: false,
228
                    element_font_size: DEFAULT_FONT_SIZE,
229
                    parent_font_size,
230
                    root_font_size,
231
                    containing_block_size: PhysicalSize::new(0.0, 0.0),
232
                    element_size: None,
233
                    viewport_size: PhysicalSize::new(0.0, 0.0),
234
                };
235
                v.inner
236
                    .resolve_with_context(&context, PropertyContext::FontSize)
237
            });
238

            
239
        // Step 3: fallback to DEFAULT (matches original .unwrap_or).
240
        sizes[idx] = resolved.unwrap_or(DEFAULT_FONT_SIZE);
241
    }
242
    sizes
243
}
244

            
245
/// Un-memoised recursive resolution, used as the fallback for
246
/// non-normal pseudo-states in [`get_element_font_size`] and
247
/// directly by tests that bypass the StyledDom-scoped cache.
248
/// Keeps the original semantics verbatim.
249
2770834
fn resolve_font_size_slow(
250
2770834
    styled_dom: &StyledDom,
251
2770834
    dom_id: NodeId,
252
2770834
    node_state: &StyledNodeState,
253
2770834
) -> f32 {
254
    // ITERATIVE resolution (was unbounded self-recursion up the parent chain, which
255
    // stack-overflowed on deeply nested DOMs and was O(N*depth)). We walk `parent_id`
256
    // in a loop to collect the ancestor chain, then resolve top-down so each node's
257
    // `em` inherits from its already-resolved parent. Result is identical to the old
258
    // recursive version for a well-formed tree, but bounded by the tree depth in
259
    // stack usage (a single Vec of ancestors instead of nested frames).
260
    //
261
    // Each ancestor is resolved against its OWN `styled_node_state` (previously the
262
    // recursion incorrectly threaded the *child's* state into parent/root resolution),
263
    // matching the sibling `get_parent_font_size` / `get_root_font_size` helpers.
264
2770834
    let hierarchy = styled_dom.node_hierarchy.as_container();
265
2770834
    let states = styled_dom.styled_nodes.as_container();
266
2770834
    let root_id = NodeId::new(0);
267

            
268
    // Root font-size, resolved from NodeId(0) with no parent and root == DEFAULT
269
    // (mirrors the original: for node 0 the root branch returned DEFAULT directly).
270
2770834
    let root_font_size = if dom_id == root_id {
271
1014840
        DEFAULT_FONT_SIZE
272
    } else {
273
1755994
        let root_state = &states[root_id].styled_node_state;
274
1755994
        resolve_font_size_one(
275
1755994
            styled_dom,
276
1755994
            root_id,
277
1755994
            root_state,
278
            DEFAULT_FONT_SIZE,
279
            DEFAULT_FONT_SIZE,
280
        )
281
    };
282

            
283
    // Collect the ancestor chain: chain[0] == dom_id, chain.last() == topmost ancestor.
284
2770834
    let mut chain = Vec::new();
285
2770834
    let mut cur = Some(dom_id);
286
13877147
    while let Some(id) = cur {
287
11106313
        chain.push(id);
288
11106313
        cur = hierarchy
289
11106313
            .get(id)
290
11106313
            .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id);
291
11106313
    }
292

            
293
    // Resolve top-down. The topmost ancestor has parent_font_size == DEFAULT; each
294
    // subsequent node inherits the previously-resolved value as its parent size.
295
2770834
    let mut parent_font_size = DEFAULT_FONT_SIZE;
296
2770834
    let mut resolved = DEFAULT_FONT_SIZE;
297
11106313
    for &id in chain.iter().rev() {
298
        // The target node keeps the caller-provided state (its own state, per the
299
        // public contract); ancestors use their own stored state.
300
11106313
        let this_state = if id == dom_id {
301
2770834
            node_state
302
        } else {
303
8335479
            &states[id].styled_node_state
304
        };
305
11106313
        let this_root_fs = if id == root_id {
306
2770834
            DEFAULT_FONT_SIZE
307
        } else {
308
8335479
            root_font_size
309
        };
310
11106313
        resolved =
311
11106313
            resolve_font_size_one(styled_dom, id, this_state, parent_font_size, this_root_fs);
312
11106313
        parent_font_size = resolved;
313
    }
314
2770834
    resolved
315
2770834
}
316

            
317
/// Resolves a single node's font-size given its already-resolved `parent_font_size`
318
/// and `root_font_size`. Contains the per-node logic that the old recursive
319
/// `resolve_font_size_slow` applied at each frame (computed-values px short-circuit,
320
/// then a full cascade walk), with no recursion of its own.
321
12862312
fn resolve_font_size_one(
322
12862312
    styled_dom: &StyledDom,
323
12862312
    dom_id: NodeId,
324
12862312
    node_state: &StyledNodeState,
325
12862312
    parent_font_size: f32,
326
12862312
    root_font_size: f32,
327
12862312
) -> f32 {
328
12862312
    let node_data = &styled_dom.node_data.as_container()[dom_id];
329
12862312
    let cache = &styled_dom.css_property_cache.ptr;
330

            
331
12862312
    if let Some(vec) = cache.computed_values.get(dom_id.index()) {
332
12862279
        if let Ok(idx) = vec.binary_search_by_key(&CssPropertyType::FontSize, |(k, _)| *k) {
333
8050833
            if let CssProperty::FontSize(css_val) = &vec[idx].1.property {
334
8050833
                if let Some(fs) = css_val.get_property() {
335
8050833
                    if fs.inner.metric == azul_css::props::basic::length::SizeMetric::Px {
336
8050833
                        return fs.inner.number.get();
337
                    }
338
                }
339
            }
340
4811446
        }
341
33
    }
342

            
343
4811479
    cache
344
4811479
        .get_font_size(node_data, &dom_id, node_state)
345
4811479
        .and_then(|v| v.get_property().copied())
346
4811479
        .map_or(DEFAULT_FONT_SIZE, |v| {
347
            let context = ResolutionContext {
348
                vertical_writing_mode: false,
349
                element_font_size: DEFAULT_FONT_SIZE,
350
                parent_font_size,
351
                root_font_size,
352
                containing_block_size: PhysicalSize::new(0.0, 0.0),
353
                element_size: None,
354
                viewport_size: PhysicalSize::new(0.0, 0.0),
355
            };
356
            v.inner
357
                .resolve_with_context(&context, PropertyContext::FontSize)
358
        })
359
12862312
}
360

            
361
/// Helper function to get parent's computed font-size.
362
///
363
/// Retrieves the parent's own `StyledNodeState` so that pseudo-class-specific
364
/// font-size rules (e.g. `div:hover { font-size: 32px }`) are resolved
365
/// against the parent's actual state, not the child's.
366
300834
#[must_use] pub fn get_parent_font_size(
367
300834
    styled_dom: &StyledDom,
368
300834
    dom_id: NodeId,
369
300834
    _node_state: &StyledNodeState, // child's state — intentionally unused
370
300834
) -> f32 {
371
300834
    styled_dom
372
300834
        .node_hierarchy
373
300834
        .as_container()
374
300834
        .get(dom_id)
375
300834
        .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id)
376
300834
        .map_or(DEFAULT_FONT_SIZE, |parent_id| {
377
299994
            let parent_state = &styled_dom.styled_nodes.as_container()[parent_id].styled_node_state;
378
299994
            get_element_font_size(styled_dom, parent_id, parent_state)
379
299994
        })
380
300834
}
381

            
382
/// Helper function to get root element's font-size.
383
///
384
/// Uses the root element's own `StyledNodeState` so that pseudo-class-specific
385
/// rules are resolved correctly regardless of which node triggered the call.
386
575971
#[must_use] pub fn get_root_font_size(styled_dom: &StyledDom, _node_state: &StyledNodeState) -> f32 {
387
575971
    let root_id = NodeId::new(0);
388
575971
    let root_state = &styled_dom.styled_nodes.as_container()[root_id].styled_node_state;
389
575971
    get_element_font_size(styled_dom, root_id, root_state)
390
575971
}
391

            
392
/// A value that can be Auto, Initial, Inherit, or an explicit value.
393
/// This preserves CSS cascade semantics better than Option<T>.
394
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
395
#[derive(Default)]
396
pub enum MultiValue<T> {
397
    /// CSS 'auto' keyword
398
    #[default]
399
    Auto,
400
    /// CSS 'initial' keyword - use initial value
401
    Initial,
402
    /// CSS 'inherit' keyword - inherit from parent
403
    Inherit,
404
    /// Explicit value (e.g., "10px", "50%")
405
    Exact(T),
406
}
407

            
408
impl<T> MultiValue<T> {
409
    /// Returns true if this is an Auto value
410
90161
    pub const fn is_auto(&self) -> bool {
411
90161
        matches!(self, Self::Auto)
412
90161
    }
413

            
414
    /// Returns true if this is an explicit value
415
6
    pub const fn is_exact(&self) -> bool {
416
6
        matches!(self, Self::Exact(_))
417
6
    }
418

            
419
    /// Gets the exact value if present
420
1694020
    pub fn exact(self) -> Option<T> {
421
1694020
        match self {
422
1091714
            Self::Exact(v) => Some(v),
423
602306
            _ => None,
424
        }
425
1694020
    }
426

            
427
    /// Gets the exact value or returns the provided default
428
3945373
    pub fn unwrap_or(self, default: T) -> T {
429
3945373
        match self {
430
3945323
            Self::Exact(v) => v,
431
50
            _ => default,
432
        }
433
3945373
    }
434

            
435
    /// Gets the exact value or returns `T::default()`
436
6649299
    pub fn unwrap_or_default(self) -> T
437
6649299
    where
438
6649299
        T: Default,
439
    {
440
6649299
        match self {
441
6174607
            Self::Exact(v) => v,
442
474692
            _ => T::default(),
443
        }
444
6649299
    }
445

            
446
    /// Maps the inner value if Exact, otherwise returns self unchanged
447
9
    pub fn map<U, F>(self, f: F) -> MultiValue<U>
448
9
    where
449
9
        F: FnOnce(T) -> U,
450
    {
451
9
        match self {
452
3
            Self::Exact(v) => MultiValue::Exact(f(v)),
453
2
            Self::Auto => MultiValue::Auto,
454
2
            Self::Initial => MultiValue::Initial,
455
2
            Self::Inherit => MultiValue::Inherit,
456
        }
457
9
    }
458
}
459

            
460
// Implement helper methods for LayoutOverflow specifically
461
impl MultiValue<LayoutOverflow> {
462
    /// Returns true if this overflow value causes content to be clipped.
463
    /// This includes Hidden, Clip, Auto, and Scroll (all values except Visible).
464
1375192
    #[must_use] pub const fn is_clipped(&self) -> bool {
465
1351445
        matches!(
466
1374467
            self,
467
23747
            Self::Exact(
468
23747
                LayoutOverflow::Hidden
469
23747
                    | LayoutOverflow::Clip
470
23747
                    | LayoutOverflow::Auto
471
23747
                    | LayoutOverflow::Scroll
472
23747
            )
473
        )
474
1375192
    }
475

            
476
667531
    #[must_use] pub const fn is_scroll(&self) -> bool {
477
664441
        matches!(
478
667514
            self,
479
3090
            Self::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
480
        )
481
667531
    }
482

            
483
    // +spec:overflow:3be57c - overflow:hidden disables user scrolling but programmatic scrolling still works
484
    /// Does this value establish a SCROLL CONTAINER (css-overflow-3 §3.1)?
485
    /// `hidden | scroll | auto` — an `overflow: hidden` box is
486
    /// programmatically scrollable even though its user scrolling is
487
    /// disabled. The unset sentinel (initial = visible) does not.
488
427539
    #[must_use] pub const fn is_scroll_container(&self) -> bool {
489
424100
        matches!(
490
427508
            self,
491
3439
            Self::Exact(
492
3439
                LayoutOverflow::Hidden | LayoutOverflow::Scroll | LayoutOverflow::Auto
493
3439
            )
494
        )
495
427539
    }
496

            
497
    /// Does this value allow scrolling DIRECTLY TRIGGERED BY THE USER
498
    /// (wheel, scrollbar, keyboard)? `hidden` does not.
499
563
    #[must_use] pub const fn allows_user_scrolling(&self) -> bool {
500
298
        matches!(
501
552
            self,
502
265
            Self::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
503
        )
504
563
    }
505

            
506
5606
    #[must_use] pub const fn is_auto_overflow(&self) -> bool {
507
5605
        matches!(self, Self::Exact(LayoutOverflow::Auto))
508
5606
    }
509

            
510
8
    #[must_use] pub const fn is_hidden(&self) -> bool {
511
7
        matches!(self, Self::Exact(LayoutOverflow::Hidden))
512
8
    }
513

            
514
9
    #[must_use] pub const fn is_hidden_or_clip(&self) -> bool {
515
6
        matches!(
516
6
            self,
517
3
            Self::Exact(LayoutOverflow::Hidden | LayoutOverflow::Clip)
518
        )
519
9
    }
520

            
521
13
    #[must_use] pub const fn is_scroll_explicit(&self) -> bool {
522
11
        matches!(self, Self::Exact(LayoutOverflow::Scroll))
523
13
    }
524

            
525
6749
    #[must_use] pub const fn is_clip(&self) -> bool {
526
6747
        matches!(self, Self::Exact(LayoutOverflow::Clip))
527
6749
    }
528

            
529
8
    #[must_use] pub const fn is_visible_or_clip(&self) -> bool {
530
6
        matches!(
531
5
            self,
532
2
            Self::Exact(LayoutOverflow::Visible | LayoutOverflow::Clip)
533
        )
534
8
    }
535

            
536
    /// True iff `overflow` is EXPLICITLY set to a value that establishes a block
537
    /// formatting context (CSS 2.2 §9.4.1: `hidden`/`scroll`/`auto`). `visible`,
538
    /// `clip`, and the unset/initial/inherit sentinel do NOT — the initial value
539
    /// is `visible`, so an unset overflow must not establish a BFC. Using
540
    /// `!is_visible_or_clip()` for this was wrong: the "not set" `MultiValue::Auto`
541
    /// sentinel is not visible/clip, so every plain block spuriously got a BFC on
542
    /// the slow cascade path (the fast path returns `Exact(Visible)` and did not).
543
27892
    #[must_use] pub const fn establishes_bfc(&self) -> bool {
544
27226
        matches!(
545
27887
            self,
546
666
            Self::Exact(LayoutOverflow::Hidden | LayoutOverflow::Scroll | LayoutOverflow::Auto)
547
        )
548
27892
    }
549

            
550
    // +spec:overflow:833078 - visible/clip compute to auto/hidden if other axis is scrollable
551
    /// Resolves the computed value per CSS Overflow 3 § 3.1:
552
    /// visible/clip values compute to auto/hidden (respectively)
553
    /// if the other axis is neither visible nor clip.
554
    ///
555
    /// The UNSET sentinel means the initial value `visible`, and the rule
556
    /// applies to it the same way: an unset axis computes to `auto` when the
557
    /// other axis is scrollable. The old catch-all left it unresolved, so on
558
    /// the slow path (pseudo-states / no compact cache) callers treated the
559
    /// axis as visible instead of a scroll container - the compact fast path
560
    /// (which always materializes `Exact`) disagreed.
561
1332363
    #[must_use] pub const fn resolve_computed(
562
1332363
        &self,
563
1332363
        other_axis: &Self,
564
1332363
    ) -> Self {
565
1332363
        let this = match self {
566
1332325
            Self::Exact(v) => *v,
567
38
            _ => LayoutOverflow::Visible,
568
        };
569
1332363
        let other = match other_axis {
570
1332325
            Self::Exact(v) => *v,
571
38
            _ => LayoutOverflow::Visible,
572
        };
573
1332363
        let resolved = this.resolve_computed(other);
574
        // Keep the sentinel when nothing changed (an unset axis stays unset
575
        // unless the rule upgrades it), so downstream unset-vs-explicit
576
        // distinctions (BFC establishment) are preserved.
577
38
        match self {
578
1332325
            Self::Exact(_) => Self::Exact(resolved),
579
38
            _ if !matches!(resolved, LayoutOverflow::Visible) => Self::Exact(resolved),
580
27
            _ => *self,
581
        }
582
1332363
    }
583
}
584

            
585
// Implement helper methods for LayoutPosition
586
impl MultiValue<LayoutPosition> {
587
13567
    #[must_use] pub const fn is_absolute_or_fixed(&self) -> bool {
588
12257
        matches!(
589
13563
            self,
590
1310
            Self::Exact(LayoutPosition::Absolute | LayoutPosition::Fixed)
591
        )
592
13567
    }
593
}
594

            
595
// Implement helper methods for LayoutFloat
596
impl MultiValue<LayoutFloat> {
597
12258
    #[must_use] pub const fn is_none(&self) -> bool {
598
229
        matches!(
599
12253
            self,
600
3
            Self::Auto
601
1
                | Self::Initial
602
1
                | Self::Inherit
603
12024
                | Self::Exact(LayoutFloat::None)
604
        )
605
12258
    }
606
}
607

            
608

            
609
/// Helper macro to reduce boilerplate for simple CSS property getters
610
/// Returns the inner `PixelValue` wrapped in `MultiValue`
611
macro_rules! get_css_property_pixel {
612
    // Variant WITH compact cache fast path for i16-encoded resolved px properties
613
    ($fn_name:ident, $cache_method:ident, $ua_property:expr, compact_i16 = $compact_method:ident) => {
614
4211484
        #[must_use] pub fn $fn_name(
615
4211484
            styled_dom: &StyledDom,
616
4211484
            node_id: NodeId,
617
4211484
            node_state: &StyledNodeState,
618
4211484
        ) -> MultiValue<PixelValue> {
619
            // FAST PATH: compact cache for normal state (O(1) array lookup)
620
4211484
            if node_state.is_normal() {
621
4211436
                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
622
4211424
                    let raw = cc.$compact_method(node_id.index());
623
4211424
                    if raw == azul_css::compact_cache::I16_AUTO {
624
462734
                        return MultiValue::Auto;
625
3748690
                    }
626
3748690
                    if raw == azul_css::compact_cache::I16_INITIAL {
627
                        return MultiValue::Initial;
628
3748690
                    }
629
3748690
                    if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
630
                        // Valid value: decode i16 ×10 → px
631
3633015
                        return MultiValue::Exact(PixelValue::px(f32::from(raw) / 10.0));
632
115675
                    }
633
                    // I16_SENTINEL or I16_INHERIT → fall through to slow path
634
12
                }
635
48
            }
636

            
637
115735
            let node_data = &styled_dom.node_data.as_container()[node_id];
638

            
639
115735
            let author_css = styled_dom
640
115735
                .css_property_cache
641
115735
                .ptr
642
115735
                .$cache_method(node_data, &node_id, node_state);
643

            
644
115735
            if let Some(ref val) = author_css {
645
115679
                if val.is_auto() {
646
                    return MultiValue::Auto;
647
115679
                }
648
115679
                if let Some(exact) = val.get_property().copied() {
649
115679
                    return MultiValue::Exact(exact.inner);
650
                }
651
56
            }
652

            
653
56
            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
654

            
655
56
            if let Some(ua_prop) = ua_css {
656
                if let Some(inner) = ua_prop.get_pixel_inner() {
657
                    return MultiValue::Exact(inner);
658
                }
659
56
            }
660

            
661
56
            MultiValue::Initial
662
4211484
        }
663
    };
664
}
665

            
666
/// Helper trait to extract `PixelValue` from any `CssProperty` variant
667
trait CssPropertyPixelInner {
668
    fn get_pixel_inner(&self) -> Option<PixelValue>;
669
}
670

            
671
impl CssPropertyPixelInner for CssProperty {
672
    fn get_pixel_inner(&self) -> Option<PixelValue> {
673
        match self {
674
            Self::Left(CssPropertyValue::Exact(v)) => Some(v.inner),
675
            Self::Right(CssPropertyValue::Exact(v)) => Some(v.inner),
676
            Self::Top(CssPropertyValue::Exact(v)) => Some(v.inner),
677
            Self::Bottom(CssPropertyValue::Exact(v)) => Some(v.inner),
678
            Self::MarginLeft(CssPropertyValue::Exact(v)) => Some(v.inner),
679
            Self::MarginRight(CssPropertyValue::Exact(v)) => Some(v.inner),
680
            Self::MarginTop(CssPropertyValue::Exact(v)) => Some(v.inner),
681
            Self::MarginBottom(CssPropertyValue::Exact(v)) => Some(v.inner),
682
            Self::PaddingLeft(CssPropertyValue::Exact(v)) => Some(v.inner),
683
            Self::PaddingRight(CssPropertyValue::Exact(v)) => Some(v.inner),
684
            Self::PaddingTop(CssPropertyValue::Exact(v)) => Some(v.inner),
685
            Self::PaddingBottom(CssPropertyValue::Exact(v)) => Some(v.inner),
686
            _ => None,
687
        }
688
    }
689
}
690

            
691
/// Generic macro for CSS properties with UA CSS fallback - returns `MultiValue`<T>
692
macro_rules! get_css_property {
693
    // Variant WITH compact cache fast path (for enum properties in Tier 1)
694
    ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr, compact = $compact_method:ident) => {
695
19464775
        #[must_use] pub fn $fn_name(
696
19464775
            styled_dom: &StyledDom,
697
19464775
            node_id: NodeId,
698
19464775
            node_state: &StyledNodeState,
699
19464775
        ) -> MultiValue<$return_type> {
700
            // FAST PATH: compact cache for normal state (O(1) array + bitshift)
701
            // NOTE (M12.7): skipping this fast path does NOT fix get_display_type's
702
            // divergence — the slow path / the `match get_display_type(...)` on the
703
            // LayoutDisplay enum (a niche-discriminant) mis-lifts too. So this isn't the
704
            // cache (unlike the font-size fix); it's the deeper niche/enum decode. Kept.
705
19464775
            if node_state.is_normal() {
706
19462650
                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
707
19462586
                    return MultiValue::Exact(cc.$compact_method(node_id.index()));
708
64
                }
709
2125
            }
710

            
711
            // SLOW PATH: full cascade resolution
712
2189
            let node_data = &styled_dom.node_data.as_container()[node_id];
713

            
714
            // 1. Check author CSS first
715
2189
            let author_css = styled_dom
716
2189
                .css_property_cache
717
2189
                .ptr
718
2189
                .$cache_method(node_data, &node_id, node_state);
719

            
720
2189
            if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
721
276
                return MultiValue::Exact(val);
722
1913
            }
723

            
724
            // 2. Check User Agent CSS
725
1913
            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
726

            
727
1913
            if let Some(ua_prop) = ua_css {
728
                if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
729
                    return MultiValue::Exact(val);
730
                }
731
1913
            }
732

            
733
            // 3. Fallback to Auto (not set)
734
1913
            MultiValue::Auto
735
19464775
        }
736
    };
737
    // Variant WITH compact cache for u32-encoded dimension enums (LayoutWidth/LayoutHeight)
738
    // These types have Auto, Px(PixelValue), MinContent, MaxContent, Calc variants
739
    ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr, compact_u32_dim = $compact_raw_method:ident, $px_variant:path, $auto_variant:path, $min_content_variant:path, $max_content_variant:path) => {
740
1699754
        #[must_use] pub fn $fn_name(
741
1699754
            styled_dom: &StyledDom,
742
1699754
            node_id: NodeId,
743
1699754
            node_state: &StyledNodeState,
744
1699754
        ) -> MultiValue<$return_type> {
745
            // FAST PATH: compact cache for normal state
746
1699754
            if node_state.is_normal() {
747
1699717
                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
748
1699712
                    let raw = cc.$compact_raw_method(node_id.index());
749
1699712
                    match raw {
750
1499550
                        azul_css::compact_cache::U32_AUTO => return MultiValue::Auto,
751
                        azul_css::compact_cache::U32_INITIAL => return MultiValue::Initial,
752
                        azul_css::compact_cache::U32_NONE => return MultiValue::Auto,
753
                        azul_css::compact_cache::U32_MIN_CONTENT => return MultiValue::Exact($min_content_variant),
754
                        azul_css::compact_cache::U32_MAX_CONTENT => return MultiValue::Exact($max_content_variant),
755
                        azul_css::compact_cache::U32_SENTINEL | azul_css::compact_cache::U32_INHERIT => {
756
                            // fall through to slow path
757
                        }
758
                        _ => {
759
                            // Valid encoded pixel value
760
200162
                            if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
761
200162
                                return MultiValue::Exact($px_variant(pv));
762
                            }
763
                            // decode failed → slow path
764
                        }
765
                    }
766
5
                }
767
37
            }
768

            
769
            // SLOW PATH: full cascade resolution
770
42
            let node_data = &styled_dom.node_data.as_container()[node_id];
771

            
772
42
            let author_css = styled_dom
773
42
                .css_property_cache
774
42
                .ptr
775
42
                .$cache_method(node_data, &node_id, node_state);
776

            
777
42
            if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
778
27
                return MultiValue::Exact(val);
779
15
            }
780

            
781
15
            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
782

            
783
15
            if let Some(ua_prop) = ua_css {
784
                if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
785
                    return MultiValue::Exact(val);
786
                }
787
15
            }
788

            
789
15
            MultiValue::Auto
790
1699754
        }
791
    };
792
    // Variant WITH compact cache for u32-encoded dimension structs (LayoutMinWidth etc.)
793
    // These types are struct { inner: PixelValue }
794
    ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr, compact_u32_struct = $compact_raw_method:ident) => {
795
1683034
        #[must_use] pub fn $fn_name(
796
1683034
            styled_dom: &StyledDom,
797
1683034
            node_id: NodeId,
798
1683034
            node_state: &StyledNodeState,
799
1683034
        ) -> MultiValue<$return_type> {
800
            // FAST PATH: compact cache for normal state
801
1683034
            if node_state.is_normal() {
802
1683000
                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
803
1682992
                    let raw = cc.$compact_raw_method(node_id.index());
804
1682992
                    match raw {
805
1668931
                        azul_css::compact_cache::U32_AUTO | azul_css::compact_cache::U32_NONE => return MultiValue::Auto,
806
                        azul_css::compact_cache::U32_INITIAL => return MultiValue::Initial,
807
                        azul_css::compact_cache::U32_SENTINEL | azul_css::compact_cache::U32_INHERIT => {
808
                            // fall through to slow path
809
                        }
810
                        _ => {
811
14061
                            if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
812
14061
                                return MultiValue::Exact(
813
14061
                                    <$return_type as azul_css::props::PixelValueTaker>::from_pixel_value(pv)
814
14061
                                );
815
                            }
816
                        }
817
                    }
818
8
                }
819
34
            }
820

            
821
            // SLOW PATH
822
42
            let node_data = &styled_dom.node_data.as_container()[node_id];
823

            
824
42
            let author_css = styled_dom
825
42
                .css_property_cache
826
42
                .ptr
827
42
                .$cache_method(node_data, &node_id, node_state);
828

            
829
42
            if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
830
                return MultiValue::Exact(val);
831
42
            }
832

            
833
42
            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
834

            
835
42
            if let Some(ua_prop) = ua_css {
836
                if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
837
                    return MultiValue::Exact(val);
838
                }
839
42
            }
840

            
841
42
            MultiValue::Auto
842
1683034
        }
843
    };
844
    // Variant WITHOUT compact cache (original behavior)
845
    ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr) => {
846
154820
        #[must_use] pub fn $fn_name(
847
154820
            styled_dom: &StyledDom,
848
154820
            node_id: NodeId,
849
154820
            node_state: &StyledNodeState,
850
154820
        ) -> MultiValue<$return_type> {
851
154820
            let node_data = &styled_dom.node_data.as_container()[node_id];
852

            
853
            // 1. Check author CSS first
854
154820
            let author_css = styled_dom
855
154820
                .css_property_cache
856
154820
                .ptr
857
154820
                .$cache_method(node_data, &node_id, node_state);
858

            
859
154820
            if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
860
165
                return MultiValue::Exact(val);
861
154655
            }
862

            
863
            // 2. Check User Agent CSS
864
154655
            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
865

            
866
154655
            if let Some(ua_prop) = ua_css {
867
                if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
868
                    return MultiValue::Exact(val);
869
                }
870
154655
            }
871

            
872
            // 3. Fallback to Auto (not set)
873
154655
            MultiValue::Auto
874
154820
        }
875
    };
876
}
877

            
878
/// Helper trait to extract typed values from UA CSS properties
879
trait ExtractPropertyValue<T> {
880
    fn extract(&self) -> Option<T>;
881
}
882

            
883
fn extract_property_value<T>(prop: &CssProperty) -> Option<T>
884
where
885
    CssProperty: ExtractPropertyValue<T>,
886
{
887
    prop.extract()
888
}
889

            
890
// Implement extraction for all layout types
891

            
892
impl ExtractPropertyValue<LayoutWidth> for CssProperty {
893
    fn extract(&self) -> Option<LayoutWidth> {
894
        match self {
895
            Self::Width(CssPropertyValue::Exact(v)) => Some(v.clone()),
896
            _ => None,
897
        }
898
    }
899
}
900

            
901
impl ExtractPropertyValue<LayoutHeight> for CssProperty {
902
    fn extract(&self) -> Option<LayoutHeight> {
903
        match self {
904
            Self::Height(CssPropertyValue::Exact(v)) => Some(v.clone()),
905
            _ => None,
906
        }
907
    }
908
}
909

            
910
impl ExtractPropertyValue<LayoutMinWidth> for CssProperty {
911
    fn extract(&self) -> Option<LayoutMinWidth> {
912
        match self {
913
            Self::MinWidth(CssPropertyValue::Exact(v)) => Some(*v),
914
            _ => None,
915
        }
916
    }
917
}
918

            
919
impl ExtractPropertyValue<LayoutMinHeight> for CssProperty {
920
    fn extract(&self) -> Option<LayoutMinHeight> {
921
        match self {
922
            Self::MinHeight(CssPropertyValue::Exact(v)) => Some(*v),
923
            _ => None,
924
        }
925
    }
926
}
927

            
928
impl ExtractPropertyValue<LayoutMaxWidth> for CssProperty {
929
    fn extract(&self) -> Option<LayoutMaxWidth> {
930
        match self {
931
            Self::MaxWidth(CssPropertyValue::Exact(v)) => Some(*v),
932
            _ => None,
933
        }
934
    }
935
}
936

            
937
impl ExtractPropertyValue<LayoutMaxHeight> for CssProperty {
938
    fn extract(&self) -> Option<LayoutMaxHeight> {
939
        match self {
940
            Self::MaxHeight(CssPropertyValue::Exact(v)) => Some(*v),
941
            _ => None,
942
        }
943
    }
944
}
945

            
946
impl ExtractPropertyValue<LayoutDisplay> for CssProperty {
947
    fn extract(&self) -> Option<LayoutDisplay> {
948
        match self {
949
            Self::Display(CssPropertyValue::Exact(v)) => Some(*v),
950
            _ => None,
951
        }
952
    }
953
}
954

            
955
impl ExtractPropertyValue<LayoutWritingMode> for CssProperty {
956
    fn extract(&self) -> Option<LayoutWritingMode> {
957
        match self {
958
            Self::WritingMode(CssPropertyValue::Exact(v)) => Some(*v),
959
            _ => None,
960
        }
961
    }
962
}
963

            
964
impl ExtractPropertyValue<LayoutFlexWrap> for CssProperty {
965
    fn extract(&self) -> Option<LayoutFlexWrap> {
966
        match self {
967
            Self::FlexWrap(CssPropertyValue::Exact(v)) => Some(*v),
968
            _ => None,
969
        }
970
    }
971
}
972

            
973
impl ExtractPropertyValue<LayoutJustifyContent> for CssProperty {
974
    fn extract(&self) -> Option<LayoutJustifyContent> {
975
        match self {
976
            Self::JustifyContent(CssPropertyValue::Exact(v)) => Some(*v),
977
            _ => None,
978
        }
979
    }
980
}
981

            
982
impl ExtractPropertyValue<StyleTextAlign> for CssProperty {
983
    fn extract(&self) -> Option<StyleTextAlign> {
984
        match self {
985
            Self::TextAlign(CssPropertyValue::Exact(v)) => Some(*v),
986
            _ => None,
987
        }
988
    }
989
}
990

            
991
impl ExtractPropertyValue<LayoutFloat> for CssProperty {
992
    fn extract(&self) -> Option<LayoutFloat> {
993
        match self {
994
            Self::Float(CssPropertyValue::Exact(v)) => Some(*v),
995
            _ => None,
996
        }
997
    }
998
}
999

            
impl ExtractPropertyValue<LayoutClear> for CssProperty {
    fn extract(&self) -> Option<LayoutClear> {
        match self {
            Self::Clear(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<LayoutOverflow> for CssProperty {
    fn extract(&self) -> Option<LayoutOverflow> {
        match self {
            Self::OverflowX(CssPropertyValue::Exact(v))
            | Self::OverflowY(CssPropertyValue::Exact(v))
            | Self::OverflowBlock(CssPropertyValue::Exact(v))
            | Self::OverflowInline(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<LayoutPosition> for CssProperty {
    fn extract(&self) -> Option<LayoutPosition> {
        match self {
            Self::Position(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<LayoutBoxSizing> for CssProperty {
    fn extract(&self) -> Option<LayoutBoxSizing> {
        match self {
            Self::BoxSizing(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<PixelValue> for CssProperty {
    fn extract(&self) -> Option<PixelValue> {
        self.get_pixel_inner()
    }
}
impl ExtractPropertyValue<LayoutFlexDirection> for CssProperty {
    fn extract(&self) -> Option<LayoutFlexDirection> {
        match self {
            Self::FlexDirection(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<LayoutAlignItems> for CssProperty {
    fn extract(&self) -> Option<LayoutAlignItems> {
        match self {
            Self::AlignItems(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<LayoutAlignContent> for CssProperty {
    fn extract(&self) -> Option<LayoutAlignContent> {
        match self {
            Self::AlignContent(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleFontWeight> for CssProperty {
    fn extract(&self) -> Option<StyleFontWeight> {
        match self {
            Self::FontWeight(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleFontStyle> for CssProperty {
    fn extract(&self) -> Option<StyleFontStyle> {
        match self {
            Self::FontStyle(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleVisibility> for CssProperty {
    fn extract(&self) -> Option<StyleVisibility> {
        match self {
            Self::Visibility(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleWhiteSpace> for CssProperty {
    fn extract(&self) -> Option<StyleWhiteSpace> {
        match self {
            Self::WhiteSpace(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleDirection> for CssProperty {
    fn extract(&self) -> Option<StyleDirection> {
        match self {
            Self::Direction(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleUnicodeBidi> for CssProperty {
    fn extract(&self) -> Option<StyleUnicodeBidi> {
        match self {
            Self::UnicodeBidi(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleTextBoxTrim> for CssProperty {
    fn extract(&self) -> Option<StyleTextBoxTrim> {
        match self {
            Self::TextBoxTrim(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleTextBoxEdge> for CssProperty {
    fn extract(&self) -> Option<StyleTextBoxEdge> {
        match self {
            Self::TextBoxEdge(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleDominantBaseline> for CssProperty {
    fn extract(&self) -> Option<StyleDominantBaseline> {
        match self {
            Self::DominantBaseline(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleAlignmentBaseline> for CssProperty {
    fn extract(&self) -> Option<StyleAlignmentBaseline> {
        match self {
            Self::AlignmentBaseline(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleBaselineSource> for CssProperty {
    fn extract(&self) -> Option<StyleBaselineSource> {
        match self {
            Self::BaselineSource(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleLineFitEdge> for CssProperty {
    fn extract(&self) -> Option<StyleLineFitEdge> {
        match self {
            Self::LineFitEdge(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleInitialLetterAlign> for CssProperty {
    fn extract(&self) -> Option<StyleInitialLetterAlign> {
        match self {
            Self::InitialLetterAlign(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleInitialLetterWrap> for CssProperty {
    fn extract(&self) -> Option<StyleInitialLetterWrap> {
        match self {
            Self::InitialLetterWrap(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleScrollbarGutter> for CssProperty {
    fn extract(&self) -> Option<StyleScrollbarGutter> {
        match self {
            Self::ScrollbarGutter(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleOverflowClipMargin> for CssProperty {
    fn extract(&self) -> Option<StyleOverflowClipMargin> {
        match self {
            Self::OverflowClipMargin(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleVerticalAlign> for CssProperty {
    fn extract(&self) -> Option<StyleVerticalAlign> {
        match self {
            Self::VerticalAlign(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
get_css_property!(
    get_writing_mode,
    get_writing_mode,
    LayoutWritingMode,
    CssPropertyType::WritingMode,
    compact = get_writing_mode
);
get_css_property!(
    get_css_width,
    get_width,
    LayoutWidth,
    CssPropertyType::Width,
    compact_u32_dim = get_width_raw,
    LayoutWidth::Px,
    LayoutWidth::Auto,
    LayoutWidth::MinContent,
    LayoutWidth::MaxContent
);
get_css_property!(
    get_css_height,
    get_height,
    LayoutHeight,
    CssPropertyType::Height,
    compact_u32_dim = get_height_raw,
    LayoutHeight::Px,
    LayoutHeight::Auto,
    LayoutHeight::MinContent,
    LayoutHeight::MaxContent
);
get_css_property!(
    get_wrap,
    get_flex_wrap,
    LayoutFlexWrap,
    CssPropertyType::FlexWrap,
    compact = get_flex_wrap
);
get_css_property!(
    get_justify_content,
    get_justify_content,
    LayoutJustifyContent,
    CssPropertyType::JustifyContent,
    compact = get_justify_content
);
get_css_property!(
    get_text_align,
    get_text_align,
    StyleTextAlign,
    CssPropertyType::TextAlign,
    compact = get_text_align
);
get_css_property!(
    get_float,
    get_float,
    LayoutFloat,
    CssPropertyType::Float,
    compact = get_float
);
get_css_property!(
    get_clear,
    get_clear,
    LayoutClear,
    CssPropertyType::Clear,
    compact = get_clear
);
get_css_property!(
    get_overflow_x_declared,
    get_overflow_x,
    LayoutOverflow,
    CssPropertyType::OverflowX,
    compact = get_overflow_x
);
get_css_property!(
    get_overflow_y_declared,
    get_overflow_y,
    LayoutOverflow,
    CssPropertyType::OverflowY,
    compact = get_overflow_y
);
// +spec:overflow:17654b - overflow-block and overflow-inline logical properties resolve to physical overflow based on writing mode
/// Physical `overflow-x`, with the css-overflow-3 logical fallback: when the
/// physical property is unset, a declared `overflow-inline` (horizontal
/// writing modes) or `overflow-block` (vertical) supplies the value. On the
/// compact fast path the mapping already happened at build time, in
/// declaration order (equal-specificity last-wins); this slow-path fallback
/// uses "physical if declared, else logical" as the cascade approximation.
2213179
#[must_use] pub fn get_overflow_x(
2213179
    styled_dom: &StyledDom,
2213179
    node_id: NodeId,
2213179
    node_state: &StyledNodeState,
2213179
) -> MultiValue<LayoutOverflow> {
2213179
    let phys = get_overflow_x_declared(styled_dom, node_id, node_state);
2213179
    if matches!(phys, MultiValue::Exact(_)) {
2212695
        return phys;
484
    }
484
    let vertical = matches!(
484
        get_writing_mode(styled_dom, node_id, node_state),
        MultiValue::Exact(LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr)
    );
484
    let logical = if vertical {
        get_overflow_block(styled_dom, node_id, node_state)
    } else {
484
        get_overflow_inline(styled_dom, node_id, node_state)
    };
484
    if matches!(logical, MultiValue::Exact(_)) {
        logical
    } else {
484
        phys
    }
2213179
}
/// Physical `overflow-y`; see [`get_overflow_x`] for the logical fallback.
2068748
#[must_use] pub fn get_overflow_y(
2068748
    styled_dom: &StyledDom,
2068748
    node_id: NodeId,
2068748
    node_state: &StyledNodeState,
2068748
) -> MultiValue<LayoutOverflow> {
2068748
    let phys = get_overflow_y_declared(styled_dom, node_id, node_state);
2068748
    if matches!(phys, MultiValue::Exact(_)) {
2068397
        return phys;
351
    }
351
    let vertical = matches!(
351
        get_writing_mode(styled_dom, node_id, node_state),
        MultiValue::Exact(LayoutWritingMode::VerticalRl | LayoutWritingMode::VerticalLr)
    );
351
    let logical = if vertical {
        get_overflow_inline(styled_dom, node_id, node_state)
    } else {
351
        get_overflow_block(styled_dom, node_id, node_state)
    };
351
    if matches!(logical, MultiValue::Exact(_)) {
        logical
    } else {
351
        phys
    }
2068748
}
// +spec:overflow:17654b - overflow-block and overflow-inline logical properties resolve to physical overflow based on writing mode
get_css_property!(
    get_overflow_block,
    get_overflow_block,
    LayoutOverflow,
    CssPropertyType::OverflowBlock
);
get_css_property!(
    get_overflow_inline,
    get_overflow_inline,
    LayoutOverflow,
    CssPropertyType::OverflowInline
);
get_css_property!(
    get_position,
    get_position,
    LayoutPosition,
    CssPropertyType::Position,
    compact = get_position
);
get_css_property!(
    get_css_box_sizing,
    get_box_sizing,
    LayoutBoxSizing,
    CssPropertyType::BoxSizing,
    compact = get_box_sizing
);
get_css_property!(
    get_flex_direction,
    get_flex_direction,
    LayoutFlexDirection,
    CssPropertyType::FlexDirection,
    compact = get_flex_direction
);
get_css_property!(
    get_align_items,
    get_align_items,
    LayoutAlignItems,
    CssPropertyType::AlignItems,
    compact = get_align_items
);
get_css_property!(
    get_align_content,
    get_align_content,
    LayoutAlignContent,
    CssPropertyType::AlignContent,
    compact = get_align_content
);
get_css_property!(
    get_font_weight_property,
    get_font_weight,
    StyleFontWeight,
    CssPropertyType::FontWeight,
    compact = get_font_weight
);
get_css_property!(
    get_font_style_property,
    get_font_style,
    StyleFontStyle,
    CssPropertyType::FontStyle,
    compact = get_font_style
);
get_css_property!(
    get_visibility,
    get_visibility,
    StyleVisibility,
    CssPropertyType::Visibility,
    compact = get_visibility
);
get_css_property!(
    get_white_space_property,
    get_white_space,
    StyleWhiteSpace,
    CssPropertyType::WhiteSpace,
    compact = get_white_space
);
// +spec:writing-modes:3af12f - unicode-bidi does not affect direction for layout; we use direction property directly
get_css_property!(
    get_direction_property,
    get_direction,
    StyleDirection,
    CssPropertyType::Direction,
    compact = get_direction
);
// +spec:display-property:346799 - inline-level elements with unicode-bidi:normal have no effect on text ordering
// +spec:writing-modes:3e2632 - unicode-bidi property resolves embedding level for bidi algorithm (LRE/RLE/PDF)
// +spec:writing-modes:d2c94f - direction+unicode-bidi properties map to UAX#9 bidirectional algorithm
get_css_property!(
    get_unicode_bidi_property,
    get_unicode_bidi,
    StyleUnicodeBidi,
    CssPropertyType::UnicodeBidi
);
// +spec:display-property:db5125 - text-box-trim on inline boxes trims content box to text-box-edge metric
// +spec:display-property:dceb24 - text-box-trim on inline boxes: content edges coincide with text baselines
get_css_property!(
    get_text_box_trim_property,
    get_text_box_trim,
    StyleTextBoxTrim,
    CssPropertyType::TextBoxTrim
);
get_css_property!(
    get_text_box_edge_property,
    get_text_box_edge,
    StyleTextBoxEdge,
    CssPropertyType::TextBoxEdge
);
get_css_property!(
    get_dominant_baseline_property,
    get_dominant_baseline,
    StyleDominantBaseline,
    CssPropertyType::DominantBaseline
);
get_css_property!(
    get_alignment_baseline_property,
    get_alignment_baseline,
    StyleAlignmentBaseline,
    CssPropertyType::AlignmentBaseline
);
get_css_property!(
    get_baseline_source_property,
    get_baseline_source,
    StyleBaselineSource,
    CssPropertyType::BaselineSource
);
get_css_property!(
    get_line_fit_edge_property,
    get_line_fit_edge,
    StyleLineFitEdge,
    CssPropertyType::LineFitEdge
);
get_css_property!(
    get_initial_letter_align_property,
    get_initial_letter_align,
    StyleInitialLetterAlign,
    CssPropertyType::InitialLetterAlign
);
get_css_property!(
    get_initial_letter_wrap_property,
    get_initial_letter_wrap,
    StyleInitialLetterWrap,
    CssPropertyType::InitialLetterWrap
);
// +spec:overflow:5d15e2 - block-start/block-end scrollbar gutter follows same rules as inline gutters when auto
//
// Hand-rolled fast path: 99% of nodes don't set scrollbar-gutter, and the
// default is `auto`. The compact cache stores the enum in 2 bits of
// tier2_cold.hot_flags, so we can return the answer without a cascade walk.
#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
1007974
#[must_use] pub fn get_scrollbar_gutter_property(
1007974
    styled_dom: &StyledDom,
1007974
    node_id: NodeId,
1007974
    node_state: &StyledNodeState,
1007974
) -> MultiValue<StyleScrollbarGutter> {
    // FAST PATH: 2-bit enum in hot_flags
1007974
    if node_state.is_normal() {
1007954
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1007953
            let bits = cc.get_scrollbar_gutter_bits(node_id.index());
1007953
            let val = match bits {
1007953
                azul_css::compact_cache::SCROLLBAR_GUTTER_AUTO => StyleScrollbarGutter::Auto,
                azul_css::compact_cache::SCROLLBAR_GUTTER_STABLE => StyleScrollbarGutter::Stable,
                azul_css::compact_cache::SCROLLBAR_GUTTER_BOTH_EDGES => {
                    StyleScrollbarGutter::StableBothEdges
                }
                _ => StyleScrollbarGutter::Auto,
            };
1007953
            return MultiValue::Exact(val);
1
        }
20
    }
    // SLOW PATH: cascade resolution for pseudo-states or missing cache
21
    let node_data = &styled_dom.node_data.as_container()[node_id];
21
    let author_css = styled_dom
21
        .css_property_cache
21
        .ptr
21
        .get_scrollbar_gutter(node_data, &node_id, node_state);
21
    if let Some(val) = author_css.and_then(|v| v.get_property().copied()) {
        return MultiValue::Exact(val);
21
    }
21
    MultiValue::Auto
1007974
}
get_css_property!(
    get_overflow_clip_margin_property,
    get_overflow_clip_margin,
    StyleOverflowClipMargin,
    CssPropertyType::OverflowClipMargin
);
get_css_property!(
    get_object_fit_property,
    get_object_fit,
    StyleObjectFit,
    CssPropertyType::ObjectFit
);
get_css_property!(
    get_text_overflow_property,
    get_text_overflow,
    StyleTextOverflow,
    CssPropertyType::TextOverflow
);
// +spec:writing-modes:257296 - text-orientation getter for vertical typesetting (upright/sideways)
//
// Hand-rolled (not macro-generated) to attach a negative fast-path: most
// nodes have no text-orientation declared (default = Mixed), so we avoid a
// cascade walk per fc.rs call (which is called ~2× per node).
462718
#[must_use] pub fn get_text_orientation_property(
462718
    styled_dom: &StyledDom,
462718
    node_id: NodeId,
462718
    node_state: &StyledNodeState,
462718
) -> MultiValue<StyleTextOrientation> {
462718
    if node_state.is_normal() {
462707
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
462705
            if !cc.has_text_orientation(node_id.index()) {
462705
                return MultiValue::Auto;
            }
2
        }
11
    }
13
    let node_data = &styled_dom.node_data.as_container()[node_id];
13
    if let Some(val) = styled_dom
13
        .css_property_cache
13
        .ptr
13
        .get_text_orientation(node_data, &node_id, node_state)
13
        .and_then(|v| v.get_property().copied())
    {
        return MultiValue::Exact(val);
13
    }
13
    let ua = azul_core::ua_css::get_ua_property(
13
        &node_data.node_type,
13
        CssPropertyType::TextOrientation,
    );
13
    if let Some(ua_prop) = ua {
        if let Some(val) = extract_property_value::<StyleTextOrientation>(ua_prop) {
            return MultiValue::Exact(val);
        }
13
    }
13
    MultiValue::Auto
462718
}
get_css_property!(
    get_object_position_property,
    get_object_position,
    StyleObjectPosition,
    CssPropertyType::ObjectPosition
);
get_css_property!(
    get_aspect_ratio_property,
    get_aspect_ratio,
    StyleAspectRatio,
    CssPropertyType::AspectRatio
);
// NOTE: vertical-align does NOT use the compact cache because the compact cache
// only stores keyword variants (3 bits = 8 values) and silently drops
// Percentage/Length values by mapping them to Baseline. Always use the slow path.
56587
#[must_use] pub fn get_vertical_align_property(
56587
    styled_dom: &StyledDom,
56587
    node_id: NodeId,
56587
    node_state: &StyledNodeState,
56587
) -> MultiValue<StyleVerticalAlign> {
56587
    let node_data = &styled_dom.node_data.as_container()[node_id];
56587
    let author_css = styled_dom
56587
        .css_property_cache
56587
        .ptr
56587
        .get_vertical_align(node_data, &node_id, node_state);
56587
    if let Some(val) = author_css.and_then(|v| v.get_property().copied()) {
783
        return MultiValue::Exact(val);
55804
    }
55804
    let ua_css = azul_core::ua_css::get_ua_property(
55804
        &node_data.node_type,
55804
        CssPropertyType::VerticalAlign,
    );
55804
    if let Some(ua_prop) = ua_css {
        if let Some(val) = extract_property_value::<StyleVerticalAlign>(ua_prop) {
            return MultiValue::Exact(val);
        }
55804
    }
55804
    MultiValue::Auto
56587
}
// Complex Property Getters
/// Get border radius for all four corners (raw CSS property values)
378489
#[must_use] pub fn get_style_border_radius(
378489
    styled_dom: &StyledDom,
378489
    node_id: NodeId,
378489
    node_state: &StyledNodeState,
378489
) -> StyleBorderRadius {
    use azul_css::props::basic::pixel::PixelValue;
    // FAST PATH: all four corners live in tier2_cold as i16 px × 10. The
    // common case (no rounded corners anywhere) reads four bytes and bails.
378489
    if node_state.is_normal() {
378484
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
378483
            let idx = node_id.index();
1513932
            let decode = |raw: i16| -> PixelValue {
1513932
                if raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
1511948
                    PixelValue::px(0.0)
                } else {
1984
                    PixelValue::px(f32::from(raw) / 10.0)
                }
1513932
            };
378483
            return StyleBorderRadius {
378483
                top_left: decode(cc.get_border_top_left_radius_raw(idx)),
378483
                top_right: decode(cc.get_border_top_right_radius_raw(idx)),
378483
                bottom_right: decode(cc.get_border_bottom_right_radius_raw(idx)),
378483
                bottom_left: decode(cc.get_border_bottom_left_radius_raw(idx)),
378483
            };
1
        }
5
    }
6
    let node_data = &styled_dom.node_data.as_container()[node_id];
6
    let top_left = styled_dom
6
        .css_property_cache
6
        .ptr
6
        .get_border_top_left_radius(node_data, &node_id, node_state)
6
        .and_then(|br| br.get_property_or_default())
6
        .map(|v| v.inner)
6
        .unwrap_or_default();
6
    let top_right = styled_dom
6
        .css_property_cache
6
        .ptr
6
        .get_border_top_right_radius(node_data, &node_id, node_state)
6
        .and_then(|br| br.get_property_or_default())
6
        .map(|v| v.inner)
6
        .unwrap_or_default();
6
    let bottom_right = styled_dom
6
        .css_property_cache
6
        .ptr
6
        .get_border_bottom_right_radius(node_data, &node_id, node_state)
6
        .and_then(|br| br.get_property_or_default())
6
        .map(|v| v.inner)
6
        .unwrap_or_default();
6
    let bottom_left = styled_dom
6
        .css_property_cache
6
        .ptr
6
        .get_border_bottom_left_radius(node_data, &node_id, node_state)
6
        .and_then(|br| br.get_property_or_default())
6
        .map(|v| v.inner)
6
        .unwrap_or_default();
6
    StyleBorderRadius {
6
        top_left,
6
        top_right,
6
        bottom_right,
6
        bottom_left,
6
    }
378489
}
/// Get border radius for all four corners (resolved to pixels)
///
/// # Arguments
/// * `element_size` - The element's own size (width × height) for % resolution. According to CSS
///   spec, border-radius % uses element's own dimensions.
1707686
#[must_use] pub fn get_border_radius(
1707686
    styled_dom: &StyledDom,
1707686
    node_id: NodeId,
1707686
    node_state: &StyledNodeState,
1707686
    element_size: PhysicalSizeImport,
1707686
    viewport_size: LogicalSize,
1707686
) -> BorderRadius {
    use azul_css::props::basic::{PhysicalSize, PropertyContext, ResolutionContext};
    // FAST PATH: all four corners as i16 px × 10 in tier2_cold. The
    // overwhelmingly common case (no rounded corners) reads four bytes and
    // returns zeros without a cascade walk.
1707686
    if node_state.is_normal() {
1707657
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1707648
            let idx = node_id.index();
1707648
            let tl = cc.get_border_top_left_radius_raw(idx);
1707648
            let tr = cc.get_border_top_right_radius_raw(idx);
1707648
            let br = cc.get_border_bottom_right_radius_raw(idx);
1707648
            let bl = cc.get_border_bottom_left_radius_raw(idx);
            // sentinel = "unset" = 0 px (no corner radius)
1707648
            let thresh = azul_css::compact_cache::I16_SENTINEL_THRESHOLD;
6830592
            let decode = |raw: i16| -> f32 {
6830592
                if raw >= thresh {
6824648
                    0.0
                } else {
5944
                    f32::from(raw) / 10.0
                }
6830592
            };
1707648
            return BorderRadius {
1707648
                top_left: decode(tl),
1707648
                top_right: decode(tr),
1707648
                bottom_right: decode(br),
1707648
                bottom_left: decode(bl),
1707648
            };
9
        }
29
    }
38
    let node_data = &styled_dom.node_data.as_container()[node_id];
    // Get font sizes for em/rem resolution
38
    let element_font_size = get_element_font_size(styled_dom, node_id, node_state);
38
    let parent_font_size = styled_dom
38
        .node_hierarchy
38
        .as_container()
38
        .get(node_id)
38
        .and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id)
38
        .map_or(DEFAULT_FONT_SIZE, |p| get_element_font_size(styled_dom, p, node_state));
38
    let root_font_size = get_root_font_size(styled_dom, node_state);
    // Create resolution context
38
    let context = ResolutionContext {
38
        vertical_writing_mode: false,
38
        element_font_size,
38
        parent_font_size,
38
        root_font_size,
38
        containing_block_size: PhysicalSize::new(0.0, 0.0), // Not used for border-radius
38
        element_size: Some(PhysicalSize::new(element_size.width, element_size.height)),
38
        viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
38
    };
38
    let top_left = styled_dom
38
        .css_property_cache
38
        .ptr
38
        .get_border_top_left_radius(node_data, &node_id, node_state)
38
        .and_then(|br| br.get_property().copied())
38
        .unwrap_or_default();
38
    let top_right = styled_dom
38
        .css_property_cache
38
        .ptr
38
        .get_border_top_right_radius(node_data, &node_id, node_state)
38
        .and_then(|br| br.get_property().copied())
38
        .unwrap_or_default();
38
    let bottom_right = styled_dom
38
        .css_property_cache
38
        .ptr
38
        .get_border_bottom_right_radius(node_data, &node_id, node_state)
38
        .and_then(|br| br.get_property().copied())
38
        .unwrap_or_default();
38
    let bottom_left = styled_dom
38
        .css_property_cache
38
        .ptr
38
        .get_border_bottom_left_radius(node_data, &node_id, node_state)
38
        .and_then(|br| br.get_property().copied())
38
        .unwrap_or_default();
38
    BorderRadius {
38
        top_left: top_left
38
            .inner
38
            .resolve_with_context(&context, PropertyContext::BorderRadius),
38
        top_right: top_right
38
            .inner
38
            .resolve_with_context(&context, PropertyContext::BorderRadius),
38
        bottom_right: bottom_right
38
            .inner
38
            .resolve_with_context(&context, PropertyContext::BorderRadius),
38
        bottom_left: bottom_left
38
            .inner
38
            .resolve_with_context(&context, PropertyContext::BorderRadius),
38
    }
1707686
}
// +spec:stacking-contexts:a93e62 - stack level from z-index for stacking context ordering
// +spec:stacking-contexts:ae50ae - z-index specifies stack level; auto resolves to 0 (inherited from parent stacking context)
/// Get z-index for stacking context ordering.
///
/// Returns the resolved integer z-index value:
/// - `z-index: auto` → 0 (participates in parent's stacking context)
/// - `z-index: <integer>` → that integer value
9201
#[must_use] pub fn get_z_index(styled_dom: &StyledDom, node_id: Option<NodeId>) -> i32 {
    use azul_css::props::layout::position::LayoutZIndex;
9201
    let Some(node_id) = node_id else {
1
        return 0;
    };
9200
    let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
    // FAST PATH: compact cache for normal state
9200
    if node_state.is_normal() {
9199
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
9198
            let raw = cc.get_z_index(node_id.index());
9198
            if raw == azul_css::compact_cache::I16_AUTO {
9190
                return 0;
8
            }
8
            if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
5
                return i32::from(raw);
3
            }
            // I16_SENTINEL → fall through to slow path
1
        }
1
    }
    // SLOW PATH
5
    let node_data = &styled_dom.node_data.as_container()[node_id];
5
    styled_dom
5
        .css_property_cache
5
        .ptr
5
        .get_z_index(node_data, &node_id, node_state)
5
        .and_then(|v| v.get_property())
5
        .map_or(0, |z| match z {
            LayoutZIndex::Auto => 0,
3
            LayoutZIndex::Integer(i) => *i,
3
        })
9201
}
// +spec:positioning:c041c4 - positioned elements with z-index != auto establish stacking contexts
// z-index:<integer> ALWAYS establishes new stacking context on positioned elements
/// Returns true if z-index is `auto` (the initial value), false if it's an explicit `<integer>`.
/// This distinction matters for stacking context creation per §9.9.1.
1307619
#[must_use] pub fn is_z_index_auto(styled_dom: &StyledDom, node_id: Option<NodeId>) -> bool {
    use azul_css::props::layout::position::LayoutZIndex;
1307619
    let Some(node_id) = node_id else {
1
        return true;
    };
1307618
    let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
    // FAST PATH: compact cache for normal state
1307618
    if node_state.is_normal() {
1307602
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1307602
            let raw = cc.get_z_index(node_id.index());
1307602
            if raw == azul_css::compact_cache::I16_AUTO {
1307594
                return true;
8
            }
8
            if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
5
                return false; // explicit integer
3
            }
            // I16_SENTINEL → fall through to slow path
        }
16
    }
    // SLOW PATH
19
    let node_data = &styled_dom.node_data.as_container()[node_id];
19
    styled_dom
19
        .css_property_cache
19
        .ptr
19
        .get_z_index(node_data, &node_id, node_state)
19
        .and_then(|v| v.get_property())
19
        .is_none_or(|z| matches!(z, LayoutZIndex::Auto)) // no value = auto
1307619
}
// Rendering Property Getters
/// Information about background color for a node
///
/// # CSS Background Propagation (Special Case for HTML Root)
///
/// According to CSS Backgrounds and Borders Module Level 3, Section "The Canvas Background
/// and the HTML `<body>` Element":
///
/// For HTML documents where the root element is `<html>`, if the computed value of
/// `background-image` on the root element is `none` AND its `background-color` is `transparent`,
/// user agents **must propagate** the computed values of the background properties from the
/// first `<body>` child element to the root element.
///
/// This behavior exists for backwards compatibility with older HTML where backgrounds were
/// typically set on `<body>` using `bgcolor` attributes, and ensures that the `<body>`
/// background covers the entire viewport/canvas even when `<body>` itself has constrained
/// dimensions.
///
/// Implementation: When requesting the background of an `<html>` node, we first check if it
/// has a transparent background with no image. If so, we look for a `<body>` child and use
/// its background instead.
#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
65842
#[must_use] pub fn get_background_color(
65842
    styled_dom: &StyledDom,
65842
    node_id: NodeId,
65842
    node_state: &StyledNodeState,
65842
) -> ColorU {
65842
    let node_data = &styled_dom.node_data.as_container()[node_id];
65842
    let cache = &styled_dom.css_property_cache.ptr;
    // Fast path: Get this node's background.
    // Negative fast path: if compact cache says `has_background == 0` on a
    // normal-state node, skip the cascade walk entirely. Only declared backgrounds
    // set the bit, so `false` is a safe "unconditionally transparent" signal.
67948
    let get_node_bg = |nid: NodeId, ndata: &azul_core::dom::NodeData, state: &StyledNodeState| {
67948
        if state.is_normal() {
67938
            if let Some(ref cc) = cache.compact_cache {
67937
                if !cc.has_background(nid.index()) {
65382
                    return None;
2555
                }
1
            }
10
        }
2566
        cache
2566
            .get_background_content(ndata, &nid, state)
2566
            .and_then(|bg| bg.get_property())
2566
            .and_then(|bg_vec| bg_vec.get(0).cloned())
2566
            .and_then(|first_bg| match &first_bg {
2540
                azul_css::props::style::StyleBackgroundContent::Color(color) => Some(*color),
                azul_css::props::style::StyleBackgroundContent::Image(_) => None, // Has image, not transparent
18
                _ => None,
2558
            })
67948
    };
65842
    let own_bg = get_node_bg(node_id, node_data, node_state);
    // CSS Background Propagation: Special handling for <html> root element
    // Only check propagation if this is an Html node AND has transparent background (no
    // color/image)
65842
    if !matches!(node_data.node_type, NodeType::Html) || own_bg.is_some() {
        // Not Html or has its own background - return own background or transparent
63727
        return own_bg.unwrap_or(ColorU {
63727
            r: 0,
63727
            g: 0,
63727
            b: 0,
63727
            a: 0,
63727
        });
2115
    }
    // Html node with transparent background - check if we should propagate from <body>
2115
    let first_child = styled_dom
2115
        .node_hierarchy
2115
        .as_container()
2115
        .get(node_id)
2115
        .and_then(|node| node.first_child_id(node_id));
2115
    let Some(first_child) = first_child else {
        return ColorU {
            r: 0,
            g: 0,
            b: 0,
            a: 0,
        };
    };
2115
    let first_child_data = &styled_dom.node_data.as_container()[first_child];
    // Check if first child is <body>
2115
    if !matches!(first_child_data.node_type, NodeType::Body) {
9
        return ColorU {
9
            r: 0,
9
            g: 0,
9
            b: 0,
9
            a: 0,
9
        };
2106
    }
    // Propagate <body>'s background to <html> (canvas)
2106
    let first_child_state = &styled_dom.styled_nodes.as_container()[first_child].styled_node_state;
2106
    get_node_bg(first_child, first_child_data, first_child_state).unwrap_or(ColorU {
2106
        r: 0,
2106
        g: 0,
2106
        b: 0,
2106
        a: 0,
2106
    })
65842
}
/// Returns all background content layers for a node (colors, gradients, images).
/// This is used for rendering backgrounds that may include linear/radial/conic gradients.
///
/// CSS Background Propagation (CSS Backgrounds 3, Section 2.11.2):
/// For HTML documents, if the root `<html>` element has no background (transparent with no image),
/// propagate the background from the first `<body>` child element.
1095342
#[must_use] pub fn get_background_contents(
1095342
    styled_dom: &StyledDom,
1095342
    node_id: NodeId,
1095342
    node_state: &StyledNodeState,
1095342
) -> Vec<azul_css::props::style::StyleBackgroundContent> {
    use azul_core::dom::NodeType;
    use azul_css::props::style::StyleBackgroundContent;
1095342
    let node_data = &styled_dom.node_data.as_container()[node_id];
1095342
    let cache = &styled_dom.css_property_cache.ptr;
    // Helper to get backgrounds for a node.
    // Negative fast path: if compact cache says `has_background == 0` on a normal
    // pseudo-state node, return empty without walking the cascade.
1095342
    let get_node_backgrounds = |nid: NodeId,
                                ndata: &azul_core::dom::NodeData,
                                state: &StyledNodeState|
1115690
     -> Vec<StyleBackgroundContent> {
1115690
        if state.is_normal() {
1115679
            if let Some(ref cc) = cache.compact_cache {
1115678
                if !cc.has_background(nid.index()) {
805998
                    return Vec::new();
309680
                }
1
            }
11
        }
309692
        cache
309692
            .get_background_content(ndata, &nid, state)
309692
            .and_then(|bg| bg.get_property())
309692
            .map(|bg_vec| bg_vec.iter().cloned().collect())
309692
            .unwrap_or_default()
1115690
    };
1095342
    let own_backgrounds = get_node_backgrounds(node_id, node_data, node_state);
    // CSS Background Propagation: Special handling for <html> root element
    // Only check propagation if this is an Html node AND has no backgrounds
1095342
    if !matches!(node_data.node_type, NodeType::Html) || !own_backgrounds.is_empty() {
1074958
        return own_backgrounds;
20384
    }
    // Html node with no backgrounds - check if we should propagate from <body>
20384
    let first_child = styled_dom
20384
        .node_hierarchy
20384
        .as_container()
20384
        .get(node_id)
20384
        .and_then(|node| node.first_child_id(node_id));
20384
    let Some(first_child) = first_child else {
        return own_backgrounds;
    };
20384
    let first_child_data = &styled_dom.node_data.as_container()[first_child];
    // Check if first child is <body>
20384
    if !matches!(first_child_data.node_type, NodeType::Body) {
36
        return own_backgrounds;
20348
    }
    // Propagate <body>'s backgrounds to <html> (canvas)
20348
    let first_child_state = &styled_dom.styled_nodes.as_container()[first_child].styled_node_state;
20348
    get_node_backgrounds(first_child, first_child_data, first_child_state)
1095342
}
/// Information about border rendering
#[derive(Copy, Clone, Debug)]
pub struct BorderInfo {
    pub widths: crate::solver3::display_list::StyleBorderWidths,
    pub colors: crate::solver3::display_list::StyleBorderColors,
    pub styles: crate::solver3::display_list::StyleBorderStyles,
}
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
433002
#[must_use] pub fn get_border_info(
433002
    styled_dom: &StyledDom,
433002
    node_id: NodeId,
433002
    node_state: &StyledNodeState,
433002
) -> BorderInfo {
    use crate::solver3::display_list::{StyleBorderColors, StyleBorderStyles, StyleBorderWidths};
    use azul_css::css::CssPropertyValue;
    use azul_css::props::basic::color::ColorU;
    use azul_css::props::basic::pixel::PixelValue;
    use azul_css::props::style::border::{
        BorderStyle, StyleBorderBottomColor, StyleBorderBottomStyle, StyleBorderLeftColor,
        StyleBorderLeftStyle, StyleBorderRightColor, StyleBorderRightStyle, StyleBorderTopColor,
        StyleBorderTopStyle,
    };
    use azul_css::props::style::{
        LayoutBorderBottomWidth, LayoutBorderLeftWidth, LayoutBorderRightWidth,
        LayoutBorderTopWidth,
    };
    // FAST PATH: compact cache for normal state
433002
    if node_state.is_normal() {
432996
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
432995
            let idx = node_id.index();
            // Border widths: decode from compact i16 (resolved px × 10).
            // Previously this block called the slow convenience getters
            // despite being in the "fast path" branch — 2014 slow walks
            // per width × 4 widths per cold excel.html layout. Fixed
            // 2026-04-17.
1731980
            let make_width_px = |raw: i16| -> Option<PixelValue> {
1731980
                if raw == azul_css::compact_cache::I16_AUTO
1731980
                    || raw == azul_css::compact_cache::I16_INITIAL
1731980
                    || raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD
                {
                    None
                } else {
1731980
                    Some(PixelValue::px(f32::from(raw) / 10.0))
                }
1731980
            };
432995
            let widths = StyleBorderWidths {
432995
                top: make_width_px(cc.get_border_top_width_raw(idx))
432995
                    .map(|px| CssPropertyValue::Exact(LayoutBorderTopWidth { inner: px })),
432995
                right: make_width_px(cc.get_border_right_width_raw(idx))
432995
                    .map(|px| CssPropertyValue::Exact(LayoutBorderRightWidth { inner: px })),
432995
                bottom: make_width_px(cc.get_border_bottom_width_raw(idx))
432995
                    .map(|px| CssPropertyValue::Exact(LayoutBorderBottomWidth { inner: px })),
432995
                left: make_width_px(cc.get_border_left_width_raw(idx))
432995
                    .map(|px| CssPropertyValue::Exact(LayoutBorderLeftWidth { inner: px })),
            };
            // Border colors from compact cache.
            //
            // SENTINEL COLLISION: the cache packs RGBA into one u32 and uses
            // raw == 0 as "unset" — which is the same encoding as explicit
            // transparent black {0,0,0,0}. Both therefore decode to `None`.
            // That is safe ONLY because every consumer treats a missing
            // border color as "paint nothing" (see the transparent
            // `default_color` in cpurender/raster.rs); do not map `None`
            // to any visible color downstream.
1731980
            let make_color = |raw: u32| -> Option<ColorU> {
1731980
                if raw == 0 {
1670173
                    None
                } else {
61807
                    Some(ColorU {
61807
                        r: ((raw >> 24) & 0xFF) as u8,
61807
                        g: ((raw >> 16) & 0xFF) as u8,
61807
                        b: ((raw >> 8) & 0xFF) as u8,
61807
                        a: (raw & 0xFF) as u8,
61807
                    })
                }
1731980
            };
432995
            let colors = StyleBorderColors {
432995
                top: make_color(cc.get_border_top_color_raw(idx))
432995
                    .map(|c| CssPropertyValue::Exact(StyleBorderTopColor { inner: c })),
432995
                right: make_color(cc.get_border_right_color_raw(idx))
432995
                    .map(|c| CssPropertyValue::Exact(StyleBorderRightColor { inner: c })),
432995
                bottom: make_color(cc.get_border_bottom_color_raw(idx))
432995
                    .map(|c| CssPropertyValue::Exact(StyleBorderBottomColor { inner: c })),
432995
                left: make_color(cc.get_border_left_color_raw(idx))
432995
                    .map(|c| CssPropertyValue::Exact(StyleBorderLeftColor { inner: c })),
            };
            // Border styles from compact cache
432995
            let styles = StyleBorderStyles {
432995
                top: Some(CssPropertyValue::Exact(StyleBorderTopStyle {
432995
                    inner: cc.get_border_top_style(idx),
432995
                })),
432995
                right: Some(CssPropertyValue::Exact(StyleBorderRightStyle {
432995
                    inner: cc.get_border_right_style(idx),
432995
                })),
432995
                bottom: Some(CssPropertyValue::Exact(StyleBorderBottomStyle {
432995
                    inner: cc.get_border_bottom_style(idx),
432995
                })),
432995
                left: Some(CssPropertyValue::Exact(StyleBorderLeftStyle {
432995
                    inner: cc.get_border_left_style(idx),
432995
                })),
432995
            };
432995
            return BorderInfo {
432995
                widths,
432995
                colors,
432995
                styles,
432995
            };
1
        }
6
    }
    // SLOW PATH: full cascade
7
    let node_data = &styled_dom.node_data.as_container()[node_id];
    // Get all border widths
7
    let widths = StyleBorderWidths {
7
        top: styled_dom
7
            .css_property_cache
7
            .ptr
7
            .get_border_top_width(node_data, &node_id, node_state)
7
            .copied(),
7
        right: styled_dom
7
            .css_property_cache
7
            .ptr
7
            .get_border_right_width(node_data, &node_id, node_state)
7
            .copied(),
7
        bottom: styled_dom
7
            .css_property_cache
7
            .ptr
7
            .get_border_bottom_width(node_data, &node_id, node_state)
7
            .copied(),
7
        left: styled_dom
7
            .css_property_cache
7
            .ptr
7
            .get_border_left_width(node_data, &node_id, node_state)
7
            .copied(),
7
    };
    // Get all border colors
7
    let colors = StyleBorderColors {
7
        top: styled_dom
7
            .css_property_cache
7
            .ptr
7
            .get_border_top_color(node_data, &node_id, node_state)
7
            .copied(),
7
        right: styled_dom
7
            .css_property_cache
7
            .ptr
7
            .get_border_right_color(node_data, &node_id, node_state)
7
            .copied(),
7
        bottom: styled_dom
7
            .css_property_cache
7
            .ptr
7
            .get_border_bottom_color(node_data, &node_id, node_state)
7
            .copied(),
7
        left: styled_dom
7
            .css_property_cache
7
            .ptr
7
            .get_border_left_color(node_data, &node_id, node_state)
7
            .copied(),
7
    };
    // Get all border styles
7
    let styles = StyleBorderStyles {
7
        top: styled_dom
7
            .css_property_cache
7
            .ptr
7
            .get_border_top_style(node_data, &node_id, node_state)
7
            .copied(),
7
        right: styled_dom
7
            .css_property_cache
7
            .ptr
7
            .get_border_right_style(node_data, &node_id, node_state)
7
            .copied(),
7
        bottom: styled_dom
7
            .css_property_cache
7
            .ptr
7
            .get_border_bottom_style(node_data, &node_id, node_state)
7
            .copied(),
7
        left: styled_dom
7
            .css_property_cache
7
            .ptr
7
            .get_border_left_style(node_data, &node_id, node_state)
7
            .copied(),
7
    };
7
    BorderInfo {
7
        widths,
7
        colors,
7
        styles,
7
    }
433002
}
/// Convert `BorderInfo` to `InlineBorderInfo` for inline elements
///
/// This resolves the CSS property values to concrete pixel values and colors
/// that can be used during text rendering.
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
54517
fn get_inline_border_info(
54517
    styled_dom: &StyledDom,
54517
    node_id: NodeId,
54517
    node_state: &StyledNodeState,
54517
    border_info: &BorderInfo,
54517
    viewport: PhysicalSize,
54517
) -> Option<crate::text3::cache::InlineBorderInfo> {
    use crate::text3::cache::InlineBorderInfo;
    // Fetch padding values for inline elements. Viewport units (vw/vh/...) resolve
    // against the real viewport instead of being treated as raw pixels.
218068
    fn resolve_padding(
218068
        mv: MultiValue<PixelValue>,
218068
        viewport: PhysicalSize,
218068
    ) -> f32 {
218068
        match mv {
218068
            MultiValue::Exact(pv) => super::calc::resolve_pixel_value_with_viewport(
218068
                &pv,
                0.0,
                DEFAULT_FONT_SIZE,
                DEFAULT_FONT_SIZE,
218068
                viewport.width,
218068
                viewport.height,
            ),
            _ => 0.0,
        }
218068
    }
    macro_rules! border_width_px {
        ($field:expr) => {
            $field
                .as_ref()
218068
                .and_then(|v| v.get_property())
218068
                .map(|w| w.inner.number.get())
                .unwrap_or(0.0)
        };
    }
    macro_rules! border_color {
        ($field:expr) => {
            $field
                .as_ref()
44
                .and_then(|v| v.get_property())
                .map(|c| c.inner)
                .unwrap_or(ColorU::BLACK)
        };
    }
    // Extract border-radius (simplified - uses the average of all corners if uniform)
29
    fn get_border_radius_px(
29
        styled_dom: &StyledDom,
29
        node_id: NodeId,
29
        node_state: &StyledNodeState,
29
    ) -> Option<f32> {
29
        let node_data = &styled_dom.node_data.as_container()[node_id];
29
        let top_left = styled_dom
29
            .css_property_cache
29
            .ptr
29
            .get_border_top_left_radius(node_data, &node_id, node_state)
29
            .and_then(|br| br.get_property().copied())
29
            .map(|v| v.inner.number.get());
29
        let top_right = styled_dom
29
            .css_property_cache
29
            .ptr
29
            .get_border_top_right_radius(node_data, &node_id, node_state)
29
            .and_then(|br| br.get_property().copied())
29
            .map(|v| v.inner.number.get());
29
        let bottom_left = styled_dom
29
            .css_property_cache
29
            .ptr
29
            .get_border_bottom_left_radius(node_data, &node_id, node_state)
29
            .and_then(|br| br.get_property().copied())
29
            .map(|v| v.inner.number.get());
29
        let bottom_right = styled_dom
29
            .css_property_cache
29
            .ptr
29
            .get_border_bottom_right_radius(node_data, &node_id, node_state)
29
            .and_then(|br| br.get_property().copied())
29
            .map(|v| v.inner.number.get());
        // If any radius is defined, use the maximum (for inline, uniform radius is most common)
29
        let radii: Vec<f32> = [top_left, top_right, bottom_left, bottom_right]
29
            .into_iter()
29
            .flatten()
29
            .collect();
29
        if radii.is_empty() {
29
            None
        } else {
            Some(radii.into_iter().fold(0.0f32, f32::max))
        }
29
    }
54517
    let top = border_width_px!(&border_info.widths.top);
54517
    let right = border_width_px!(&border_info.widths.right);
54517
    let bottom = border_width_px!(&border_info.widths.bottom);
54517
    let left = border_width_px!(&border_info.widths.left);
54517
    let p_top = resolve_padding(get_css_padding_top(styled_dom, node_id, node_state), viewport);
54517
    let p_right = resolve_padding(get_css_padding_right(styled_dom, node_id, node_state), viewport);
54517
    let p_bottom = resolve_padding(get_css_padding_bottom(styled_dom, node_id, node_state), viewport);
54517
    let p_left = resolve_padding(get_css_padding_left(styled_dom, node_id, node_state), viewport);
    // Only return Some if there's actually a border or padding
54517
    let has_border = top > 0.0 || right > 0.0 || bottom > 0.0 || left > 0.0;
54517
    let has_padding = p_top > 0.0 || p_right > 0.0 || p_bottom > 0.0 || p_left > 0.0;
54517
    if !has_border && !has_padding {
54488
        return None;
29
    }
    // CSS 2.2 §8.6: detect direction for visual-order border/padding rendering in bidi
29
    let is_rtl = matches!(
29
        get_direction_property(styled_dom, node_id, node_state),
        MultiValue::Exact(StyleDirection::Rtl)
    );
29
    Some(InlineBorderInfo {
29
        top,
29
        right,
29
        bottom,
29
        left,
29
        top_color: border_color!(&border_info.colors.top),
29
        right_color: border_color!(&border_info.colors.right),
29
        bottom_color: border_color!(&border_info.colors.bottom),
29
        left_color: border_color!(&border_info.colors.left),
29
        radius: get_border_radius_px(styled_dom, node_id, node_state),
29
        padding_top: p_top,
29
        padding_right: p_right,
29
        padding_bottom: p_bottom,
29
        padding_left: p_left,
29
        is_first_fragment: true,
29
        is_last_fragment: true,
29
        is_rtl,
29
    })
54517
}
// Selection and Caret Styling
/// Style information for text selection rendering
#[derive(Debug, Clone, Copy, Default)]
pub struct SelectionStyle {
    /// Background color of the selection highlight
    pub bg_color: ColorU,
    /// Text color when selected (overrides normal text color).
    ///
    /// `None` means "no authority said anything" — neither
    /// `-azul-selection-color` nor the system style — and the painter must then
    /// leave the glyph's normal colour alone. Use [`Self::text_color_or`]
    /// rather than testing this field, so that rule lives in one place.
    pub text_color: Option<ColorU>,
    /// Border radius for selection rectangles
    pub radius: f32,
}
impl SelectionStyle {
    /// The colour to paint a glyph that falls INSIDE the selection, given the
    /// colour it would otherwise have.
    ///
    /// The selection painter draws `bg_color` behind the glyphs, so with an
    /// opaque system highlight the unrecoloured glyphs are dark-on-dark; the
    /// text pass has to apply this to every glyph covered by a selection rect
    /// (see `HANDOFF-text-fix.md` — `paint_inline_content`).
    #[must_use]
2
    pub fn text_color_or(&self, normal: ColorU) -> ColorU {
2
        self.text_color.unwrap_or(normal)
2
    }
}
/// Get selection style for a node
1089
#[must_use] pub fn get_selection_style(
1089
    styled_dom: &StyledDom,
1089
    node_id: Option<NodeId>,
1089
    system_style: Option<&std::sync::Arc<azul_css::system::SystemStyle>>,
1089
) -> SelectionStyle {
1089
    let Some(node_id) = node_id else {
1
        return SelectionStyle::default();
    };
1088
    let node_data = &styled_dom.node_data.as_container()[node_id];
1088
    let node_state = &StyledNodeState::default();
    // Try to get selection background from CSS, otherwise use system color, otherwise hard-coded default
1088
    let default_bg = system_style
1088
        .and_then(|ss| ss.colors.selection_background.as_option().copied())
1088
        .unwrap_or(ColorU {
1088
            r: 51,
1088
            g: 153,
1088
            b: 255, // Standard blue selection color
1088
            a: 128, // Semi-transparent
1088
        });
1088
    let bg_color = styled_dom
1088
        .css_property_cache
1088
        .ptr
1088
        .get_selection_background_color(node_data, &node_id, node_state)
1088
        .and_then(|c| c.get_property().copied())
1088
        .map_or(default_bg, |c| c.inner);
    // Try to get selection text color from CSS, otherwise use system color
1088
    let default_text = system_style.and_then(|ss| ss.colors.selection_text.as_option().copied());
1088
    let text_color = styled_dom
1088
        .css_property_cache
1088
        .ptr
1088
        .get_selection_color(node_data, &node_id, node_state)
1088
        .and_then(|c| c.get_property().copied())
1088
        .map(|c| c.inner)
1088
        .or(default_text);
1088
    let radius = styled_dom
1088
        .css_property_cache
1088
        .ptr
1088
        .get_selection_radius(node_data, &node_id, node_state)
1088
        .and_then(|r| r.get_property().copied())
1088
        .map_or(0.0, |r| r.inner.to_pixels_internal(0.0, DEFAULT_EM_SIZE, DEFAULT_EM_SIZE));
1088
    SelectionStyle {
1088
        bg_color,
1088
        text_color,
1088
        radius,
1088
    }
1089
}
/// Style information for caret rendering.
#[derive(Debug, Clone, Copy)]
pub struct CaretStyle {
    /// Color of the caret bar
    pub color: ColorU,
    /// Width of the caret bar in pixels
    pub width: f32,
    /// Blink animation duration (0 = no blink).
    ///
    /// A [`CssDuration`], not a bare millisecond count, so a stylesheet written
    /// in the clockless `t` unit (`caret-animation-duration: 5t`) keeps its FRAME
    /// count all the way to the blink timer. Flattening it to milliseconds here
    /// would silently reintroduce wall-clock rounding.
    pub animation_duration: CssDuration,
}
impl Default for CaretStyle {
2
    fn default() -> Self {
2
        Self {
2
            color: ColorU::BLACK,
2
            width: DEFAULT_CARET_WIDTH_PX,
2
            animation_duration: CssDuration::from_millis(DEFAULT_CARET_BLINK_MS),
2
        }
2
    }
}
/// Get caret style for a node
2701
#[must_use] pub fn get_caret_style(styled_dom: &StyledDom, node_id: Option<NodeId>) -> CaretStyle {
2701
    let Some(node_id) = node_id else {
2
        return CaretStyle::default();
    };
2699
    let node_data = &styled_dom.node_data.as_container()[node_id];
2699
    let node_state = &StyledNodeState::default();
2699
    let color = styled_dom
2699
        .css_property_cache
2699
        .ptr
2699
        .get_caret_color(node_data, &node_id, node_state)
2699
        .and_then(|c| c.get_property().copied())
        // CSS `caret-color: auto` (the initial value) resolves to currentColor — the
        // element's text color — which by construction contrasts with the background.
        // Falling back to BLACK made the caret invisible on dark backgrounds / dark
        // system themes (and `color` IS inherited while `caret-color` may not be, so a
        // child text node still gets the right colour here).
2699
        .map_or_else(|| {
2699
            styled_dom
2699
                .css_property_cache
2699
                .ptr
2699
                .get_text_color_or_default(node_data, &node_id, node_state)
2699
                .inner
2699
        }, |c| c.inner);
2699
    let width = styled_dom
2699
        .css_property_cache
2699
        .ptr
2699
        .get_caret_width(node_data, &node_id, node_state)
2699
        .and_then(|w| w.get_property().copied())
2699
        .map_or(DEFAULT_CARET_WIDTH_PX, |w| w.inner.to_pixels_internal(0.0, DEFAULT_EM_SIZE, DEFAULT_EM_SIZE));
    // Bound first so the fallback is neither a lazy closure nor an inline call
    // in `map_or` — both shapes trip a clippy lint, and neither reads better.
2699
    let default_blink = CssDuration::from_millis(DEFAULT_CARET_BLINK_MS);
2699
    let animation_duration = styled_dom
2699
        .css_property_cache
2699
        .ptr
2699
        .get_caret_animation_duration(node_data, &node_id, node_state)
2699
        .and_then(|d| d.get_property().copied())
2699
        .map_or(default_blink, |d| d.inner);
2699
    CaretStyle {
2699
        color,
2699
        width,
2699
        animation_duration,
2699
    }
2701
}
// Scrollbar Information
/// Get scrollbar information from a layout node.
///
/// Scrollbar requirements are computed during the layout phase in two paths:
/// - BFC layout: `compute_scrollbar_info()` in cache.rs
/// - Taffy layout: set in the measure callback in `taffy_bridge.rs`
///
/// If neither path set `scrollbar_info`, the node genuinely does not need
/// scrollbars. The previous heuristic (>3 children = force overflow) caused
/// false-positive scrollbars on normal containers.
2
#[must_use] pub fn get_scrollbar_info_from_layout(node: &LayoutNode) -> ScrollbarRequirements {
2
    node.scrollbar_info.unwrap_or_default()
2
}
/// Resolve the **layout-effective** scrollbar width for a node, in pixels.
///
/// This combines three inputs:
/// 1. CSS `scrollbar-width` property on the node (`auto` → 16, `thin` → 8, `none` → 0)
/// 2. OS-level `ScrollbarPreferences.visibility` (overlay scrollbars → 0 layout reservation)
/// 3. Custom `-azul-scrollbar-style` width override
///
/// For **overlay** scrollbars (macOS `WhenScrolling`, or equivalent), this returns `0.0`
/// because overlay scrollbars are painted on top of content and do not consume layout space.
/// The scrollbar is still *rendered*, but no space is reserved during layout.
// +spec:overflow:b83014 - overlay scrollbars do not create scrollbar gutters
///
/// During display-list generation, use `get_scrollbar_style()` instead — that returns
/// the full visual style including the *paint* width (which may be non-zero for overlay).
123
pub fn get_layout_scrollbar_width_px<T: ParsedFontTrait>(
123
    ctx: &crate::solver3::LayoutContext<'_, T>,
123
    dom_id: NodeId,
123
    styled_node_state: &StyledNodeState,
123
) -> f32 {
    // Resolve the full scrollbar style (includes per-node CSS overrides + system style).
    // `reserve_width_px` already accounts for overlay vs legacy:
    //   overlay (WhenScrolling) → 0.0
    //   legacy (Always)         → visual_width_px
123
    let style = get_scrollbar_style(
123
        ctx.styled_dom,
123
        dom_id,
123
        styled_node_state,
123
        ctx.system_style.as_deref(),
    );
123
    style.reserve_width_px
123
}
get_css_property!(
    get_display_property_internal,
    get_display,
    LayoutDisplay,
    CssPropertyType::Display,
    compact = get_display
);
5729569
#[must_use] pub fn get_display_property(
5729569
    styled_dom: &StyledDom,
5729569
    dom_id: Option<NodeId>,
5729569
) -> MultiValue<LayoutDisplay> {
5729569
    let Some(id) = dom_id else {
121
        return MultiValue::Exact(LayoutDisplay::Inline);
    };
5729448
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
5729448
    get_display_property_internal(styled_dom, id, node_state)
5729569
}
/// CSS Display Module Level 3: Blockification of display values.
///
/// When an element is floated, absolutely positioned, or is the root element,
/// its computed display value may be "blockified" per the table in CSS Display 3 §2.7.
/// This function returns the blockified display value without mutating any state.
#[allow(clippy::match_same_arms)] // enum/value mapping/dispatch table: one arm per input variant (or cross-type bindings that can't merge)
113759
#[must_use] pub const fn blockify_display(raw_display: LayoutDisplay) -> LayoutDisplay {
113759
    match raw_display {
        // Inline-level display types become their block-level equivalents
5055
        LayoutDisplay::Inline => LayoutDisplay::Block,
        // Per CSS Display 3 §2.7: inline-block blockifies to block
        // (for legacy reasons, loses its flow-root nature)
13966
        LayoutDisplay::InlineBlock => LayoutDisplay::Block,
32
        LayoutDisplay::InlineFlex => LayoutDisplay::Flex,
32
        LayoutDisplay::InlineTable => LayoutDisplay::Table,
32
        LayoutDisplay::InlineGrid => LayoutDisplay::Grid,
        // CSS 2.2 §9.7: table-internal display values blockify to block
        // for absolutely positioned, floated, or root elements
        LayoutDisplay::TableRowGroup
        | LayoutDisplay::TableColumn
        | LayoutDisplay::TableColumnGroup
        | LayoutDisplay::TableHeaderGroup
        | LayoutDisplay::TableFooterGroup
        | LayoutDisplay::TableRow
        | LayoutDisplay::TableCell
259
        | LayoutDisplay::TableCaption => LayoutDisplay::Block,
        // css-display-3 §2.7: run-in blockifies to block.
32
        LayoutDisplay::RunIn => LayoutDisplay::Block,
        // Already block-level types are unchanged. NOTE: `display: contents`
        // on the ROOT element must compute to `block` (css-display-3 §2.5) -
        // that special case is the CALLER's to apply, since only it knows
        // whether the node is the root; for non-root elements `contents`
        // must pass through untouched.
94351
        other => other,
    }
113759
}
// +spec:positioning:c31c24 - blockification is a computed-value change for absolute/float/root elements
/// Resolves the computed display value for an element, applying blockification
/// rules per CSS Display Module Level 3 §2.7.
// +spec:display-property:641ac5 - computed display value applies blockification/inlinification (not "as specified")
///
/// This centralizes the blockification decision so that all layout phases
/// (`layout_tree`, sizing, positioning) use consistent display values.
// +spec:floats:52aea6 - computed display blockified for floated/positioned/root elements
// +spec:positioning:ce02a1 - out-of-flow boxes (floated or absolutely positioned) get blockified display
// four independent layout-state flags drive the blockification decision; bundling them
// into a struct would add ceremony without clarifying this pure decision function.
#[allow(clippy::fn_params_excessive_bools)]
181944
#[must_use] pub fn get_computed_display(
181944
    raw_display: LayoutDisplay,
181944
    is_absolute_or_fixed: bool,
181944
    is_floated: bool,
181944
    is_root: bool,
181944
    is_flex_grid_child: bool,
181944
) -> LayoutDisplay {
181944
    if raw_display == LayoutDisplay::None {
17
        return LayoutDisplay::None;
181927
    }
    // +spec:positioning:69468c - absolute/fixed blockifies the box
181927
    if is_absolute_or_fixed || is_floated || is_root || is_flex_grid_child {
113360
        blockify_display(raw_display)
    } else {
68567
        raw_display
    }
181944
}
// +spec:font-metrics:f7affa - vertical-align shorthand: maps CSS vertical-align values to inline layout alignment
/// Reads the CSS `vertical-align` property for a DOM node and converts it to
/// the text3 `VerticalAlign` enum used during inline layout.
// +spec:display-property:24c160 - vertical-align aligns inline-level box within the line
55737
#[must_use] pub fn get_vertical_align_for_node(
55737
    styled_dom: &StyledDom,
55737
    dom_id: NodeId,
55737
) -> crate::text3::cache::VerticalAlign {
55737
    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
55737
    let va = match get_vertical_align_property(styled_dom, dom_id, node_state) {
18
        MultiValue::Exact(v) => v,
55719
        _ => StyleVerticalAlign::default(),
    };
55737
    match va {
55719
        StyleVerticalAlign::Baseline => crate::text3::cache::VerticalAlign::Baseline,
        StyleVerticalAlign::Top => crate::text3::cache::VerticalAlign::Top,
        StyleVerticalAlign::Middle => crate::text3::cache::VerticalAlign::Middle,
        StyleVerticalAlign::Bottom => crate::text3::cache::VerticalAlign::Bottom,
9
        StyleVerticalAlign::Sub => crate::text3::cache::VerticalAlign::Sub,
9
        StyleVerticalAlign::Superscript => crate::text3::cache::VerticalAlign::Super,
        StyleVerticalAlign::TextTop => crate::text3::cache::VerticalAlign::TextTop,
        StyleVerticalAlign::TextBottom => crate::text3::cache::VerticalAlign::TextBottom,
        // +spec:line-height:b41ee3 - percentage vertical-align: raise/lower by % of line-height, 0% = baseline
        StyleVerticalAlign::Percentage(p) => {
            let font_size = get_element_font_size(styled_dom, dom_id, node_state);
            // Line-height uses the parser convention (see `get_line_height_value` /
            // the LineHeight::Px path): a NEGATIVE normalized value is an absolute
            // px length, a positive one is a unitless multiple of font-size. The
            // old `normalized() * font_size` scaled (and sign-flipped) absolute
            // line-heights — e.g. `line-height: 30px` + `vertical-align: 50%` gave
            // -240px instead of +15px.
            let line_height = get_line_height_value(styled_dom, dom_id, node_state)
                .map_or(font_size * 1.2, |lh| {
                    let n = lh.inner.normalized();
                    if n < 0.0 { -n } else { n * font_size }
                });
            crate::text3::cache::VerticalAlign::Offset(p.normalized() * line_height)
        }
        // §10.8.1: <length> is absolute offset from baseline
        StyleVerticalAlign::Length(l) => {
            let font_size = get_element_font_size(styled_dom, dom_id, node_state);
            // TODO(superplan): viewport units (vw/vh/...) in a vertical-align <length>
            // fall back to raw pixels here because this getter has no viewport ctx.
            // Threading `viewport_size` requires changing this fn's signature, but one
            // of its callers (`sizing.rs::process_layout_children`) lives outside
            // Group 2's file ownership — deferred. (The sibling path in
            // fc.rs::translate_to_text3_constraints already resolves it via
            // `resolve_pixel_value_with_viewport`.)
            let px = super::calc::resolve_pixel_value(&l, 0.0, font_size, font_size);
            crate::text3::cache::VerticalAlign::Offset(px)
        }
    }
55737
}
/// Per-document memo for [`get_style_properties`], owned by
/// [`crate::solver3::LayoutContext`].
///
/// Keyed on everything that can change the answer for a node: its index,
/// its `StyledNodeState` (`:hover`/`:focus`/`:disabled` select different
/// declarations) and the viewport (`vw`/`vh` and percentages resolve
/// against it). The DOCUMENT is not part of the key because the cache
/// belongs to one — see the field docs for why that matters.
/// Per-document memo for [`get_style_properties_cached`].
///
/// TWO maps on purpose. `by_node` answers "what is this node's style?" and
/// `by_value` answers "has anyone already built this exact style?".
///
/// With only the first, every node got its OWN `Arc` even when the computed
/// style was byte-identical — sharing by PRODUCER IDENTITY rather than by
/// RESULT VALUE. Measured before this: 672 distinct `Arc<StyleProperties>`
/// backing 31,086 glyphs on one markdown document, where the document has
/// on the order of ten distinct text styles. Stylo shipped the same defect
/// (109k `ComputedValues` where 2,200 were expected) and it is invisible
/// without counting the pointers, which is what `AZ_PROFILE=memory` now
/// does.
#[derive(Default, Debug)]
pub struct StyleCache {
    by_node: HashMap<
        (u32, StyledNodeState, u32, u32),
        std::sync::Arc<StyleProperties>,
    >,
    by_value: HashMap<u64, Vec<std::sync::Arc<StyleProperties>>>,
}
/// [`get_style_properties`], memoised in a caller-owned per-document cache
/// and shared behind an `Arc`.
///
/// The cache is passed separately from `styled_dom` rather than taking the
/// whole `LayoutContext`, so callers can hand over two disjoint fields of
/// the same context without a borrow conflict.
#[must_use]
140400
pub fn get_style_properties_cached(
140400
    cache: &mut StyleCache,
140400
    styled_dom: &StyledDom,
140400
    dom_id: NodeId,
140400
    system_style: Option<&std::sync::Arc<azul_css::system::SystemStyle>>,
140400
    viewport_size: PhysicalSize,
140400
) -> std::sync::Arc<StyleProperties> {
140400
    let node_state = styled_dom
140400
        .styled_nodes
140400
        .as_container()
140400
        .get(dom_id)
140400
        .map(|n| n.styled_node_state)
140400
        .unwrap_or_default();
140400
    let key = (
140400
        dom_id.index() as u32,
140400
        node_state,
140400
        viewport_size.width.to_bits(),
140400
        viewport_size.height.to_bits(),
140400
    );
140400
    if let Some(v) = cache.by_node.get(&key) {
87185
        drop(crate::probe::Probe::span("style_props_memo_hit"));
87185
        return std::sync::Arc::clone(v);
53215
    }
53215
    let _p = crate::probe::Probe::span("style_props_build");
53215
    let built = get_style_properties(styled_dom, dom_id, system_style, viewport_size);
    // Share by VALUE. Nodes that resolve to the same computed style get the
    // same allocation, so a thousand paragraphs cost one StyleProperties
    // rather than a thousand.
    //
    // The bucket is a Vec and the match is a real `==`, never the hash
    // alone: a 64-bit collision would otherwise hand two DIFFERENT styles
    // the same allocation and silently repaint text in the wrong font. The
    // Vec is length 1 in every non-colliding case.
53215
    let value_hash = {
        use core::hash::{Hash, Hasher};
53215
        let mut h = std::collections::hash_map::DefaultHasher::new();
53215
        built.hash(&mut h);
53215
        h.finish()
    };
53215
    let bucket = cache.by_value.entry(value_hash).or_default();
53215
    let shared = if let Some(existing) = bucket.iter().find(|existing| ***existing == built) {
42319
        drop(crate::probe::Probe::span("style_props_value_shared"));
42319
        std::sync::Arc::clone(existing)
    } else {
10896
        let fresh = std::sync::Arc::new(built);
10896
        bucket.push(std::sync::Arc::clone(&fresh));
10896
        fresh
    };
53215
    cache.by_node.insert(key, std::sync::Arc::clone(&shared));
53215
    shared
140400
}
#[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
/// # Panics
///
/// Panics only on an internal indexing invariant (an in-range `get().unwrap()` over the font-family list).
54953
pub fn get_style_properties(
54953
    styled_dom: &StyledDom,
54953
    dom_id: NodeId,
54953
    system_style: Option<&std::sync::Arc<azul_css::system::SystemStyle>>,
54953
    viewport_size: PhysicalSize,
54953
) -> StyleProperties {
    use azul_css::props::basic::{PhysicalSize, PropertyContext, ResolutionContext};
54953
    let node_data = &styled_dom.node_data.as_container()[dom_id];
54953
    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
54953
    let cache = &styled_dom.css_property_cache.ptr;
    // Fast path: use compact cache reverse map (works for inherited values on text nodes).
    // Slow path: only for non-normal pseudo states (:hover, :focus, etc.)
54953
    let font_families = if node_state.is_normal() {
54953
        cache
54953
            .compact_cache
54953
            .as_ref()
54953
            .and_then(|cc| {
54953
                let fh = cc.tier2b_text[dom_id.index()].font_family_hash;
54953
                if fh == 0 {
12541
                    return None;
42412
                }
42412
                cc.font_hash_to_families.get(&fh).cloned()
54953
            })
54953
            .unwrap_or_else(|| {
12541
                StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
12541
            })
    } else {
        cache
            .get_font_family(node_data, &dom_id, node_state)
            .and_then(|v| v.get_property().cloned())
            .unwrap_or_else(|| {
                StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
            })
    };
    // Get parent's font-size for proper em resolution in font-size property.
    // FAST PATH: `get_parent_font_size` goes through `get_element_font_size`
    // which hits the memoised `resolved_font_sizes_px` Vec (O(1) array index).
    // The old code here walked the full CSS cascade for every call — 1485
    // slow walks per cold excel.html layout. Replaced 2026-04-17.
54953
    let parent_font_size = get_parent_font_size(styled_dom, dom_id, node_state);
54953
    let root_font_size = get_root_font_size(styled_dom, node_state);
    // Create resolution context for font-size (em refers to parent)
54953
    let font_size_context = ResolutionContext {
54953
        vertical_writing_mode: false,
54953
        element_font_size: DEFAULT_FONT_SIZE, /* Not used for font-size property */
54953
        parent_font_size,
54953
        root_font_size,
54953
        containing_block_size: PhysicalSize::new(0.0, 0.0),
54953
        element_size: None,
54953
        viewport_size,
54953
    };
    // Get font-size: either from this node's CSS, or inherit from parent
    // font-size is an inheritable property, so if the node doesn't have
    // an explicit font-size, it should inherit from the parent (not default to 16px)
54953
    let font_size = {
        // FAST PATH: compact cache for normal state.
        // Sentinel/inherit/initial → inherit from parent directly (which is
        // what the slow cascade walk would fall back to via `.unwrap_or(parent_font_size)`
        // anyway — avoid the walk entirely).
54953
        let mut fast_font_size: Option<f32> = None;
54953
        let mut compact_said_inherit = false;
54953
        if node_state.is_normal() {
54953
            if let Some(ref cc) = cache.compact_cache {
54953
                let raw = cc.get_font_size_raw(dom_id.index());
54953
                if raw == azul_css::compact_cache::U32_SENTINEL
54953
                    || raw == azul_css::compact_cache::U32_INHERIT
54953
                    || raw == azul_css::compact_cache::U32_INITIAL
7181
                {
7181
                    compact_said_inherit = true;
47772
                } else if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
47772
                    fast_font_size = Some(
47772
                        pv.resolve_with_context(&font_size_context, PropertyContext::FontSize),
47772
                    );
47772
                }
            }
        }
54953
        fast_font_size.unwrap_or_else(|| {
7181
            if compact_said_inherit {
7181
                parent_font_size
            } else {
                cache
                    .get_font_size(node_data, &dom_id, node_state)
                    .and_then(|v| v.get_property().copied())
                    .map_or(parent_font_size, |v| {
                        v.inner
                            .resolve_with_context(&font_size_context, PropertyContext::FontSize)
                    })
            }
7181
        })
    };
54953
    let color_from_cache = {
        // FAST PATH: compact cache for text color
54953
        let mut fast_color = None;
54953
        if node_state.is_normal() {
54953
            if let Some(ref cc) = cache.compact_cache {
54953
                let raw = cc.get_text_color_raw(dom_id.index());
54953
                if raw != 0 {
35942
                    // Decode 0xRRGGBBAA → ColorU
35942
                    fast_color = Some(ColorU {
35942
                        r: (raw >> 24) as u8,
35942
                        g: (raw >> 16) as u8,
35942
                        b: (raw >> 8) as u8,
35942
                        a: raw as u8,
35942
                    });
35957
                }
            }
        }
54953
        fast_color.or_else(|| {
19011
            cache
19011
                .get_text_color(node_data, &dom_id, node_state)
19011
                .and_then(|v| v.get_property().copied())
19011
                .map(|v| v.inner)
19011
        })
    };
    // CSS initial value for 'color' is UA-dependent but conventionally black.
    // Do NOT use system_style.colors.text here — that reflects the OS theme
    // (e.g. white on macOS dark mode) and would produce white text on
    // explicitly light-colored backgrounds.  System colors (CanvasText etc.)
    // should only be used when referenced through CSS system-color keywords.
54953
    let color = color_from_cache.unwrap_or(ColorU::BLACK);
    // +spec:font-metrics:e480da - line-height: normal/number/length/percentage resolution
54953
    let line_height = {
        // FAST PATH: compact cache for line-height (stored as normalized × 1000 i16).
        // When the cache returns Some → we have a resolved value.
        // When it returns None AND node_state is normal → the compact cache stored
        // the sentinel, which means "line-height: normal" (the spec default).
        // Previously we fell through to a cascade walk here — but the default
        // has already been authoritatively decided by the builder, so the walk
        // would only ever re-confirm "no value, normal". 1600 pure-waste walks
        // per cold excel.html layout. Short-circuit to Normal directly.
54953
        let mut fast_lh = None;
54953
        let mut sentinel_normal = false;
54953
        if node_state.is_normal() {
54953
            if let Some(ref cc) = cache.compact_cache {
54953
                if let Some(decoded) = cc.get_line_height(dom_id.index()) {
                    // get_line_height returns stored/10. The builder's split
                    // scale (see core/src/compact.rs): NEGATIVE = absolute px
                    // stored as -px x 10, so decoded == -px directly;
                    // positive = multiple x 1000, so decoded == multiple x 100.
                    fast_lh = Some(crate::text3::cache::LineHeight::Px(
4176
                        if decoded < 0.0 {
783
                            -decoded
                        } else {
3393
                            (decoded / 100.0) * font_size
                        },
                    ));
50777
                } else {
50777
                    // Sentinel in compact cache = "normal" (CSS default).
50777
                    sentinel_normal = true;
50777
                }
            }
        }
54953
        if sentinel_normal {
50777
            crate::text3::cache::LineHeight::Normal
        } else {
4176
            fast_lh.unwrap_or_else(|| {
                cache
                    .get_line_height(node_data, &dom_id, node_state)
                    .and_then(|v| v.get_property().copied())
                    .map_or(crate::text3::cache::LineHeight::Normal, |v| {
                        // Negative normalized() = absolute px value (parser convention
                        // for "50px" etc.); positive = multiple of font-size.
                        let n = v.inner.normalized();
                        crate::text3::cache::LineHeight::Px(if n < 0.0 { -n } else { n * font_size })
                    })
            })
        }
    };
    // Get background color for INLINE elements only
    // CSS background-color is NOT inherited. For block-level elements (th, td, div, etc.),
    // the background is painted separately by paint_element_background() in display_list.rs.
    // Only inline elements (span, em, strong, a, etc.) should have their background color
    // propagated through StyleProperties for the text rendering pipeline.
    //
    // FAST PATH: use the compact-cache-backed display getter. The old code
    // here called `cache.get_display(..)` (the 3-arg convenience method on
    // CssPropertyCache) which routes through `get_property_slow` — 1485 slow
    // walks per cold excel.html layout. Replaced 2026-04-17.
54953
    let display = match get_display_property(styled_dom, Some(dom_id)) {
54953
        MultiValue::Exact(v) => v,
        _ => LayoutDisplay::Inline,
    };
    // For inline and inline-block elements, get background content and border info
    // Block elements have their backgrounds/borders painted by display_list.rs
54953
    let (background_color, background_content, border) =
54953
        if matches!(display, LayoutDisplay::Inline | LayoutDisplay::InlineBlock) {
54510
            let bg = get_background_color(styled_dom, dom_id, node_state);
54510
            let bg_color = if bg.a > 0 { Some(bg) } else { None };
            // Get full background contents (including gradients)
54510
            let bg_contents = get_background_contents(styled_dom, dom_id, node_state);
            // Get border info for inline elements
54510
            let border_info = get_border_info(styled_dom, dom_id, node_state);
54510
            let inline_border =
54510
                get_inline_border_info(styled_dom, dom_id, node_state, &border_info, viewport_size);
54510
            (bg_color, bg_contents, inline_border)
        } else {
            // Block-level elements: background/border is painted by display_list.rs
            // via push_backgrounds_and_border() in DisplayListBuilder
443
            (None, Vec::new(), None)
        };
    // Query font-weight from CSS cache
54953
    let font_weight = match get_font_weight_property(styled_dom, dom_id, node_state) {
54953
        MultiValue::Exact(v) => v,
        _ => StyleFontWeight::Normal,
    };
    // Query font-style from CSS cache
54953
    let font_style = match get_font_style_property(styled_dom, dom_id, node_state) {
54953
        MultiValue::Exact(v) => v,
        _ => StyleFontStyle::Normal,
    };
    // Convert StyleFontWeight/StyleFontStyle to fontconfig types
54953
    let fc_weight = super::fc::convert_font_weight(font_weight);
54953
    let fc_style = super::fc::convert_font_style(font_style);
    // Check if any font family is a FontRef - if so, use FontStack::Ref
    // This allows embedded fonts (like Material Icons) to bypass fontconfig
54953
    let font_stack = {
58392
        let font_ref = (0..font_families.len()).find_map(|i| match font_families.get(i).unwrap() {
9
            StyleFontFamily::Ref(r) => Some(r.clone()),
58383
            _ => None,
58392
        });
54953
        font_ref.map_or_else(
54944
            || {
                // Get platform for resolving system font types. None on the paged /
                // PDF layout path (system_style is hard-coded None there);
                // build_font_selector_stack then resolves via Platform::current() so
                // the names stay in lock-step with the font-loading pass.
54944
                let platform = system_style.map(|ss| &ss.platform);
54944
                FontStack::Stack(build_font_selector_stack_memo(
54944
                    &font_families,
54944
                    platform,
54944
                    fc_weight,
54944
                    fc_style,
54944
                ))
54944
            },
            FontStack::Ref,
        )
    };
    // Get letter-spacing from CSS
54953
    let letter_spacing = {
        // FAST PATH: compact cache for letter-spacing (i16 resolved px × 10)
54953
        let mut fast_ls = None;
54953
        if node_state.is_normal() {
54953
            if let Some(ref cc) = cache.compact_cache {
54953
                if let Some(px_val) = cc.get_letter_spacing(dom_id.index()) {
54953
                    fast_ls = Some(crate::text3::cache::Spacing::PxF(px_val));
54953
                }
            }
        }
54953
        fast_ls.unwrap_or_else(|| {
            cache
                .get_letter_spacing(node_data, &dom_id, node_state)
                .and_then(|v| v.get_property().copied())
                .map(|v| {
                    let px_value = v
                        .inner
                        .resolve_with_context(&font_size_context, PropertyContext::FontSize);
                    crate::text3::cache::Spacing::PxF(px_value)
                })
                .unwrap_or_default()
        })
    };
    // Get word-spacing from CSS
54953
    let word_spacing = {
        // FAST PATH: compact cache for word-spacing (i16 resolved px × 10)
54953
        let mut fast_ws = None;
54953
        if node_state.is_normal() {
54953
            if let Some(ref cc) = cache.compact_cache {
54953
                if let Some(px_val) = cc.get_word_spacing(dom_id.index()) {
54953
                    fast_ws = Some(crate::text3::cache::Spacing::PxF(px_val));
54953
                }
            }
        }
54953
        fast_ws.unwrap_or_else(|| {
            cache
                .get_word_spacing(node_data, &dom_id, node_state)
                .and_then(|v| v.get_property().copied())
                .map(|v| {
                    let px_value = v
                        .inner
                        .resolve_with_context(&font_size_context, PropertyContext::FontSize);
                    crate::text3::cache::Spacing::PxF(px_value)
                })
                .unwrap_or_default()
        })
    };
    // Get text-decoration from CSS.
    //
    // Fast path: the compact cache keeps a `has_text_decoration` flag. If
    // unset (the overwhelmingly common case — plain body text has no
    // decoration set), skip the 4-pseudo-state × 6-layer cascade walk
    // entirely. Only nodes that actually set text-decoration pay the walk.
54953
    let text_decoration = {
54953
        let mut skip_walk = false;
54953
        if node_state.is_normal() {
54953
            if let Some(ref cc) = cache.compact_cache {
54953
                if !cc.has_text_decoration(dom_id.index()) {
54575
                    skip_walk = true;
54575
                }
            }
        }
54953
        if skip_walk {
54575
            crate::text3::cache::TextDecoration::default()
        } else {
378
            cache
378
                .get_text_decoration(node_data, &dom_id, node_state)
378
                .and_then(|v| v.get_property().copied())
378
                .map(crate::text3::cache::TextDecoration::from_css)
378
                .unwrap_or_default()
        }
    };
    // Get tab-size (tab-size) from CSS.
    //
    // tab-size defaults to `I16_SENTINEL` in the compact cache builder
    // (spec default is "8", meaning 8 space widths). The old fallback
    // called `cache.get_tab_size(..)` (slow cascade) for every node whose
    // raw was SENTINEL — virtually every node, because almost nothing sets
    // tab-size. That was 1485 pure-waste slow walks per cold layout.
    //
    // New behaviour: sentinel → 8.0 directly. Only walk the cascade when
    // the compact cache is genuinely unavailable (no `compact_cache`) or
    // the node is in a pseudo-state that bypassed the cache.
54953
    let tab_size = {
54953
        let mut fast_tab = None;
54953
        if node_state.is_normal() {
54953
            if let Some(ref cc) = cache.compact_cache {
54953
                let raw = cc.get_tab_size_raw(dom_id.index());
54953
                if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
                    fast_tab = Some(f32::from(raw) / 10.0);
54953
                } else {
54953
                    // Sentinel / Inherit / Initial → spec default is 8.
54953
                    fast_tab = Some(8.0);
54953
                }
            }
        }
54953
        fast_tab.unwrap_or_else(|| {
            cache
                .get_tab_size(node_data, &dom_id, node_state)
                .and_then(|v| v.get_property().copied())
                .map_or(DEFAULT_TAB_SIZE, |v| v.inner.number.get())
        })
    };
    // Get text-transform from CSS (uppercase / lowercase / capitalize / full-width).
    // Applied to the run text before shaping (fc.rs::apply_text_transform) so that
    // intrinsic widths reflect the transformed glyphs.
54953
    let text_transform = cache
54953
        .get_text_transform(node_data, &dom_id, node_state)
54953
        .and_then(|v| v.get_property().copied())
54953
        .map(|t| {
            use azul_css::props::style::text::StyleTextTransform as Css;
            use crate::text3::cache::TextTransform as T3;
18
            match t {
                Css::None => T3::None,
9
                Css::Uppercase => T3::Uppercase,
9
                Css::Lowercase => T3::Lowercase,
                Css::Capitalize => T3::Capitalize,
                Css::FullWidth => T3::FullWidth,
            }
18
        })
54953
        .unwrap_or_default();
54953
    StyleProperties {
54953
        font_stack,
54953
        font_size_px: font_size,
54953
        color,
54953
        background_color,
54953
        background_content,
54953
        border,
54953
        line_height,
54953
        letter_spacing,
54953
        word_spacing,
54953
        text_decoration,
54953
        tab_size,
54953
        text_transform,
54953
        // Per-run vertical-align so a `<span style="vertical-align:super/sub">` shifts
54953
        // its text clusters (get_item_vertical_align reads this). Without it every text
54953
        // cluster fell back to the IFC root's alignment (baseline), so sub/super/length
54953
        // vertical-align on inline spans had no effect.
54953
        vertical_align: get_vertical_align_for_node(styled_dom, dom_id),
54953
        // These still use defaults - could be extended in future:
54953
        // font_features, font_variations, writing_mode,
54953
        // text_orientation, text_combine_upright, font_variant_*
54953
        ..Default::default()
54953
    }
54953
}
865
#[must_use] pub fn get_list_style_type(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> StyleListStyleType {
865
    let Some(id) = dom_id else {
1
        return StyleListStyleType::default();
    };
864
    let node_data = &styled_dom.node_data.as_container()[id];
864
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
864
    styled_dom
864
        .css_property_cache
864
        .ptr
864
        .get_list_style_type(node_data, &id, node_state)
864
        .and_then(|v| v.get_property().copied())
864
        .unwrap_or_default()
865
}
433
#[must_use] pub fn get_list_style_position(
433
    styled_dom: &StyledDom,
433
    dom_id: Option<NodeId>,
433
) -> StyleListStylePosition {
433
    let Some(id) = dom_id else {
1
        return StyleListStylePosition::default();
    };
432
    let node_data = &styled_dom.node_data.as_container()[id];
432
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
432
    styled_dom
432
        .css_property_cache
432
        .ptr
432
        .get_list_style_position(node_data, &id, node_state)
432
        .and_then(|v| v.get_property().copied())
432
        .unwrap_or_default()
433
}
// New: Taffy Bridge Getters - Box Model Properties with Ua Css Fallback
use azul_css::props::layout::{
    LayoutInsetBottom, LayoutLeft, LayoutMarginBottom, LayoutMarginLeft, LayoutMarginRight,
    LayoutMarginTop, LayoutMaxHeight, LayoutMaxWidth, LayoutMinHeight, LayoutMinWidth,
    LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutPaddingTop, LayoutRight,
    LayoutTop,
};
/// Get inset (position) properties - returns MultiValue<PixelValue>
get_css_property_pixel!(
    get_css_left,
    get_left,
    CssPropertyType::Left,
    compact_i16 = get_left
);
get_css_property_pixel!(
    get_css_right,
    get_right,
    CssPropertyType::Right,
    compact_i16 = get_right
);
get_css_property_pixel!(
    get_css_top,
    get_top,
    CssPropertyType::Top,
    compact_i16 = get_top
);
get_css_property_pixel!(
    get_css_bottom,
    get_bottom,
    CssPropertyType::Bottom,
    compact_i16 = get_bottom
);
/// Get margin properties - returns MultiValue<PixelValue>
get_css_property_pixel!(
    get_css_margin_left,
    get_margin_left,
    CssPropertyType::MarginLeft,
    compact_i16 = get_margin_left_raw
);
get_css_property_pixel!(
    get_css_margin_right,
    get_margin_right,
    CssPropertyType::MarginRight,
    compact_i16 = get_margin_right_raw
);
get_css_property_pixel!(
    get_css_margin_top,
    get_margin_top,
    CssPropertyType::MarginTop,
    compact_i16 = get_margin_top_raw
);
get_css_property_pixel!(
    get_css_margin_bottom,
    get_margin_bottom,
    CssPropertyType::MarginBottom,
    compact_i16 = get_margin_bottom_raw
);
/// Get padding properties - returns MultiValue<PixelValue>
get_css_property_pixel!(
    get_css_padding_left,
    get_padding_left,
    CssPropertyType::PaddingLeft,
    compact_i16 = get_padding_left_raw
);
get_css_property_pixel!(
    get_css_padding_right,
    get_padding_right,
    CssPropertyType::PaddingRight,
    compact_i16 = get_padding_right_raw
);
get_css_property_pixel!(
    get_css_padding_top,
    get_padding_top,
    CssPropertyType::PaddingTop,
    compact_i16 = get_padding_top_raw
);
get_css_property_pixel!(
    get_css_padding_bottom,
    get_padding_bottom,
    CssPropertyType::PaddingBottom,
    compact_i16 = get_padding_bottom_raw
);
/// Get min/max size properties
get_css_property!(
    get_css_min_width,
    get_min_width,
    LayoutMinWidth,
    CssPropertyType::MinWidth,
    compact_u32_struct = get_min_width_raw
);
get_css_property!(
    get_css_min_height,
    get_min_height,
    LayoutMinHeight,
    CssPropertyType::MinHeight,
    compact_u32_struct = get_min_height_raw
);
get_css_property!(
    get_css_max_width,
    get_max_width,
    LayoutMaxWidth,
    CssPropertyType::MaxWidth,
    compact_u32_struct = get_max_width_raw
);
get_css_property!(
    get_css_max_height,
    get_max_height,
    LayoutMaxHeight,
    CssPropertyType::MaxHeight,
    compact_u32_struct = get_max_height_raw
);
/// Get border width properties (no UA CSS fallback needed, defaults to 0)
get_css_property_pixel!(
    get_css_border_left_width,
    get_border_left_width,
    CssPropertyType::BorderLeftWidth,
    compact_i16 = get_border_left_width_raw
);
get_css_property_pixel!(
    get_css_border_right_width,
    get_border_right_width,
    CssPropertyType::BorderRightWidth,
    compact_i16 = get_border_right_width_raw
);
get_css_property_pixel!(
    get_css_border_top_width,
    get_border_top_width,
    CssPropertyType::BorderTopWidth,
    compact_i16 = get_border_top_width_raw
);
get_css_property_pixel!(
    get_css_border_bottom_width,
    get_border_bottom_width,
    CssPropertyType::BorderBottomWidth,
    compact_i16 = get_border_bottom_width_raw
);
// Fragmentation (page breaking) properties
/// Get break-before property for paged media
662668
#[must_use] pub fn get_break_before(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> PageBreak {
662668
    let Some(id) = dom_id else {
1
        return PageBreak::Auto;
    };
662667
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
    // UA fallback: node types with intrinsic break behavior (the canonical
    // `<pagebreak/>` element carries UA `break-before: page`). The author
    // bitset below knows nothing about UA properties, so this must be the
    // miss path on BOTH branches, not `Auto`.
662667
    let ua_fallback = |styled_dom: &StyledDom| -> PageBreak {
662658
        let node_data = &styled_dom.node_data.as_container()[id];
662658
        azul_core::ua_css::get_ua_property(
662658
            node_data.get_node_type(),
662658
            CssPropertyType::BreakBefore,
        )
662658
        .and_then(|p| {
54
            if let CssProperty::BreakBefore(v) = p {
54
                v.get_property().copied()
            } else {
                None
            }
54
        })
662658
        .unwrap_or(PageBreak::Auto)
662658
    };
    // Negative fast path: break-* is almost never declared by authors.
662667
    if node_state.is_normal() {
662662
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
662661
            if !cc.has_break(id.index()) {
662652
                return ua_fallback(styled_dom);
9
            }
1
        }
5
    }
15
    let node_data = &styled_dom.node_data.as_container()[id];
15
    styled_dom
15
        .css_property_cache
15
        .ptr
15
        .get_break_before(node_data, &id, node_state)
15
        .and_then(|v| v.get_property().copied())
15
        .unwrap_or_else(|| ua_fallback(styled_dom))
662668
}
/// Get break-after property for paged media
662326
#[must_use] pub fn get_break_after(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> PageBreak {
662326
    let Some(id) = dom_id else {
1
        return PageBreak::Auto;
    };
662325
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
662325
    if node_state.is_normal() {
662320
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
662319
            if !cc.has_break(id.index()) {
662310
                return PageBreak::Auto;
9
            }
1
        }
5
    }
15
    let node_data = &styled_dom.node_data.as_container()[id];
15
    styled_dom
15
        .css_property_cache
15
        .ptr
15
        .get_break_after(node_data, &id, node_state)
15
        .and_then(|v| v.get_property().copied())
15
        .unwrap_or(PageBreak::Auto)
662326
}
/// Check if a `PageBreak` value forces a page break (always, page, left, right, etc.)
1324677
#[must_use] pub const fn is_forced_page_break(page_break: PageBreak) -> bool {
1324609
    matches!(
1324677
        page_break,
        PageBreak::Always
            | PageBreak::Page
            | PageBreak::Left
            | PageBreak::Right
            | PageBreak::Recto
            | PageBreak::Verso
            | PageBreak::All
    )
1324677
}
/// Get break-inside property for paged media
7
#[must_use] pub fn get_break_inside(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> BreakInside {
7
    let Some(id) = dom_id else {
1
        return BreakInside::Auto;
    };
6
    let node_data = &styled_dom.node_data.as_container()[id];
6
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
6
    styled_dom
6
        .css_property_cache
6
        .ptr
6
        .get_break_inside(node_data, &id, node_state)
6
        .and_then(|v| v.get_property().copied())
6
        .unwrap_or(BreakInside::Auto)
7
}
/// Get orphans property (minimum lines at bottom of page)
3
#[must_use] pub fn get_orphans(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> u32 {
3
    let Some(id) = dom_id else {
1
        return 2; // Default value
    };
2
    let node_data = &styled_dom.node_data.as_container()[id];
2
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_orphans(node_data, &id, node_state)
2
        .and_then(|v| v.get_property().copied())
2
        .map_or(2, |o| o.inner)
3
}
/// Get widows property (minimum lines at top of page)
3
#[must_use] pub fn get_widows(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> u32 {
3
    let Some(id) = dom_id else {
1
        return 2; // Default value
    };
2
    let node_data = &styled_dom.node_data.as_container()[id];
2
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_widows(node_data, &id, node_state)
2
        .and_then(|v| v.get_property().copied())
2
        .map_or(2, |w| w.inner)
3
}
/// Get box-decoration-break property
1
#[must_use] pub fn get_box_decoration_break(
1
    styled_dom: &StyledDom,
1
    dom_id: Option<NodeId>,
1
) -> BoxDecorationBreak {
1
    let Some(id) = dom_id else {
1
        return BoxDecorationBreak::Slice;
    };
    let node_data = &styled_dom.node_data.as_container()[id];
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
    styled_dom
        .css_property_cache
        .ptr
        .get_box_decoration_break(node_data, &id, node_state)
        .and_then(|v| v.get_property().copied())
        .unwrap_or(BoxDecorationBreak::Slice)
1
}
// Helper functions for break properties
/// Check if a `PageBreak` value is avoid
20
#[must_use] pub const fn is_avoid_page_break(page_break: &PageBreak) -> bool {
20
    matches!(page_break, PageBreak::Avoid | PageBreak::AvoidPage)
20
}
/// Check if a `BreakInside` value prevents breaks
5
#[must_use] pub const fn is_avoid_break_inside(break_inside: &BreakInside) -> bool {
2
    matches!(
5
        break_inside,
        BreakInside::Avoid | BreakInside::AvoidPage | BreakInside::AvoidColumn
    )
5
}
// Font Chain Resolution - Pre-Layout Font Loading
use std::collections::HashMap;
use rust_fontconfig::{
    FcFontCache, FcWeight, FontFallbackChain, PatternMatch, UnicodeRange,
    DEFAULT_UNICODE_FALLBACK_SCRIPTS,
};
use crate::text3::cache::{FontChainKey, FontChainKeyOrRef, FontSelector, FontStack, FontStyle};
/// Build a fontconfig `FontSelector` stack from a list of CSS font families.
///
/// Shared by `get_style_properties` and `collect_font_stacks_from_styled_dom`.
/// `Ref` families are skipped (callers handle embedded fonts via `FontStack::Ref`),
/// `SystemType` families expand to the platform's fallback chain, and the generic
/// `sans-serif`/`serif`/`monospace` fallbacks are appended if not already present.
///
/// When `platform` is `None` (e.g. the paged / PDF layout path that hard-codes
/// `system_style = None`), system fonts resolve via `Platform::current()` so the
/// names stay in lock-step with the font-loading pass (which always uses
/// `Platform::current()`); diverging to a bare "sans-serif" would not match the
/// names the loader registered → zero glyphs → text collapses to 0 width.
// The `platform` binding uses a pre-declared `let current;` so the else branch can
// extend the lifetime of a freshly-computed Platform and hand back a reference to it;
// map_or_else cannot express this (the closure would return a dangling local ref).
#[allow(clippy::option_if_let_else)]
/// The system's fontconfig `<alias><prefer>` lists for the CSS generic
/// families (Linux only), parsed once per process.
///
/// Chromium - and every fontconfig-linked toolkit - resolves `sans-serif`
/// through these aliases, so the SAME machine that renders reftests with
/// Noto Sans in Chrome must not render Ubuntu in azul just because
/// rust-fontconfig's hardcoded per-OS candidate list puts Ubuntu first.
/// The preferred families are inserted BEFORE the generic in the selector
/// stack; the generic stays, so rust-fontconfig's expansion still appends
/// its own fallbacks after them.
///
/// Only the `<alias>` subset of the fontconfig configuration is read
/// (fonts.conf + conf.d/*.conf in sorted order, matching fontconfig's
/// accumulation order); everything else in those files is ignored.
#[cfg(all(target_os = "linux", feature = "std"))]
22672
fn fontconfig_generic_aliases() -> &'static std::collections::BTreeMap<String, Vec<String>> {
    use std::collections::BTreeMap;
    use std::sync::OnceLock;
    static ALIASES: OnceLock<BTreeMap<String, Vec<String>>> = OnceLock::new();
22672
    ALIASES.get_or_init(|| {
20
        let mut map: BTreeMap<String, Vec<String>> = BTreeMap::new();
        // FONTCONFIG_FILE overrides the system configuration wholesale -
        // exactly like libfontconfig - which is how the reftest pipeline
        // pins a hermetic font set for azul AND Chrome simultaneously.
20
        if let Ok(custom) = std::env::var("FONTCONFIG_FILE") {
            if !custom.is_empty() {
                let files = vec![std::path::PathBuf::from(custom)];
                collect_alias_files(&files, &mut map);
                return map;
            }
20
        }
20
        let mut files: Vec<std::path::PathBuf> = vec!["/etc/fonts/fonts.conf".into()];
20
        if let Ok(dir) = std::fs::read_dir("/etc/fonts/conf.d") {
20
            let mut confs: Vec<_> = dir
20
                .filter_map(Result::ok)
660
                .map(|e| e.path())
660
                .filter(|p| p.extension().is_some_and(|e| e == "conf"))
20
                .collect();
20
            confs.sort();
20
            files.extend(confs);
        }
20
        collect_alias_files(&files, &mut map);
20
        map
20
    })
22672
}
/// Accumulate `<alias><prefer>` families from the given fontconfig files
/// into `map` (generic-family keys only, first occurrence wins per family).
#[cfg(all(target_os = "linux", feature = "std"))]
20
fn collect_alias_files(
20
    files: &[std::path::PathBuf],
20
    map: &mut std::collections::BTreeMap<String, Vec<String>>,
20
) {
680
    for path in files {
660
        let Ok(content) = std::fs::read_to_string(path) else { continue };
660
        let mut rest = content.as_str();
6760
        while let Some(start) = rest.find("<alias") {
6100
            let Some(end_rel) = rest[start..].find("</alias>") else { break };
6100
            let block = &rest[start..start + end_rel];
6100
            rest = &rest[start + end_rel + "</alias>".len()..];
6100
            let Some(fam) = extract_xml_tag(block, "family") else { continue };
6100
            let fam_lower = fam.to_ascii_lowercase();
6100
            if !matches!(fam_lower.as_str(), "sans-serif" | "serif" | "monospace") {
5800
                continue;
300
            }
300
            let Some(prefer_start) = block.find("<prefer>") else { continue };
240
            let prefer_end = block[prefer_start..]
240
                .find("</prefer>")
240
                .map_or(block.len(), |e| prefer_start + e);
240
            let mut prefer_block = &block[prefer_start..prefer_end];
240
            let entry = map.entry(fam_lower).or_default();
4560
            while let Some(f) = extract_xml_tag(prefer_block, "family") {
153780
                if !entry.iter().any(|e| e.eq_ignore_ascii_case(&f)) {
4260
                    entry.push(f.clone());
4260
                }
4320
                let Some(pos) = prefer_block.find("</family>") else { break };
4320
                prefer_block = &prefer_block[pos + "</family>".len()..];
            }
        }
    }
20
}
/// First `<tag>...</tag>` text content inside `block`, trimmed.
#[cfg(all(target_os = "linux", feature = "std"))]
10660
fn extract_xml_tag(block: &str, tag: &str) -> Option<String> {
10660
    let open = alloc::format!("<{tag}>");
10660
    let close = alloc::format!("</{tag}>");
10660
    let s = block.find(&open)? + open.len();
10420
    let e = block[s..].find(&close)? + s;
10420
    Some(block[s..e].trim().to_string())
10660
}
/// Push `family` onto the selector stack, preceded by the system's
/// fontconfig alias preferences when it is a CSS generic family (see
/// `fontconfig_generic_aliases`).
24746
fn push_family_with_system_aliases(
24746
    stack: &mut Vec<FontSelector>,
24746
    family: String,
24746
    weight: FcWeight,
24746
    style: FontStyle,
24746
) {
    #[cfg(all(target_os = "linux", feature = "std"))]
    {
24746
        let lower = family.to_ascii_lowercase();
24746
        if matches!(lower.as_str(), "sans-serif" | "serif" | "monospace") {
22647
            if let Some(prefs) = fontconfig_generic_aliases().get(&lower) {
1630584
                for pref in prefs {
106001073
                    if !stack.iter().any(|f| f.family.eq_ignore_ascii_case(pref)) {
1056585
                        stack.push(FontSelector {
1056585
                            family: pref.clone(),
1056585
                            weight,
1056585
                            style,
1056585
                            unicode_ranges: Vec::new(),
1056585
                        });
1056585
                    }
                }
            }
2099
        }
    }
24746
    stack.push(FontSelector {
24746
        family,
24746
        weight,
24746
        style,
24746
        unicode_ranges: Vec::new(),
24746
    });
24746
}
/// Memoised [`build_font_selector_stack`].
///
/// Building the stack is PURE — the same (families, platform, weight,
/// style) always yields the same selectors — but it was being rebuilt for
/// every text node during intrinsic sizing and again during inline layout.
/// On a document whose body sets one `font-family` that every block
/// inherits, that is the identical eight-selector stack constructed 82
/// times per pagination, each build allocating a `String` per selector plus
/// a lowercase copy and a fontconfig alias lookup per generic. It measured
/// 18% of a warm release-mode pagination (`bfss_*` spans) and was the
/// single largest source of short-lived allocations in the layout pass.
///
/// The memo is per-thread (layout is single-threaded per document, so no
/// lock) and keyed on everything the builder reads, so a document that
/// changes family, weight, style or platform gets a fresh build — see the
/// `memo_is_keyed_on_*` tests.
#[allow(clippy::implicit_hasher)]
54952
fn build_font_selector_stack_memo(
54952
    font_families: &StyleFontFamilyVec,
54952
    platform: Option<&azul_css::system::Platform>,
54952
    fc_weight: FcWeight,
54952
    fc_style: FontStyle,
54952
) -> Vec<FontSelector> {
    use core::hash::{Hash, Hasher};
    use std::cell::RefCell;
    use std::collections::HashMap;
    // Bounded so a pathological document (thousands of distinct
    // family/weight combinations) cannot grow the memo without limit; the
    // realistic working set is single digits.
    const MAX_ENTRIES: usize = 256;
    thread_local! {
        static MEMO: RefCell<HashMap<u64, Vec<FontSelector>>> =
            RefCell::new(HashMap::new());
    }
54952
    let key = {
54952
        let mut h = std::collections::hash_map::DefaultHasher::new();
58391
        for i in 0..font_families.len() {
58391
            font_families.get(i).unwrap().hash(&mut h);
58391
        }
        // `Platform` is a repr(C) FFI enum without `Hash`; hash its
        // discriminant plus the one payload the builder can read (the
        // Linux desktop-environment name).
54952
        match platform {
54815
            None => 0u8.hash(&mut h),
137
            Some(p) => {
137
                1u8.hash(&mut h);
137
                core::mem::discriminant(p).hash(&mut h);
137
                if let azul_css::system::Platform::Linux(de) = p {
135
                    core::mem::discriminant(de).hash(&mut h);
135
                    if let azul_css::system::DesktopEnvironment::Other(name) = de {
                        name.as_str().hash(&mut h);
135
                    }
2
                }
            }
        }
54952
        core::mem::discriminant(&fc_weight).hash(&mut h);
54952
        core::mem::discriminant(&fc_style).hash(&mut h);
54952
        h.finish()
    };
54952
    if let Some(hit) = MEMO.with(|m| m.borrow().get(&key).cloned()) {
52201
        drop(crate::probe::Probe::span("font_stack_memo_hit"));
52201
        return hit;
2751
    }
2751
    let _p = crate::probe::Probe::span("font_stack_build");
2751
    let built = build_font_selector_stack(font_families, platform, fc_weight, fc_style);
2751
    MEMO.with(|m| {
2751
        let mut m = m.borrow_mut();
2751
        if m.len() >= MAX_ENTRIES {
            m.clear();
2751
        }
2751
        m.insert(key, built.clone());
2751
    });
2751
    built
54952
}
7549
fn build_font_selector_stack(
7549
    font_families: &StyleFontFamilyVec,
7549
    platform: Option<&azul_css::system::Platform>,
7549
    fc_weight: FcWeight,
7549
    fc_style: FontStyle,
7549
) -> Vec<FontSelector> {
7549
    let mut stack = Vec::with_capacity(font_families.len() + 3);
7727
    for i in 0..font_families.len() {
7727
        let family = font_families.get(i).unwrap();
7727
        if matches!(family, StyleFontFamily::Ref(_)) {
            continue;
7727
        }
        // Both spellings of a system font expand to the platform fallback
        // chain: the typed `SystemType(..)` variant AND the magic-string
        // form `System("system:ui")` used by the widget style tables — no
        // fontconfig database knows a family literally called "system:ui".
7727
        let system_type = match family {
55
            StyleFontFamily::SystemType(s) => Some(*s),
7672
            StyleFontFamily::System(name) => {
7672
                azul_css::system::SystemFontType::from_css_str(name.as_str())
            }
            _ => None,
        };
7727
        if let Some(system_type) = system_type {
            let current;
276
            let platform = if let Some(p) = platform { p } else {
130
                current = azul_css::system::Platform::current();
130
                &current
            };
276
            let font_names = system_type.get_fallback_chain(platform);
276
            let system_weight = if system_type.is_bold() {
1
                FcWeight::Bold
            } else {
275
                fc_weight
            };
276
            let system_style = if system_type.is_italic() {
                FontStyle::Italic
            } else {
276
                fc_style
            };
1926
            for font_name in font_names {
1650
                stack.push(FontSelector {
1650
                    family: font_name.to_string(),
1650
                    weight: system_weight,
1650
                    style: system_style,
1650
                    unicode_ranges: Vec::new(),
1650
                });
1650
            }
7451
        } else {
7451
            // as_query_string, NOT as_string: FontManager queries fontconfig with the
7451
            // RAW name. as_string() CSS-quotes whitespace names ("Times New Roman" ->
7451
            // "\"Times New Roman\""), which corrupts the query for every multi-word font.
7451
            push_family_with_system_aliases(
7451
                &mut stack,
7451
                family.as_query_string(),
7451
                fc_weight,
7451
                fc_style,
7451
            );
7451
        }
    }
30196
    for fallback in &["sans-serif", "serif", "monospace"] {
22647
        if !stack
22647
            .iter()
1850265
            .any(|f| f.family.eq_ignore_ascii_case(fallback))
17295
        {
17295
            push_family_with_system_aliases(
17295
                &mut stack,
17295
                (*fallback).to_string(),
17295
                FcWeight::Normal,
17295
                FontStyle::Normal,
17295
            );
17295
        }
    }
7549
    stack
7549
}
/// Result of collecting font stacks from a `StyledDom`
/// Contains all unique font stacks and the mapping from `StyleFontFamiliesHash` to `FontChainKey`
#[derive(Debug, Clone)]
pub struct CollectedFontStacks {
    /// All unique font stacks found in the document (system/file fonts via fontconfig)
    pub font_stacks: Vec<Vec<FontSelector>>,
    /// Map from the font stack hash to the index in `font_stacks`
    pub hash_to_index: HashMap<u64, usize>,
    /// Direct `FontRefs` that bypass fontconfig (e.g., embedded icon fonts)
    /// These are keyed by their pointer address for uniqueness
    pub font_refs: HashMap<usize, azul_css::props::basic::font::FontRef>,
}
/// Resolved font chains ready for use in layout
/// This is the result of resolving font stacks against `FcFontCache`
#[derive(Debug, Clone, Default)]
pub struct ResolvedFontChains {
    /// Map from `FontChainKeyOrRef` to the resolved `FontFallbackChain`
    /// For `FontChainKeyOrRef::Ref` variants, the `FontFallbackChain` contains
    /// a single-font chain that covers the entire Unicode range.
    pub chains: HashMap<FontChainKeyOrRef, FontFallbackChain>,
    /// CSS families that were REQUESTED but could not be matched to any
    /// font (not on disk, not registered in memory).
    ///
    /// This used to be swallowed: the resolver moved on to the next family
    /// and, if the whole stack failed, `ensure_chains_nonempty` quietly
    /// attached an arbitrary system font. Every unmatched family therefore
    /// collapsed onto the SAME `FontId`, text rendered in a font nobody
    /// asked for, and no test could tell. A failed family match is now a
    /// first-class output: it is recorded here and logged once
    /// (see `report_unresolved_families`).
    pub unresolved_families: std::collections::BTreeSet<String>,
    /// Chains that matched NOTHING at all and only render because
    /// `ensure_chains_nonempty` attached a last-resort font. These are
    /// rendering in a font the stylesheet never asked for.
    pub last_resort_chains: usize,
}
impl ResolvedFontChains {
    /// Get a font chain by its key
2
    #[must_use] pub fn get(&self, key: &FontChainKeyOrRef) -> Option<&FontFallbackChain> {
2
        self.chains.get(key)
2
    }
    /// Get a font chain by `FontChainKey` (for system fonts)
4
    #[must_use] pub fn get_by_chain_key(&self, key: &FontChainKey) -> Option<&FontFallbackChain> {
4
        self.chains.get(&FontChainKeyOrRef::Chain(key.clone()))
4
    }
    /// Get a font chain for a font stack (via fontconfig)
3
    #[must_use] pub fn get_for_font_stack(&self, font_stack: &[FontSelector]) -> Option<&FontFallbackChain> {
3
        let key = FontChainKeyOrRef::Chain(FontChainKey::from_selectors(font_stack));
3
        self.chains.get(&key)
3
    }
    /// Get a font chain for a `FontRef` pointer
6
    #[must_use] pub fn get_for_font_ref(&self, ptr: usize) -> Option<&FontFallbackChain> {
6
        self.chains.get(&FontChainKeyOrRef::Ref(ptr))
6
    }
    /// Consume self and return the inner `HashMap` with `FontChainKeyOrRef` keys
    ///
    /// This is useful when you need access to both Chain and Ref variants.
1
    #[must_use] pub fn into_inner(self) -> HashMap<FontChainKeyOrRef, FontFallbackChain> {
1
        self.chains
1
    }
    /// Consume self and return only the fontconfig-resolved chains
    ///
    /// This filters out `FontRef` entries and returns only the chains
    /// resolved via fontconfig. This is what `FontManager` expects.
5601
    #[must_use] pub fn into_fontconfig_chains(self) -> HashMap<FontChainKey, FontFallbackChain> {
        // (2026-06-10: reverted to HashMap end-to-end — the empty-hashbrown RawIter hang behind
        // the 2026-06-05 BTreeMap migration was the un-mirrored EMPTY_GROUP static, fixed
        // transpiler-side in symbol_table.rs::compute_hashbrown_empty_group_ranges.)
5601
        let mut out: HashMap<FontChainKey, FontFallbackChain> = HashMap::new();
5601
        if self.chains.is_empty() {
1165
            return out;
4436
        }
9173
        for (key, chain) in self.chains {
4737
            if let FontChainKeyOrRef::Chain(chain_key) = key {
4735
                out.insert(chain_key, chain);
4735
            }
        }
4436
        out
5601
    }
    /// Get the number of resolved chains
3865
    #[must_use] pub fn len(&self) -> usize {
3865
        self.chains.len()
3865
    }
    /// Check if there are no resolved chains
4
    #[must_use] pub fn is_empty(&self) -> bool {
4
        self.chains.is_empty()
4
    }
    /// Get the number of direct `FontRefs`
2
    #[must_use] pub fn font_refs_len(&self) -> usize {
3
        self.chains.keys().filter(|k| k.is_ref()).count()
2
    }
}
/// Collect all unique font stacks from a `StyledDom`
///
/// This is a pure function that iterates over all nodes in the DOM and
/// extracts the font-family property from each node that has text content.
///
/// # Arguments
/// * `styled_dom` - The styled DOM to extract font stacks from
/// * `platform` - The current platform for resolving system font types
///
/// # Returns
/// A `CollectedFontStacks` containing all unique font stacks and a hash-to-index mapping
#[allow(clippy::cast_possible_truncation)] // bounded graphics/coord/font/fixed-point/debug-marker cast
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
5602
#[must_use] pub fn collect_font_stacks_from_styled_dom(
5602
    styled_dom: &StyledDom,
5602
    platform: &azul_css::system::Platform,
5602
) -> CollectedFontStacks {
    use azul_css::compact_cache::{
        FONT_STYLE_MASK, FONT_STYLE_SHIFT, FONT_WEIGHT_MASK, FONT_WEIGHT_SHIFT,
    };
5602
    let mut font_stacks = Vec::new();
5602
    let mut hash_to_index: HashMap<u64, usize> = HashMap::new();
5602
    let mut font_refs: HashMap<usize, azul_css::props::basic::font::FontRef> = HashMap::new();
5602
    let node_data = styled_dom.node_data.as_container();
5602
    let cache = &styled_dom.css_property_cache.ptr;
5602
    let Some(compact) = cache.compact_cache.as_ref() else {
2
        return CollectedFontStacks {
2
            font_stacks,
2
            hash_to_index,
2
            font_refs,
2
        };
    };
    // Phase 1: Scan compact cache arrays (just u64 reads) to find unique
    // (font_family_hash, weight, style) tuples. Record one representative
    // node index per unique tuple for the expensive CSS lookup in Phase 2.
    // Key: (font_family_hash, weight_encoded, style_encoded) → representative node index
    // (2026-06-10: reverted to HashMap — the historic g81/g47 empty-hashbrown mis-lift was the
    // un-mirrored EMPTY_GROUP static, fixed transpiler-side in symbol_table.rs::
    // compute_hashbrown_empty_group_ranges. std HashMap lifts correctly now; RandomState seeds
    // via the transpiler's HashmapRandomKeys fixed-seed body.)
5600
    let mut unique_font_keys: HashMap<(u64, u8, u8), usize> = HashMap::new();
5600
    let node_count = node_data.internal.len();
    // WEB-LIFT: probe node_type bytes (NodeType #[repr(C,u8)], Text=177 per AzDom_createText).
    // 0x406D0..DC = n1.node_type bytes[0,1,2,4]; 0x406E0 = n0.node_type byte[0] (body disc).
5600
    if node_count > 1 {
5426
        let p1 = (&raw const node_data.internal[1].node_type).cast::<u8>();
5426
        let p0 = (&raw const node_data.internal[0].node_type).cast::<u8>();
5426
        unsafe {
5426
            crate::az_mark(0x606D0_u32, u32::from(core::ptr::read(p1)));
5426
            crate::az_mark(0x606D4_u32, u32::from(core::ptr::read(p1.add(1))));
5426
            crate::az_mark(0x606D8_u32, u32::from(core::ptr::read(p1.add(2))));
5426
            crate::az_mark(0x606DC_u32, u32::from(core::ptr::read(p1.add(4))));
5426
            crate::az_mark(0x606E0_u32, u32::from(core::ptr::read(p0)));
5426
        }
174
    }
5600
    let styled_nodes_phase1 = styled_dom.styled_nodes.as_container();
202840
    for i in 0..node_count {
        // Only text nodes need fonts. WEB-LIFT: the lifted `matches!(node_type,
        // NodeType::Text(_))` MIS-LIFTS (compares against a mis-lifted discriminant
        // constant) — text nodes never match → no font stack → no chain → text h=0.
        // NodeType is #[repr(C,u8)] so the discriminant is the u8 at offset 0; Text=177
        // (per AzDom_createText: `mov w8,#0xb1; strb w8,[x19]`). Compare the raw
        // discriminant to the literal 177 (a source literal lifts correctly).
202840
        let nt_disc = unsafe {
202840
            core::ptr::read((&raw const node_data.internal[i].node_type).cast::<u8>())
        };
202840
        let is_text = nt_disc == 177
139665
            || matches!(node_data.internal[i].node_type, NodeType::Text(_));
202840
        if !is_text {
139665
            continue;
63175
        }
63175
        let fh = compact.tier2b_text[i].font_family_hash;
63175
        let t1 = compact.tier1_enums[i];
63175
        let mut weight_bits = ((t1 >> FONT_WEIGHT_SHIFT) & FONT_WEIGHT_MASK) as u8;
63175
        let mut style_bits = ((t1 >> FONT_STYLE_SHIFT) & FONT_STYLE_MASK) as u8;
        // The compact bits see only AUTHOR css. Two text nodes with all-
        // default bits can still resolve to DIFFERENT weights/styles through
        // UA rules on their parents (an h1's bold vs a p's normal), and
        // deduping them into one bucket resolved+loaded only the
        // REPRESENTATIVE's chain — every run asking for the other weight was
        // unshapeable (skipped: zero lines) and its font never loaded.
        // Whenever the fast bits are at their defaults, key on the real
        // cascade instead (the same reads Phase 2 does on representatives).
63175
        if weight_bits == 0 && style_bits == 0 {
62959
            if let Some(dom_id) = NodeId::from_usize(i) {
62959
                let node_state = &styled_nodes_phase1[dom_id].styled_node_state;
62959
                if let MultiValue::Exact(w) =
62959
                    get_font_weight_property(styled_dom, dom_id, node_state)
62959
                {
62959
                    weight_bits = super::fc::convert_font_weight(w) as u8;
62959
                }
62959
                if let MultiValue::Exact(st) =
62959
                    get_font_style_property(styled_dom, dom_id, node_state)
62959
                {
62959
                    style_bits = st as u8;
62959
                }
            }
216
        }
63175
        let key = (fh, weight_bits, style_bits);
63175
        unique_font_keys.entry(key).or_insert(i);
    }
    // WASM-ONLY PROBE (REVERT): why 0 chains? 0x406C0=tag(5E5E0003), C4=node_count,
    // C8=unique_font_keys.len() (#text nodes matched in Phase 1). If C8=0 → the lifted
    // `matches!(node_type, NodeType::Text(_))` FAILS for the text node (node_type mis-lift)
    // → no font stack → no chain → text h=0. C is the count of NodeType::Text via a raw
    // discriminant byte read (node_type tag), to compare against the matches! result.
    {
5600
        let mut raw_text = 0u32;
202840
        for i in 0..node_count {
            // NodeType is repr(C,u8)-ish; read the leading discriminant byte directly.
202840
            let nt_ptr = (&raw const node_data.internal[i].node_type).cast::<u8>();
202840
            let disc = unsafe { core::ptr::read_volatile(nt_ptr) };
            // Text is one specific discriminant; count whatever the body node ISN'T.
202840
            if disc != unsafe { core::ptr::read_volatile((&raw const node_data.internal[0].node_type).cast::<u8>()) } {
193388
                raw_text += 1;
193388
            }
        }
5600
        unsafe {
5600
            crate::az_mark(0x606C0_u32, (0x5E5E_0003_u32));
5600
            crate::az_mark(0x606C4_u32, (node_count as u32));
5600
            crate::az_mark(0x606C8_u32, (unique_font_keys.len() as u32));
5600
            crate::az_mark(0x606CC_u32, (raw_text));
5600
        }
    }
    // Phase 2: For each unique tuple, do ONE expensive CSS lookup on the
    // representative node to get the actual font-family names.
5600
    let styled_nodes = styled_dom.styled_nodes.as_container();
10398
    for (&(fh, _wb, _sb), &repr_idx) in &unique_font_keys {
4798
        let Some(dom_id) = NodeId::from_usize(repr_idx) else {
            continue;
        };
4798
        let node_state = &styled_nodes[dom_id].styled_node_state;
        // Use reverse map from compact cache: hash → actual font families.
        // This works for ALL nodes including text nodes that inherit font-family
        // via compact cache (where get_property_slow would return None).
4798
        let font_families = compact
4798
            .font_hash_to_families
4798
            .get(&fh)
4798
            .cloned()
4798
            .unwrap_or_else(|| {
2760
                StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
2760
            });
        // Check for embedded FontRef
4798
        if let Some(StyleFontFamily::Ref(font_ref)) = font_families.get(0) {
9
            let ptr = font_ref.parsed as usize;
9
            font_refs.entry(ptr).or_insert_with(|| font_ref.clone());
9
            continue;
4789
        }
4789
        let font_weight = match get_font_weight_property(styled_dom, dom_id, node_state) {
4789
            MultiValue::Exact(v) => v,
            _ => StyleFontWeight::Normal,
        };
4789
        let font_style = match get_font_style_property(styled_dom, dom_id, node_state) {
4789
            MultiValue::Exact(v) => v,
            _ => StyleFontStyle::Normal,
        };
4789
        let fc_weight = super::fc::convert_font_weight(font_weight);
4789
        let fc_style = super::fc::convert_font_style(font_style);
4789
        let font_stack =
4789
            build_font_selector_stack(&font_families, Some(platform), fc_weight, fc_style);
4789
        if font_stack.is_empty() {
            continue;
4789
        }
4789
        let key = FontChainKey::from_selectors(&font_stack);
4789
        let hash = {
            use std::hash::{Hash, Hasher};
4789
            let mut hasher = std::collections::hash_map::DefaultHasher::new();
4789
            key.hash(&mut hasher);
4789
            hasher.finish()
        };
4789
        hash_to_index.entry(hash).or_insert_with(|| {
4735
            let idx = font_stacks.len();
4735
            font_stacks.push(font_stack);
4735
            idx
4735
        });
    }
5600
    CollectedFontStacks {
5600
        font_stacks,
5600
        hash_to_index,
5600
        font_refs,
5600
    }
5602
}
/// Resolve all font chains for the collected font stacks
///
/// This is a pure function that takes the collected font stacks and resolves
/// them against the `FcFontCache` to produce `FontFallbackChains`.
///
/// # Arguments
/// * `collected` - The collected font stacks from `collect_font_stacks_from_styled_dom`
/// * `fc_cache` - The fontconfig cache to resolve fonts against
///
/// # Returns
/// A `ResolvedFontChains` containing all resolved font chains
/// Walk every text node in `styled_dom` and collect the set of
/// non-ASCII codepoints actually present in the document.
///
/// Used by [`prune_chain_to_used_chars`] to drop CSS-fallback fonts
/// from a resolved chain when the *first* match in a `css_fallbacks`
/// group already covers everything the page asks for. ASCII (`< 0x80`)
/// is universally covered by every Latin font we'd resolve, so we
/// skip it here to keep the set small. Unicode characters in the
/// returned set are deduped + sorted via `BTreeSet`.
///
/// Cost: O(total text length). Cheap relative to layout itself.
5559
#[must_use] pub fn collect_used_codepoints(styled_dom: &StyledDom) -> std::collections::BTreeSet<u32> {
5559
    let mut out = std::collections::BTreeSet::new();
5559
    let node_data = styled_dom.node_data.as_container();
207956
    for node in node_data.internal {
202397
        let NodeType::Text(s) = &node.node_type else {
139462
            continue;
        };
800711
        for c in s.as_str().chars() {
800711
            let cp = c as u32;
800711
            if cp >= 0x80 {
2049
                out.insert(cp);
798662
            }
        }
    }
5559
    out
5559
}
/// Like [`collect_used_codepoints`] but keeps ASCII.
///
/// The fast-probe
/// path (`FcFontRegistry::request_fonts_fast`) *does* need ASCII:
/// "the font has to cover every codepoint I will render" is only
/// true if we tell it every codepoint, and "Segoe UI" not being
/// installed on macOS means even ASCII has to fall through to a
/// system default.
///
/// `collect_used_codepoints` strips ASCII because its caller
/// (`prune_chain_to_used_chars`) runs *after* resolution to trim an
/// already-resolved chain and every Latin-covering font passes ASCII
/// trivially. That assumption doesn't hold during probing.
48
#[must_use] pub fn collect_used_codepoints_all(styled_dom: &StyledDom) -> std::collections::BTreeSet<char> {
48
    let mut out = std::collections::BTreeSet::new();
48
    let node_data = styled_dom.node_data.as_container();
504
    for node in node_data.internal {
456
        let NodeType::Text(s) = &node.node_type else {
210
            continue;
        };
1302
        for c in s.as_str().chars() {
1302
            out.insert(c);
1302
        }
    }
48
    out
48
}
/// Trim a [`FontFallbackChain`] down to the minimum set of `FontMatch`
/// entries needed to cover `used_chars` (typically from
/// [`collect_used_codepoints`]).
///
/// For each `css_fallbacks` group, walk matches in the resolver's
/// preferred order and keep them until every codepoint in
/// `used_chars` is covered (per the OS/2 unicode-range bits cached
/// in `FontMatch.unicode_ranges`). Always keeps at least the first
/// match per group so a font listed in CSS doesn't disappear.
///
/// `unicode_fallbacks` is filtered to only include fonts whose
/// ranges intersect `used_chars` — Phase-6's
/// [`scripts_present_in_styled_dom`] already scopes the *script
/// blocks* but a single block (e.g. CJK Unified, U+4E00..U+9FFF)
/// can have hundreds of matching system fonts; this prunes them
/// down to the few that actually cover the codepoints used.
///
/// On excel.html (~ASCII-only) this drops the per-chain
/// `css_fallbacks` from 5 → 1 in each group, eliminating ~20 of
/// the 26 fonts that would otherwise be parsed by
/// `load_fonts_from_disk`.
4626
pub fn prune_chain_to_used_chars(
4626
    chain: &mut FontFallbackChain,
4626
    used_chars: &std::collections::BTreeSet<u32>,
4626
) {
11603
    fn fm_covers(fm: &rust_fontconfig::FontMatch, cp: u32) -> bool {
11603
        fm.unicode_ranges
11603
            .iter()
59330
            .any(|r| cp >= r.start && cp <= r.end)
11603
    }
107999
    for group in &mut chain.css_fallbacks {
103373
        if group.fonts.is_empty() {
70607
            continue;
32766
        }
        // Track which non-ASCII chars still need coverage as we walk
        // matches in order. We always keep at least the first match.
32766
        let mut needed: Vec<u32> = used_chars.iter().copied().collect();
32766
        needed.retain(|&cp| !fm_covers(&group.fonts[0], cp));
32766
        let mut keep = 1;
34306
        for fm in group.fonts.iter().skip(1) {
34298
            if needed.is_empty() {
31515
                break;
2783
            }
2783
            keep += 1;
5753
            needed.retain(|&cp| !fm_covers(fm, cp));
        }
32766
        group.fonts.truncate(keep);
    }
4626
    chain
4626
        .unicode_fallbacks
4626
        .retain(|fm| used_chars.iter().any(|&cp| fm_covers(fm, cp)));
4626
}
/// Scan text-node content in `styled_dom` and return the subset of
/// [`rust_fontconfig::DEFAULT_UNICODE_FALLBACK_SCRIPTS`] whose code-point
/// ranges actually appear in any text.
///
/// Short-circuits once all seven
/// ranges have been seen.
///
/// Callers pass the result as `scripts_hint` to
/// [`resolve_font_chains`] / [`collect_and_resolve_font_chains_with_registration`];
/// `rust_fontconfig::FcFontCache::resolve_font_chain_with_scripts` then
/// only pulls in Unicode-fallback fonts for scripts the document
/// actually uses. An ASCII-only page returns an empty vector, which
/// avoids dragging Arial Unicode MS, CJK fonts, etc. into the
/// resolved chain and therefore into the eager-load step.
5557
#[must_use] pub fn scripts_present_in_styled_dom(styled_dom: &StyledDom) -> Vec<UnicodeRange> {
5557
    let scripts = DEFAULT_UNICODE_FALLBACK_SCRIPTS;
5557
    let mut seen = vec![false; scripts.len()];
5557
    let mut hits = 0usize;
5557
    let node_data = styled_dom.node_data.as_container();
207947
    'outer: for node in node_data.internal {
202390
        let text: &str = match &node.node_type {
62933
            NodeType::Text(s) => s.as_str(),
139457
            _ => continue,
        };
800706
        for c in text.chars() {
800706
            let cp = c as u32;
            // Cheap reject: everything below the first fallback-script
            // range (Cyrillic starts at U+0400) is covered by the CSS
            // fallbacks' own glyphs — no reason to probe.
800706
            if cp < 0x0400 {
798877
                continue;
1829
            }
12739
            for (idx, r) in scripts.iter().enumerate() {
12739
                if !seen[idx] && cp >= r.start && cp <= r.end {
28
                    seen[idx] = true;
28
                    hits += 1;
28
                    if hits == scripts.len() {
                        break 'outer;
28
                    }
28
                    break;
12711
                }
            }
        }
    }
5557
    scripts
5557
        .iter()
5557
        .enumerate()
38899
        .filter_map(|(i, r)| if seen[i] { Some(*r) } else { None })
5557
        .collect()
5557
}
/// Resolve font chains for a collected set of stacks.
///
/// `scripts_hint`:
/// - `None` keeps the original "all 7 default scripts" behaviour
///   (Cyrillic / Arabic / Devanagari / Hiragana / Katakana / CJK /
///   Hangul) — equivalent to passing
///   `Some(rust_fontconfig::DEFAULT_UNICODE_FALLBACK_SCRIPTS)`.
/// - `Some(&[])` attaches *no* Unicode fallbacks, suitable for
///   ASCII-only documents. Combined with `prune_chain_to_used_chars`
///   this is what eliminates Arial Unicode MS / CJK / Arabic font
///   loads on Latin-only pages.
/// - `Some(ranges)` attaches fallbacks only for the listed scripts.
///   Production callers compute this via
///   [`scripts_present_in_styled_dom`].
2
#[must_use] pub fn resolve_font_chains(
2
    collected: &CollectedFontStacks,
2
    fc_cache: &FcFontCache,
2
    scripts_hint: Option<&[UnicodeRange]>,
2
) -> ResolvedFontChains {
2
    resolve_font_chains_with_registry(collected, fc_cache, None, scripts_hint, &HashMap::new())
2
}
/// Split a CSS font stack into (a) the groups that resolve to an in-memory
/// font registered BY FAMILY NAME and (b) the families that still have to
/// be looked up on disk.
///
/// In-memory fonts are the bundled/embedder/test fonts registered with
/// [`crate::text3::cache::FontManager::register_named_font`]. They must be
/// matched here, in azul, because the fast disk resolver
/// (`FcFontRegistry::request_fonts_fast`) only walks file paths and cannot
/// see them at all. Matching is on the NORMALIZED family name, which also
/// makes `font-family: "Foo Bar"` (the CSS parser keeps the quotes) match
/// the registered `Foo Bar`.
4738
fn split_memory_matches(
4738
    font_families: &[String],
4738
    memory_families: &HashMap<String, Vec<crate::text3::cache::MemoryFace>>,
4738
    weight: FcWeight,
4738
    italic: bool,
4738
    oblique: bool,
4738
) -> (
4738
    Vec<rust_fontconfig::CssFallbackGroup>,
4738
    Vec<String>,
4738
    Vec<rust_fontconfig::CssFallbackGroup>,
4738
) {
    use crate::text3::cache::MemoryFontTier;
4738
    let mut groups = Vec::new();
4738
    let mut disk = Vec::new();
4738
    let mut fallback = Vec::new();
683923
    for family in font_families {
679185
        let norm = rust_fontconfig::utils::normalize_family_name(family);
679185
        let faces = memory_families.get(&norm);
        // A primary face IS the family: it wins outright, disk never consulted.
290
        if let Some(face) =
679185
            faces.and_then(|f| pick_memory_face(f, weight, italic, oblique, MemoryFontTier::Primary))
        {
290
            groups.push(rust_fontconfig::CssFallbackGroup {
290
                css_name: family.clone(),
290
                fonts: vec![face.font_match.clone()],
290
            });
290
            continue;
678895
        }
        // Otherwise the disk gets first refusal, and a fallback face - if the
        // caller registered one - waits behind whatever the disk turns up.
678895
        disk.push(family.clone());
678895
        if let Some(face) = faces
678895
            .and_then(|f| pick_memory_face(f, weight, italic, oblique, MemoryFontTier::Fallback))
2
        {
2
            fallback.push(rust_fontconfig::CssFallbackGroup {
2
                css_name: family.clone(),
2
                fonts: vec![face.font_match.clone()],
2
            });
678893
        }
    }
4738
    (groups, disk, fallback)
4738
}
/// Choose the registered in-memory face that best matches a CSS
/// `(weight, italic/oblique)` query. Prefers faces whose slant matches the
/// request, then the nearest weight via [`rust_fontconfig::FcWeight::find_best_match`]
/// (the CSS weight-fallback order). A variable face whose `wght` axis spans the
/// requested weight is treated as an exact match.
///
/// This is what makes `font-weight: bold` actually select a registered bold
/// face: several faces (regular, bold, oblique…) share one family name, and this
/// picks among them instead of taking whichever registered last.
294
fn pick_memory_face(
294
    faces: &[crate::text3::cache::MemoryFace],
294
    weight: FcWeight,
294
    italic: bool,
294
    oblique: bool,
294
    tier: crate::text3::cache::MemoryFontTier,
294
) -> Option<&crate::text3::cache::MemoryFace> {
294
    let faces: Vec<&crate::text3::cache::MemoryFace> =
295
        faces.iter().filter(|f| f.tier == tier).collect();
294
    if faces.is_empty() {
2
        return None;
292
    }
292
    let want_slanted = italic || oblique;
    // Prefer faces matching the requested slant; fall back to all faces so a
    // family with only an upright face still resolves for `font-style: italic`.
292
    let slant_pool: Vec<&crate::text3::cache::MemoryFace> = faces
292
        .iter()
292
        .copied()
292
        .filter(|f| (f.italic || f.oblique) == want_slanted)
292
        .collect();
292
    let pool: Vec<&crate::text3::cache::MemoryFace> = if slant_pool.is_empty() {
        faces.clone()
    } else {
292
        slant_pool
    };
    // A variable face whose wght axis covers the request satisfies it exactly.
292
    let req = f32::from(weight as u16);
292
    if let Some(vf) = pool
292
        .iter()
292
        .copied()
292
        .find(|f| f.weight_axis.is_some_and(|(min, max)| req >= min && req <= max))
    {
        return Some(vf);
292
    }
    // Otherwise pick the nearest static weight (CSS fallback order).
292
    let avail: Vec<FcWeight> = pool.iter().map(|f| f.weight).collect();
292
    let best = weight.find_best_match(&avail).unwrap_or(weight);
292
    pool.iter()
292
        .copied()
292
        .find(|f| f.weight == best)
292
        .or_else(|| pool.first().copied())
294
}
/// Registry-aware variant of [`resolve_font_chains`].
///
/// When `registry`
/// is `Some`, each chain resolution goes through
/// [`rust_fontconfig::registry::FcFontRegistry::request_and_resolve_with_scripts`]
/// which priority-bumps the builder for families not yet in the
/// snapshot and waits for them — the "scout-on-demand" path that
/// avoids the eager common-stack pre-parse.
///
/// When `registry` is `None`, falls back to
/// [`rust_fontconfig::FcFontCache::resolve_font_chain_with_scripts`]
/// against the passed-in snapshot, which is what
/// [`resolve_font_chains`] does and what every code path did before
/// Phase 3.
#[allow(clippy::implicit_hasher)] // internal; memory_families always uses the default hasher
/// Concrete family names that exist ONLY because the fontconfig generic
/// alias expansion invented them — the union of every `<alias><prefer>` list
/// (see `fontconfig_generic_aliases`). Lowercased.
///
/// These are CANDIDATES, not requests: the system offers ~50 families per
/// generic and expects most of them to be absent. Nothing in the document
/// asked for `ZYSong18030`.
#[cfg(all(target_os = "linux", feature = "std"))]
637961
fn alias_candidate_names() -> &'static std::collections::BTreeSet<String> {
    use std::collections::BTreeSet;
    use std::sync::OnceLock;
    static S: OnceLock<BTreeSet<String>> = OnceLock::new();
637961
    S.get_or_init(|| {
20
        fontconfig_generic_aliases()
20
            .values()
20
            .flatten()
4260
            .map(|f| f.to_ascii_lowercase())
20
            .collect()
20
    })
637961
}
#[cfg(not(all(target_os = "linux", feature = "std")))]
fn alias_candidate_names() -> &'static std::collections::BTreeSet<String> {
    use std::collections::BTreeSet;
    use std::sync::OnceLock;
    static S: OnceLock<BTreeSet<String>> = OnceLock::new();
    S.get_or_init(BTreeSet::new)
}
/// Lowercased family names the cache can actually serve.
4617
fn available_family_names(fc_cache: &FcFontCache) -> std::collections::BTreeSet<String> {
4617
    let mut out = std::collections::BTreeSet::new();
262209
    fc_cache.for_each_pattern(|pattern, _id| {
262209
        if let Some(f) = pattern.family.as_ref() {
262209
            out.insert(f.to_ascii_lowercase());
262209
        }
262209
    });
4617
    out
4617
}
/// Drop the alias-expansion candidates the system cannot serve.
///
/// THE COST THIS REMOVES. Every CSS generic is expanded to the system's
/// `<alias><prefer>` families ahead of the generic itself, so a stack of
/// three generics reaches the resolver as ~150 concrete names. Resolving a
/// name that is not installed is not free — measured at ~0.52 ms each,
/// x142 misses = 73.8 ms of a 177 ms cold pagination, for TWO chains. The
/// same 142 misses were also the wall of `UNRESOLVED font-family` warnings.
///
/// WHY THIS IS SAFE. azul does the generic expansion ITSELF
/// (`push_family_with_system_aliases`), so every candidate reaching the
/// resolver is already a concrete name — a concrete name absent from the
/// cache cannot resolve by any route. Generics are never pruned (the
/// resolver expands those internally), and neither is any family that is
/// NOT an alias candidate: a name the DOCUMENT authored keeps its lookup
/// and keeps its warning.
///
/// KNOWN TRADE-OFF: a family that is both authored AND on the system's
/// prefer list (Arial, `DejaVu` Sans, ...) is pruned silently when absent.
/// Its absence is what alias lists exist to absorb, so it is not a
/// diagnostic worth a warning.
4617
fn prune_absent_alias_candidates(
4617
    families: &[String],
4617
    fc_cache: &FcFontCache,
4617
) -> (Vec<String>, usize) {
4617
    let aliases = alias_candidate_names();
4617
    if aliases.is_empty() {
        return (families.to_vec(), 0);
4617
    }
4617
    let available = available_family_names(fc_cache);
4617
    let mut pruned = 0usize;
4617
    let kept = families
4617
        .iter()
662161
        .filter(|f| {
662161
            let drop = should_prune_family(f, aliases, &available);
662161
            if drop {
633129
                pruned += 1;
633129
            }
662161
            !drop
662161
        })
4617
        .cloned()
4617
        .collect();
4617
    (kept, pruned)
4617
}
/// The decision behind [`prune_absent_alias_candidates`], over explicit sets
/// so it can be tested without a system font configuration.
///
/// Prune ONLY when all three hold: the name was invented by the alias
/// expansion, the system does not have it, and it is not a generic (the
/// resolver expands those itself). Anything the DOCUMENT authored fails the
/// first test and is always kept — lookup and warning intact.
662171
fn should_prune_family(
662171
    family: &str,
662171
    aliases: &std::collections::BTreeSet<String>,
662171
    available: &std::collections::BTreeSet<String>,
662171
) -> bool {
662171
    let lower = family.to_ascii_lowercase();
662171
    aliases.contains(&lower) && !available.contains(&lower) && !is_generic_family(family)
662171
}
5556
#[must_use] pub fn resolve_font_chains_with_registry(
5556
    collected: &CollectedFontStacks,
5556
    fc_cache: &FcFontCache,
5556
    registry: Option<&rust_fontconfig::registry::FcFontRegistry>,
5556
    scripts_hint: Option<&[UnicodeRange]>,
5556
    memory_families: &HashMap<String, Vec<crate::text3::cache::MemoryFace>>,
5556
) -> ResolvedFontChains {
5556
    let _probe = crate::probe::Probe::span("font_chain_resolve");
5556
    let mut chains = HashMap::new();
5556
    let trace_t0 = std::env::var_os("AZ_PAGINATE_TRACE")
5556
        .is_some()
5556
        .then(std::time::Instant::now);
5556
    let mut unresolved: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
5556
    let mut total_pruned = 0usize;
    // Resolve system/file font stacks via fontconfig
10174
    for font_stack in &collected.font_stacks {
4618
        if font_stack.is_empty() {
1
            continue;
4617
        }
        // Build font families list
        // (2026-06-10) Build the key through the ONE canonical constructor
        // (FontChainKey::from_selectors — first-wins dedup + the same empty-stack
        // fallback) so the stored key always matches the shaping-time lookup key.
4617
        let canonical_key = FontChainKey::from_selectors(font_stack);
4617
        let font_families = canonical_key.font_families.clone();
4617
        let weight = font_stack[0].weight;
4617
        let is_italic = font_stack[0].style == FontStyle::Italic;
4617
        let is_oblique = font_stack[0].style == FontStyle::Oblique;
4617
        let cache_key = FontChainKeyOrRef::Chain(FontChainKey {
4617
            font_families: font_families.clone(),
4617
            weight,
4617
            italic: is_italic,
4617
            oblique: is_oblique,
4617
        });
4617
        if std::env::var("TEXTDBG").is_ok() {
            eprintln!("[TEXTDBG] CHAIN STORE key={cache_key:?}");
4617
        }
        // Skip if already resolved
4617
        if chains.contains_key(&cache_key) {
            continue;
4617
        }
        // Resolve the font chain
        // IMPORTANT: Use False (not DontCare) when style is Normal.
        // DontCare means "accept italic too" which can match italic fonts.
        // False means "must NOT be italic" which correctly prefers Normal.
4617
        let italic = if is_italic {
18
            PatternMatch::True
        } else {
4599
            PatternMatch::False
        };
4617
        let oblique = if is_oblique {
            PatternMatch::True
        } else {
4617
            PatternMatch::False
        };
        // MEMORY FONTS FIRST (see `split_memory_matches`): a family
        // registered by name into the cache's in-memory table wins over
        // anything on disk, exactly as CSS says.
4617
        let (mem_groups, disk_families, mem_fallbacks) =
4617
            split_memory_matches(&font_families, memory_families, weight, is_italic, is_oblique);
        // Alias candidates the system cannot serve never reach the resolver:
        // each one costs a real lookup (~0.52 ms) to learn what a set
        // membership test answers for free. See
        // `prune_absent_alias_candidates`.
4617
        let (disk_families, pruned_aliases) =
4617
            prune_absent_alias_candidates(&disk_families, fc_cache);
4617
        total_pruned += pruned_aliases;
        // Registry-aware resolve: scout-on-demand path when available.
        // See `resolve_font_chains_with_registry` doc for rationale.
4617
        let mut chain = if disk_families.is_empty() {
            FontFallbackChain {
                css_fallbacks: Vec::new(),
                unicode_fallbacks: Vec::new(),
                original_stack: font_families.clone(),
            }
        } else {
4617
            registry.map_or_else(
4617
                || {
4617
                    let mut trace = Vec::new();
4617
                    fc_cache.resolve_font_chain_with_scripts(
4617
                        &disk_families,
4617
                        weight,
4617
                        italic,
4617
                        oblique,
4617
                        scripts_hint,
4617
                        &mut trace,
                    )
4617
                },
                |reg| {
                    reg.request_and_resolve_with_scripts(
                        &disk_families,
                        weight,
                        italic,
                        oblique,
                        scripts_hint,
                    )
                },
            )
        };
4617
        if !mem_groups.is_empty() {
206
            let mut merged = mem_groups;
206
            merged.append(&mut chain.css_fallbacks);
206
            chain.css_fallbacks = merged;
4411
        }
        // Fallback-tier faces go on the END: the disk has already had its turn,
        // so on a desktop these sit harmlessly behind the installed fonts, and
        // on a target with nothing installed they are what is left.
4617
        chain.css_fallbacks.extend(mem_fallbacks);
        // A family that produced no group matched NOTHING — record it (see
        // `ResolvedFontChains::unresolved_families`).
666993
        for family in &font_families {
662376
            let matched = chain
662376
                .css_fallbacks
662376
                .iter()
14620433
                .any(|g| g.css_name.eq_ignore_ascii_case(family) && !g.fonts.is_empty());
            // A pruned candidate is not a failed request — the expansion
            // invented it and the system does not have it, which is the
            // normal case an alias list exists to absorb. Reporting them
            // buried the families the DOCUMENT actually asked for under
            // ~142 lines of noise.
4088941
            let is_absent_alias = !disk_families.iter().any(|d| d == family)
633344
                && alias_candidate_names().contains(&family.to_ascii_lowercase());
662376
            if !matched && !is_generic_family(family) && !is_absent_alias {
551
                unresolved.insert(family.clone());
661825
            }
        }
        // WEB-LIFT last resort (in azul-layout, NOT rust-fontconfig — so the fragile
        // `with_memory_fonts` isn't re-codegen'd into a trapping shape): the lifted
        // resolve_font_chain query path can return an EMPTY chain even when a fallback
        // font IS registered (generic→OS-name expansion + token/unicode query is
        // lift-fragile). If the chain has no fonts, append the first registered font so
        // load_missing_for_chains / resolve_char find it and text shapes (not measure 0).
103368
        let total_fonts = chain.css_fallbacks.iter().map(|g| g.fonts.len()).sum::<usize>()
4617
            + chain.unicode_fallbacks.len();
4617
        if total_fonts == 0 {
            if let Some((_pattern, id)) = first_font_in_cache(fc_cache).as_ref() {
                // Vec::new() ranges (not pattern.unicode_ranges.clone()) — the Vec-clone
                // mis-lifts on the web backend and empty == "no range restriction" here.
                chain.unicode_fallbacks.push(rust_fontconfig::FontMatch {
                    id: *id,
                    unicode_ranges: Vec::new(),
                    fallbacks: Vec::new(),
                });
            }
4617
        }
4617
        chains.insert(cache_key, chain);
    }
    // NOTE: FontRefs bypass fontconfig entirely — the shaping code checks
    // style.font_stack for FontStack::Ref and uses the font data directly.
    // No entries are inserted into `chains` for them.
5556
    let out = ResolvedFontChains {
5556
        chains,
5556
        unresolved_families: unresolved,
5556
        last_resort_chains: 0,
5556
    };
5556
    if let Some(t0) = trace_t0 {
        eprintln!(
            "[paginate]   resolve_font_chains_with_registry {:?}: {} chain(s), {} \
             unresolved family name(s), {total_pruned} absent alias candidate(s) pruned",
            t0.elapsed(),
            out.chains.len(),
            out.unresolved_families.len(),
        );
5556
    }
5556
    report_unresolved_families(&out);
5556
    out
5556
}
/// The first `(pattern, id)` in the font cache, WITHOUT cloning the whole
/// database.
///
/// `FcFontCache::list()` allocates a `Vec<(FcPattern, FontId)>` and clones
/// every pattern in it — and each `FcPattern` owns several `String`s. Call
/// sites here only ever wanted `.first()`, so the entire font database was
/// being deep-copied to read one entry. heaptrack on a 3-page document
/// counted 717429 `String::clone` calls with `FcPattern::clone` /
/// `FcFontMetadata::clone` on the backtrace, retaining 2.3 MB.
///
/// rust-fontconfig documents `for_each_pattern` as "avoids the per-entry
/// clone that `list` incurs"; it has no early exit, so this keeps the first
/// hit and ignores the rest — one clone instead of N.
5599
fn first_font_in_cache(
5599
    fc_cache: &FcFontCache,
5599
) -> Option<(rust_fontconfig::FcPattern, FontId)> {
5599
    let mut first = None;
317151
    fc_cache.for_each_pattern(|pattern, id| {
317151
        if first.is_none() {
5599
            first = Some((pattern.clone(), *id));
311552
        }
317151
    });
5599
    first
5599
}
/// WEB-LIFT last resort, applied LIFT-SAFELY. The lifted backend drops in-place
/// mutations made through `BTreeMap::values_mut()` (the pushed `FontMatch` is silently
/// lost — same class as the cascade `From` mapped-collect drop) and mis-lifts the
/// `pattern.unicode_ranges.clone()` Vec-clone. So this rebuilds the map with an explicit
/// `for` loop (no `values_mut`) and appends a coverage-agnostic fallback using
/// `Vec::new()` ranges (the convention already used across this file for "no specific
/// range restriction"). Applied on BOTH resolver return paths — the fast path otherwise
/// returns chains with no last resort at all, so when the lifted
/// `query_matches`/`find_unicode_fallbacks` yields an empty chain even though a fallback
/// font IS registered, the text node measures 0 → `LayoutError::InvalidTree`.
5599
fn ensure_chains_nonempty(resolved: &mut ResolvedFontChains, fc_cache: &FcFontCache) {
5599
    let Some((_pattern, fallback_id)) = first_font_in_cache(fc_cache) else {
        return;
    };
5599
    let keys: Vec<FontChainKeyOrRef> = resolved.chains.keys().cloned().collect();
5599
    let mut rebuilt: HashMap<FontChainKeyOrRef, FontFallbackChain> =
5599
        HashMap::new();
5599
    let mut last_resort = 0usize;
10333
    for key in keys {
4734
        if let Some(mut chain) = resolved.chains.remove(&key) {
103602
            let total = chain.css_fallbacks.iter().map(|g| g.fonts.len()).sum::<usize>()
4734
                + chain.unicode_fallbacks.len();
4734
            if total == 0 {
                // NOT SILENT: this chain matched nothing at all. Every such
                // chain gets the SAME arbitrary `fallback_id` — which is
                // precisely how N distinct font-families collapsed onto one
                // FontId. It still renders (a missing font must never be a
                // blank screen), but it is now counted and reported.
                last_resort += 1;
                if let FontChainKeyOrRef::Chain(k) = &key {
                    eprintln!(
                        "[azul][font] LAST-RESORT fallback for font stack {:?}: nothing in \
                         the stack matched, rendering in an arbitrary system font.",
                        k.font_families
                    );
                }
                chain.unicode_fallbacks.push(rust_fontconfig::FontMatch {
                    id: fallback_id,
                    unicode_ranges: Vec::new(),
                    fallbacks: Vec::new(),
                });
4734
            }
4734
            rebuilt.insert(key, chain);
        }
    }
5599
    resolved.chains = rebuilt;
5599
    resolved.last_resort_chains = last_resort;
5599
}
/// Convenience function that collects and resolves font chains in one call
///
/// # Arguments
/// * `styled_dom` - The styled DOM to extract font stacks from
/// * `fc_cache` - The fontconfig cache to resolve fonts against
/// * `platform` - The current platform for resolving system font types
///
/// # Returns
/// A `ResolvedFontChains` containing all resolved font chains
/// Collect font stacks, register embedded fonts, and resolve font chains
/// in a single pass over the DOM nodes. Replaces the old two-pass approach
/// where `register_embedded_fonts_from_styled_dom` + `collect_and_resolve_font_chains`
/// each independently scanned all nodes.
4407
pub fn collect_and_resolve_font_chains_with_registration<T: ParsedFontTrait>(
4407
    styled_dom: &StyledDom,
4407
    fc_cache: &FcFontCache,
4407
    font_manager: &crate::text3::cache::FontManager<T>,
4407
    platform: &azul_css::system::Platform,
4407
) -> ResolvedFontChains {
4407
    let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
    // Register embedded FontRefs (from the same scan, no second pass)
4407
    for font_ref in collected.font_refs.values() {
9
        font_manager.register_embedded_font(font_ref);
9
    }
    // Fast path (rust-fontconfig 4.2): when a registry is attached
    // we can resolve each stack by cmap-probing candidate files
    // against the codepoints the DOM actually uses, instead of
    // letting `request_fonts` eagerly parse every CSS fallback
    // via allsorts. On excel.html this drops `font_chain_resolve`
    // from ~128 ms / 49 faces parsed to ~5 ms / 3 faces.
    //
    // Falls back to the legacy pattern-map resolver when:
    //   - no registry is present (offline `FcFontCache` callers)
    //   - the DOM has no text codepoints (no shaping to be done,
    //     so cmap-probing has nothing to check and partial-cover
    //     entries would be surprising)
4407
    if let Some(registry) = font_manager.registry.as_deref() {
5
        let used_chars = collect_used_codepoints_all(styled_dom);
5
        if !used_chars.is_empty() {
5
            let mut fast = resolve_font_chains_fast(
5
                &collected,
5
                registry,
5
                &used_chars,
5
                &font_manager.memory_families,
            );
5
            ensure_chains_nonempty(&mut fast, fc_cache);
5
            return fast;
        }
4402
    }
    // Legacy path: pattern-map resolver. Only reached when the
    // caller passes an `FcFontCache` without a live registry
    // (ad-hoc tests, the PDF writer, etc.).
4402
    let scripts = scripts_present_in_styled_dom(styled_dom);
4402
    let mut resolved = resolve_font_chains_with_registry(
4402
        &collected,
4402
        fc_cache,
4402
        font_manager.registry.as_deref(),
4402
        Some(&scripts),
4402
        &font_manager.memory_families,
    );
4402
    let used_chars = collect_used_codepoints(styled_dom);
4413
    for chain in resolved.chains.values_mut() {
3449
        prune_chain_to_used_chars(chain, &used_chars);
3449
    }
    // WEB-LIFT last resort (AFTER the prune, so it survives — the prune drops fonts
    // whose parsed cmap doesn't cover used_chars, which removes the registered fallback
    // before it's parsed): if a chain ended up empty, append the first registered font
    // so load_missing_for_chains finds it and text shapes instead of measuring 0.
    // LIFT-SAFE rebuild (see ensure_chains_nonempty) — the old `values_mut()` +
    // `unicode_ranges.clone()` version dropped the push in the lifted backend, leaving
    // the chain empty (web-text-min n1 measured 0xfffffffe/auto → InvalidTree).
4402
    ensure_chains_nonempty(&mut resolved, fc_cache);
4402
    resolved
4407
}
/// Fast-path resolver backed by [`FcFontRegistry::request_fonts_fast`].
///
/// Iterates `collected.font_stacks`, shapes each `(stack, weight,
/// italic, oblique)` combo into a cmap-probe request carrying the
/// DOM's codepoint set, calls the registry, and returns a
/// `ResolvedFontChains` keyed by `FontChainKeyOrRef::Chain` — the
/// same keys the legacy resolver emits, so downstream code
/// (`load_missing_for_chains`, `shape_with_font_fallback`) is
/// unchanged.
#[allow(clippy::implicit_hasher)] // internal; memory_families always uses the default hasher
45
pub fn resolve_font_chains_fast(
45
    collected: &CollectedFontStacks,
45
    registry: &rust_fontconfig::registry::FcFontRegistry,
45
    codepoints: &std::collections::BTreeSet<char>,
45
    memory_families: &HashMap<String, Vec<crate::text3::cache::MemoryFace>>,
45
) -> ResolvedFontChains {
    use rust_fontconfig::PatternMatch;
    static DBG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
45
    let dbg = *DBG.get_or_init(|| std::env::var_os("AZ_FAST_RESOLVE_DEBUG").is_some());
45
    let mut chains: HashMap<FontChainKeyOrRef, FontFallbackChain> = HashMap::new();
45
    let mut unresolved: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
162
    for font_stack in &collected.font_stacks {
117
        if font_stack.is_empty() {
            continue;
117
        }
        // (2026-06-10) Build the key through the ONE canonical constructor
        // (FontChainKey::from_selectors — first-wins dedup + the same empty-stack
        // fallback) so the stored key always matches the shaping-time lookup key.
117
        let canonical_key = FontChainKey::from_selectors(font_stack);
117
        let font_families = canonical_key.font_families.clone();
117
        let weight = font_stack[0].weight;
117
        let is_italic = font_stack[0].style == FontStyle::Italic;
117
        let is_oblique = font_stack[0].style == FontStyle::Oblique;
117
        let cache_key = FontChainKeyOrRef::Chain(FontChainKey {
117
            font_families: font_families.clone(),
117
            weight,
117
            italic: is_italic,
117
            oblique: is_oblique,
117
        });
117
        if chains.contains_key(&cache_key) {
            continue;
117
        }
117
        let italic_match = if is_italic {
            PatternMatch::True
        } else {
117
            PatternMatch::False
        };
        // ── MEMORY FONTS FIRST ──────────────────────────────────────────
        // `request_fonts_fast` only knows about fonts that exist as FILES
        // (it walks the registry's `known_paths`). A family registered via
        // `FontManager::register_named_font` (bundled embedder font, the
        // built-in mock test fonts) lives only in the `FcFontCache`'s
        // memory-font table and is INVISIBLE to it — such a family silently
        // fell through to a system fallback on every production build
        // (production always has a live registry, so it always took this
        // path). Match memory families by name here, in CSS order, and only
        // hand the remaining families to the disk probe.
117
        let (mut css_fallbacks, disk_families, mem_fallbacks) =
117
            split_memory_matches(&font_families, memory_families, weight, is_italic, is_oblique);
117
        let request = vec![(disk_families.clone(), codepoints.clone())];
117
        let mut chains_out = if disk_families.is_empty() {
            Vec::new()
        } else {
117
            registry.request_fonts_fast(&request, weight, italic_match)
        };
117
        if dbg {
            let total_fonts: usize = chains_out
                .iter()
                .map(|c| c.css_fallbacks.iter().map(|g| g.fonts.len()).sum::<usize>())
                .sum();
            eprintln!(
                "[FAST] stack {:?} w={:?} i={:?} → {} groups, {} faces",
                font_families,
                weight,
                italic_match,
                chains_out
                    .first()
                    .map_or(0, |c| c.css_fallbacks.len()),
                total_fonts,
            );
117
        }
        // Merge: memory-matched groups (in CSS order) + whatever the disk
        // probe found for the remaining families.
117
        let mut chain = chains_out.pop().unwrap_or_else(|| FontFallbackChain {
            css_fallbacks: Vec::new(),
            unicode_fallbacks: Vec::new(),
            original_stack: font_families.clone(),
        });
117
        if !css_fallbacks.is_empty() {
72
            css_fallbacks.append(&mut chain.css_fallbacks);
72
            chain.css_fallbacks = css_fallbacks;
72
        }
        // Fallback-tier faces go on the END: the disk has already had its turn,
        // so on a desktop these sit harmlessly behind the installed fonts, and
        // on a target with nothing installed they are what is left.
117
        chain.css_fallbacks.extend(mem_fallbacks);
        // A family that produced no group matched NOTHING. Record it — a
        // silently-unmatched family is the root cause of "every font-family
        // renders in the same fallback font".
16920
        for family in &font_families {
16803
            let matched = chain
16803
                .css_fallbacks
16803
                .iter()
33489
                .any(|g| g.css_name.eq_ignore_ascii_case(family) && !g.fonts.is_empty());
16803
            if !matched && !is_generic_family(family) {
16236
                unresolved.insert(family.clone());
16236
            }
        }
117
        chains.insert(cache_key, chain);
    }
45
    let out = ResolvedFontChains {
45
        chains,
45
        unresolved_families: unresolved,
45
        last_resort_chains: 0,
45
    };
45
    report_unresolved_families(&out);
45
    out
45
}
/// CSS generic families are not expected to match by name (they are
/// expanded to concrete OS families before lookup), so a missing group for
/// them is not a resolution failure worth reporting.
1297005
fn is_generic_family(family: &str) -> bool {
13957
    matches!(
1297005
        family.to_ascii_lowercase().as_str(),
1297005
        "serif"
1292353
            | "sans-serif"
1287701
            | "monospace"
1283049
            | "cursive"
1283049
            | "fantasy"
1283049
            | "system-ui"
1283048
            | "ui-serif"
1283048
            | "ui-sans-serif"
1283048
            | "ui-monospace"
1283048
            | "ui-rounded"
1283048
            | "emoji"
1283048
            | "math"
1283048
            | "fangsong"
    )
1297005
}
/// Log every family the resolver could not match, ONCE per process per
/// family name.
///
/// This is the diagnostic that was missing. Before this, a stylesheet
/// asking for `font-family: Arial` on a box with no Arial installed got a
/// system fallback and said nothing — so eight different families rendering
/// identically looked like correct behaviour to every test we had.
5601
fn report_unresolved_families(resolved: &ResolvedFontChains) {
    use std::sync::{Mutex, OnceLock};
    static SEEN: OnceLock<Mutex<std::collections::BTreeSet<String>>> = OnceLock::new();
5601
    if resolved.unresolved_families.is_empty() {
5393
        return;
208
    }
208
    let seen = SEEN.get_or_init(|| Mutex::new(std::collections::BTreeSet::new()));
208
    let Ok(mut seen) = seen.lock() else { return };
7050
    for family in &resolved.unresolved_families {
6842
        if seen.insert(family.clone()) {
1307
            eprintln!(
1307
                "[azul][font] UNRESOLVED font-family {family:?}: no font file and no \
1307
                 registered in-memory font matches this family. Text that asks for it \
1307
                 renders in a FALLBACK font. Register it with \
1307
                 FontManager::register_named_font(), or install it."
1307
            );
5537
        }
    }
5601
}
/// Legacy wrapper: collect + resolve without registration. Kept for
/// backward compatibility; defaults to the full 7-script unicode
/// fallback set.
#[must_use] pub fn collect_and_resolve_font_chains(
    styled_dom: &StyledDom,
    fc_cache: &FcFontCache,
    platform: &azul_css::system::Platform,
) -> ResolvedFontChains {
    let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
    resolve_font_chains(&collected, fc_cache, None)
}
/// Legacy wrapper: register only. Prefer `collect_and_resolve_font_chains_with_registration`.
pub fn register_embedded_fonts_from_styled_dom<T: ParsedFontTrait>(
    styled_dom: &StyledDom,
    font_manager: &crate::text3::cache::FontManager<T>,
    platform: &azul_css::system::Platform,
) {
    let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
    for font_ref in collected.font_refs.values() {
        font_manager.register_embedded_font(font_ref);
    }
}
// Font Loading Functions
use std::collections::HashSet;
use rust_fontconfig::FontId;
/// Extract all unique `FontIds` from resolved font chains
///
/// This function collects all `FontIds` that are referenced in the font chains,
/// which represents the complete set of fonts that may be needed for rendering.
9549
#[must_use] pub fn collect_font_ids_from_chains(chains: &ResolvedFontChains) -> HashSet<FontId> {
9549
    let mut font_ids = HashSet::new();
    // M12.7: hashbrown's RawIterRange (the .values() iterator below) mis-lifts
    // to wasm and loops forever on an empty map; is_empty() is len-based, so
    // bail out before iterating when there are no chains (web bare-body case).
9549
    if chains.chains.is_empty() {
2081
        return font_ids;
7468
    }
7841
    for chain in chains.chains.values() {
        // Collect from CSS fallbacks
181361
        for group in &chain.css_fallbacks {
233729
            for font in &group.fonts {
60209
                font_ids.insert(font.id);
60209
            }
        }
        // Collect from Unicode fallbacks
7870
        for font in &chain.unicode_fallbacks {
29
            font_ids.insert(font.id);
29
        }
    }
7468
    font_ids
9549
}
/// Compute which fonts need to be loaded (diff with already loaded fonts)
///
/// # Arguments
/// * `required_fonts` - Set of `FontIds` that are needed
/// * `already_loaded` - Set of `FontIds` that are already loaded
///
/// # Returns
/// Set of `FontIds` that need to be loaded
#[allow(clippy::implicit_hasher)] // internal helper; only ever called with the default-hasher HashMap/HashSet
5607
#[must_use] pub fn compute_fonts_to_load(
5607
    required_fonts: &HashSet<FontId>,
5607
    already_loaded: &HashSet<FontId>,
5607
) -> HashSet<FontId> {
    // M12.7: `.difference()` drives hashbrown's RawIterRange, which mis-lifts
    // to wasm and loops on an empty map. Nothing required → nothing to load.
5607
    if required_fonts.is_empty() {
1168
        return HashSet::new();
4439
    }
4439
    required_fonts.difference(already_loaded).copied().collect()
5607
}
/// Result of loading fonts
#[derive(Debug)]
pub struct FontLoadResult<T> {
    /// Successfully loaded fonts
    pub loaded: HashMap<FontId, T>,
    /// `FontIds` that failed to load, with error messages
    pub failed: Vec<(FontId, String)>,
}
/// Load fonts from disk using the provided loader function
///
/// This is a generic function that works with any font loading implementation.
/// The `load_fn` parameter should be a function that takes font bytes and an index,
/// and returns a parsed font or an error.
///
/// # Arguments
/// * `font_ids` - Set of `FontIds` to load
/// * `fc_cache` - The fontconfig cache to get font paths from
/// * `load_fn` - Function to load and parse font bytes
///
/// # Returns
/// A `FontLoadResult` containing successfully loaded fonts and any failures
#[allow(clippy::implicit_hasher)] // internal helper; only ever called with the default-hasher HashMap/HashSet
3179
pub fn load_fonts_from_disk<T, F>(
3179
    font_ids: &HashSet<FontId>,
3179
    fc_cache: &FcFontCache,
3179
    load_fn: F,
3179
) -> FontLoadResult<T>
3179
where
3179
    // Bytes come in as `Arc<FontBytes>` so the loader can retain
3179
    // them cheaply (one `Arc::clone` per retained copy). On disk the
3179
    // backing is an mmap, so untouched glyf/CFF pages don't count
3179
    // toward RSS — the layout shaper only faults in pages it reads.
3179
    F: Fn(
3179
        std::sync::Arc<rust_fontconfig::FontBytes>,
3179
        usize,
3179
    ) -> Result<T, crate::text3::cache::LayoutError>,
{
3179
    let mut loaded = HashMap::new();
3179
    let mut failed = Vec::new();
16777
    for font_id in font_ids {
        // Get font bytes from fc_cache as a shared mmap. Faces backed
        // by the same .ttc all observe the same `Arc<FontBytes>` via
        // rust_fontconfig's `shared_bytes` dedup.
13598
        let Some(font_bytes) = fc_cache.get_font_bytes(font_id) else {
16
            failed.push((
16
                *font_id,
16
                format!("Could not get font bytes for {font_id:?}"),
16
            ));
16
            continue;
        };
        // Get font index (for font collections like .ttc files)
13582
        let font_index = fc_cache
13582
            .get_font_by_id(font_id)
13582
            .map_or(0, |source| match source {
13427
                rust_fontconfig::OwnedFontSource::Disk(path) => path.font_index,
155
                rust_fontconfig::OwnedFontSource::Memory(font) => font.font_index,
13582
            });
        // Load the font using the provided function
13582
        match load_fn(font_bytes, font_index) {
13582
            Ok(font) => {
13582
                loaded.insert(*font_id, font);
13582
            }
            Err(e) => {
                failed.push((
                    *font_id,
                    format!("Failed to parse font {font_id:?}: {e:?}"),
                ));
            }
        }
    }
3179
    FontLoadResult { loaded, failed }
3179
}
/// Convenience function to load all required fonts for a styled DOM
///
/// This function:
/// 1. Collects all font stacks from the DOM
/// 2. Resolves them to font chains
/// 3. Extracts all required `FontIds`
/// 4. Computes which fonts need to be loaded (diff with already loaded)
/// 5. Loads the missing fonts
///
/// # Arguments
/// * `styled_dom` - The styled DOM to extract font requirements from
/// * `fc_cache` - The fontconfig cache
/// * `already_loaded` - Set of `FontIds` that are already loaded
/// * `load_fn` - Function to load and parse font bytes
/// * `platform` - The current platform for resolving system font types
///
/// # Returns
/// A tuple of (`ResolvedFontChains`, `FontLoadResult`)
#[allow(clippy::implicit_hasher)] // internal helper; only ever called with the default-hasher HashMap/HashSet
pub fn resolve_and_load_fonts<T, F>(
    styled_dom: &StyledDom,
    fc_cache: &FcFontCache,
    already_loaded: &HashSet<FontId>,
    load_fn: F,
    platform: &azul_css::system::Platform,
) -> (ResolvedFontChains, FontLoadResult<T>)
where
    F: Fn(
        std::sync::Arc<rust_fontconfig::FontBytes>,
        usize,
    ) -> Result<T, crate::text3::cache::LayoutError>,
{
    // Step 1-2: Collect and resolve font chains
    let chains = collect_and_resolve_font_chains(styled_dom, fc_cache, platform);
    // Step 3: Extract all required FontIds
    let required_fonts = collect_font_ids_from_chains(&chains);
    // Step 4: Compute diff
    let fonts_to_load = compute_fonts_to_load(&required_fonts, already_loaded);
    // Step 5: Load missing fonts
    let load_result = load_fonts_from_disk(&fonts_to_load, fc_cache, load_fn);
    (chains, load_result)
}
// ============================================================================
// Scrollbar Style Getters
// ============================================================================
use azul_css::props::style::scrollbar::{
    LayoutScrollbarWidth, ScrollbarVisibilityMode, StyleScrollbarColor,
};
/// Computed scrollbar style for a node.
///
/// All visual defaults (colors, width) come from the UA CSS conditional rules
/// in `core/src/ua_css.rs` — individual `CssPropertyWithConditions` entries for
/// `scrollbar-color` and `scrollbar-width`, keyed on `@os` / `@theme`.
///
/// Overlay behaviour (fade timing, visibility, clip) is derived from the
/// resolved `scrollbar-width` mode:
///   - `thin`  → overlay:  fade 500/200 ms, `WhenScrolling`, clip = true
///   - `auto`  → classic:  no fade, `Always`, clip = false
///   - `none`  → hidden:   no fade, `Always`, clip = false
///
/// Per-node CSS overrides (in priority order):
///   1. `-azul-scrollbar-style`  (full `ScrollbarInfo` override)
///   2. `scrollbar-width`        (overrides width + overlay mode)
///   3. `scrollbar-color`        (overrides thumb / track colours)
#[derive(Copy, Debug, Clone)]
pub struct ComputedScrollbarStyle {
    /// The scrollbar width mode (auto/thin/none)
    pub width_mode: LayoutScrollbarWidth,
    /// Visual width in pixels — used for rendering track + thumb.
    /// Non-zero even for overlay scrollbars.
    pub visual_width_px: f32,
    /// Reserve width in pixels — layout space subtracted from content area.
    /// 0 for overlay scrollbars, equal to `visual_width_px` for legacy.
    pub reserve_width_px: f32,
    /// Thumb color
    pub thumb_color: ColorU,
    /// Track color
    pub track_color: ColorU,
    /// Button color (for scroll arrows)
    pub button_color: ColorU,
    /// Corner color (where scrollbars meet)
    pub corner_color: ColorU,
    /// Whether to clip the scrollbar to the container's border-radius
    pub clip_to_container_border: bool,
    /// Delay in ms before scrollbar starts fading out (0 = never fade)
    pub fade_delay_ms: u32,
    /// Duration of fade-out animation in ms (0 = instant)
    pub fade_duration_ms: u32,
    /// Scrollbar visibility mode (always / when-scrolling / auto)
    pub visibility: ScrollbarVisibilityMode,
    /// Whether to show top/bottom (or left/right) arrow buttons.
    /// When false, the track spans the entire scrollbar length.
    pub show_scroll_buttons: bool,
    /// Size of each arrow button in px (square: width = height).
    /// Only used when `show_scroll_buttons == true`.
    pub scroll_button_size_px: f32,
    /// Whether to show the corner rect where V and H scrollbars meet.
    pub show_corner_rect: bool,
    /// Thumb color when hovered (None = use `thumb_color`)
    pub thumb_color_hover: Option<ColorU>,
    /// Thumb color when pressed/active (None = use `thumb_color`)
    pub thumb_color_active: Option<ColorU>,
    /// Track color when hovered (None = use `track_color`)
    pub track_color_hover: Option<ColorU>,
    /// Visual width when hovered (None = use `visual_width_px`)
    pub visual_width_px_hover: Option<f32>,
    /// Visual width when pressed (None = use `visual_width_px`)
    pub visual_width_px_active: Option<f32>,
}
impl Default for ComputedScrollbarStyle {
148
    fn default() -> Self {
        // Evaluate UA CSS rules with a default context (no OS info).
        // Picks the unconditional fallback: classic light, auto width.
148
        let ctx = azul_css::dynamic_selector::DynamicSelectorContext::default();
148
        let ua = azul_core::ua_css::evaluate_ua_scrollbar_css(&ctx);
148
        Self::from_ua_resolved(&ua)
148
    }
}
impl ComputedScrollbarStyle {
    /// Build from resolved UA scrollbar CSS properties.
    ///
    /// Each property is read individually from the resolved UA CSS.
661543
    fn from_ua_resolved(ua: &azul_core::ua_css::ResolvedUaScrollbar) -> Self {
661543
        let width_mode = ua.width;
661543
        let visibility = ua.visibility;
661543
        let fade_delay_ms = ua.fade_delay.ms;
661543
        let fade_duration_ms = ua.fade_duration.ms;
661543
        let visual_width_px = match width_mode {
5
            LayoutScrollbarWidth::Thin => SCROLLBAR_WIDTH_THIN,
661535
            LayoutScrollbarWidth::Auto => SCROLLBAR_WIDTH_AUTO,
3
            LayoutScrollbarWidth::None => 0.0,
        };
        // Overlay scrollbars don't reserve layout space and hide buttons / corner.
661543
        let is_overlay = visibility == ScrollbarVisibilityMode::WhenScrolling;
661543
        let reserve_width_px = if is_overlay { 0.0 } else { visual_width_px };
661543
        let show_scroll_buttons = !is_overlay;
661543
        let scroll_button_size_px = if is_overlay { 0.0 } else { visual_width_px };
661543
        let show_corner_rect = !is_overlay;
661543
        let (thumb_color, track_color) = match ua.color {
661531
            StyleScrollbarColor::Custom(c) => (c.thumb, c.track),
12
            StyleScrollbarColor::Auto => (ColorU::TRANSPARENT, ColorU::TRANSPARENT),
        };
        // Compute hover / active variants:
        // Hover: lighten thumb, widen by +SCROLLBAR_HOVER_EXPAND_PX
        // Active: darken thumb, widen by +SCROLLBAR_HOVER_EXPAND_PX
661543
        let thumb_hover = ColorU {
661543
            r: thumb_color.r.saturating_add(THUMB_HOVER_LIGHTEN),
661543
            g: thumb_color.g.saturating_add(THUMB_HOVER_LIGHTEN),
661543
            b: thumb_color.b.saturating_add(THUMB_HOVER_LIGHTEN),
661543
            a: thumb_color.a.saturating_add(THUMB_HOVER_ALPHA_ADD),
661543
        };
661543
        let thumb_active = ColorU {
661543
            r: thumb_color.r.saturating_sub(THUMB_ACTIVE_DARKEN),
661543
            g: thumb_color.g.saturating_sub(THUMB_ACTIVE_DARKEN),
661543
            b: thumb_color.b.saturating_sub(THUMB_ACTIVE_DARKEN),
661543
            a: 255,
661543
        };
661543
        let track_hover = ColorU {
661543
            r: track_color.r,
661543
            g: track_color.g,
661543
            b: track_color.b,
661543
            a: track_color.a.saturating_add(THUMB_HOVER_ALPHA_ADD),
661543
        };
661543
        let hover_width = visual_width_px + SCROLLBAR_HOVER_EXPAND_PX;
661543
        let active_width = visual_width_px + SCROLLBAR_HOVER_EXPAND_PX;
661543
        Self {
661543
            width_mode,
661543
            visual_width_px,
661543
            reserve_width_px,
661543
            thumb_color,
661543
            track_color,
661543
            button_color: ColorU::TRANSPARENT,
661543
            corner_color: ColorU::TRANSPARENT,
661543
            clip_to_container_border: is_overlay,
661543
            fade_delay_ms,
661543
            fade_duration_ms,
661543
            visibility,
661543
            show_scroll_buttons,
661543
            scroll_button_size_px,
661543
            show_corner_rect,
661543
            thumb_color_hover: Some(thumb_hover),
661543
            thumb_color_active: Some(thumb_active),
661543
            track_color_hover: Some(track_hover),
661543
            visual_width_px_hover: Some(hover_width),
661543
            visual_width_px_active: Some(active_width),
661543
        }
661543
    }
}
/// Get the computed scrollbar style for a node.
///
/// Resolution order (later wins):
///   1. UA scrollbar CSS (`CssPropertyWithConditions` in `ua_css.rs`,
///      evaluated via `@os` / `@theme` conditions)
///   2. CSS `-azul-scrollbar-style` (full `ScrollbarInfo` customisation)
///   3. CSS `scrollbar-width`  (overrides width only)
///   4. CSS `scrollbar-color`  (overrides thumb / track colours)
///   5. CSS `-azul-scrollbar-visibility` (overrides visibility + clip)
///   6. CSS `-azul-scrollbar-fade-delay` (overrides fade delay)
///   7. CSS `-azul-scrollbar-fade-duration` (overrides fade duration)
///
/// When `system_style` is `None`, falls back to the unconditional UA rule
/// (classic light scrollbar).
#[allow(clippy::too_many_lines)] // large but cohesive: single-purpose layout/render/parse routine (one branch per case)
661381
#[must_use] pub fn get_scrollbar_style(
661381
    styled_dom: &StyledDom,
661381
    node_id: NodeId,
661381
    node_state: &StyledNodeState,
661381
    system_style: Option<&azul_css::system::SystemStyle>,
661381
) -> ComputedScrollbarStyle {
661381
    let node_data = &styled_dom.node_data.as_container()[node_id];
    // Step 1: Evaluate UA scrollbar CSS using the DynamicSelector system.
661381
    let ctx = system_style.map_or_else(
        azul_css::dynamic_selector::DynamicSelectorContext::default,
        azul_css::dynamic_selector::DynamicSelectorContext::from_system_style,
    );
661381
    let ua = azul_core::ua_css::evaluate_ua_scrollbar_css(&ctx);
661381
    let result = ComputedScrollbarStyle::from_ua_resolved(&ua);
    // FAST PATH: 99% of nodes have no scrollbar CSS. Bail before walking 8 × cascade.
661381
    if node_state.is_normal() {
661369
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
661368
            if !cc.has_scrollbar_css(node_id.index()) {
661368
                return result;
            }
1
        }
12
    }
13
    let mut result = result;
    // Step 2: Check individual scrollbar part backgrounds
13
    if let Some(track) = styled_dom
13
        .css_property_cache
13
        .ptr
13
        .get_scrollbar_track(node_data, &node_id, node_state)
13
        .and_then(|v| v.get_property())
    {
        result.track_color = extract_color_from_background(track);
13
    }
13
    if let Some(thumb) = styled_dom
13
        .css_property_cache
13
        .ptr
13
        .get_scrollbar_thumb(node_data, &node_id, node_state)
13
        .and_then(|v| v.get_property())
    {
        result.thumb_color = extract_color_from_background(thumb);
13
    }
13
    if let Some(button) = styled_dom
13
        .css_property_cache
13
        .ptr
13
        .get_scrollbar_button(node_data, &node_id, node_state)
13
        .and_then(|v| v.get_property())
    {
        result.button_color = extract_color_from_background(button);
13
    }
13
    if let Some(corner) = styled_dom
13
        .css_property_cache
13
        .ptr
13
        .get_scrollbar_corner(node_data, &node_id, node_state)
13
        .and_then(|v| v.get_property())
    {
        result.corner_color = extract_color_from_background(corner);
13
    }
    // Step 3: Check for scrollbar-width (overrides width only, not overlay)
13
    if let Some(scrollbar_width) = styled_dom
13
        .css_property_cache
13
        .ptr
13
        .get_scrollbar_width(node_data, &node_id, node_state)
13
        .and_then(|v| v.get_property())
    {
        result.width_mode = *scrollbar_width;
        let w = match scrollbar_width {
            LayoutScrollbarWidth::Auto => SCROLLBAR_WIDTH_AUTO,
            LayoutScrollbarWidth::Thin => SCROLLBAR_WIDTH_THIN,
            LayoutScrollbarWidth::None => 0.0,
        };
        result.visual_width_px = w;
        if result.visibility != ScrollbarVisibilityMode::WhenScrolling {
            result.reserve_width_px = w;
        }
13
    }
    // Step 4: Check for scrollbar-color (overrides thumb/track colors)
13
    if let Some(scrollbar_color) = styled_dom
13
        .css_property_cache
13
        .ptr
13
        .get_scrollbar_color(node_data, &node_id, node_state)
13
        .and_then(|v| v.get_property())
    {
        match scrollbar_color {
            StyleScrollbarColor::Auto => { /* keep */ }
            StyleScrollbarColor::Custom(custom) => {
                result.thumb_color = custom.thumb;
                result.track_color = custom.track;
            }
        }
13
    }
    // Step 5: Check for -azul-scrollbar-visibility
13
    if let Some(vis) = styled_dom
13
        .css_property_cache
13
        .ptr
13
        .get_scrollbar_visibility(node_data, &node_id, node_state)
13
        .and_then(|v| v.get_property())
    {
        result.visibility = *vis;
        result.clip_to_container_border = *vis == ScrollbarVisibilityMode::WhenScrolling;
        // Overlay mode: no reserved layout space, hide buttons and corner
        let is_overlay = *vis == ScrollbarVisibilityMode::WhenScrolling;
        if is_overlay {
            result.reserve_width_px = 0.0;
            result.show_scroll_buttons = false;
            result.scroll_button_size_px = 0.0;
            result.show_corner_rect = false;
        } else {
            result.reserve_width_px = result.visual_width_px;
        }
13
    }
    // Step 6: Check for -azul-scrollbar-fade-delay
13
    if let Some(delay) = styled_dom
13
        .css_property_cache
13
        .ptr
13
        .get_scrollbar_fade_delay(node_data, &node_id, node_state)
13
        .and_then(|v| v.get_property())
    {
        result.fade_delay_ms = delay.ms;
13
    }
    // Step 7: Check for -azul-scrollbar-fade-duration
13
    if let Some(dur) = styled_dom
13
        .css_property_cache
13
        .ptr
13
        .get_scrollbar_fade_duration(node_data, &node_id, node_state)
13
        .and_then(|v| v.get_property())
    {
        result.fade_duration_ms = dur.ms;
13
    }
13
    result
661381
}
/// Cached wrapper for [`get_scrollbar_style`] that reuses the
/// memo stored on `LayoutContext`.
///
/// The underlying call performs
/// 9 cascade walks per node (track/thumb/button/corner/width/
/// color/visibility/fade-delay/fade-duration). The BFC, Taffy,
/// and display-list callers all hit the same node many times
/// inside a single layout pass, so caching turns ~21 rebuilds per
/// node into one.
///
/// Falls back to the uncached `get_scrollbar_style` when no ctx
/// is available (shouldn't happen in the current code paths).
985314
pub fn get_scrollbar_style_cached<T: ParsedFontTrait>(
985314
    ctx: &crate::solver3::LayoutContext<'_, T>,
985314
    node_id: NodeId,
985314
    node_state: &StyledNodeState,
985314
) -> ComputedScrollbarStyle {
985314
    if let Some(s) = ctx.scrollbar_style_cache.borrow().get(&node_id) {
345884
        return *s;
639430
    }
639430
    let style = get_scrollbar_style(
639430
        ctx.styled_dom,
639430
        node_id,
639430
        node_state,
639430
        ctx.system_style.as_deref(),
    );
639430
    ctx.scrollbar_style_cache
639430
        .borrow_mut()
639430
        .insert(node_id, style);
639430
    style
985314
}
/// Helper to extract a solid color from a `StyleBackgroundContent`
8
const fn extract_color_from_background(
8
    bg: &azul_css::props::style::background::StyleBackgroundContent,
8
) -> ColorU {
    use azul_css::props::style::background::StyleBackgroundContent;
8
    match bg {
5
        StyleBackgroundContent::Color(c) => *c,
3
        _ => ColorU::TRANSPARENT,
    }
8
}
/// Check if a node should clip its scrollbar to the container's border-radius
#[must_use] pub fn should_clip_scrollbar_to_border(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> bool {
    let style = get_scrollbar_style(styled_dom, node_id, node_state, None);
    style.clip_to_container_border
}
/// Get the scrollbar visual width in pixels for a node (used for rendering)
4
#[must_use] pub fn get_scrollbar_width_px(
4
    styled_dom: &StyledDom,
4
    node_id: NodeId,
4
    node_state: &StyledNodeState,
4
) -> f32 {
4
    let style = get_scrollbar_style(styled_dom, node_id, node_state, None);
4
    style.visual_width_px
4
}
/// Checks if text in a node is selectable based on CSS `user-select` property.
///
/// Returns `true` if the text can be selected (default behavior),
/// `false` if `user-select: none` is set.
292587
#[must_use] pub fn is_text_selectable(
292587
    styled_dom: &StyledDom,
292587
    node_id: NodeId,
292587
    node_state: &StyledNodeState,
292587
) -> bool {
292587
    let node_data = &styled_dom.node_data.as_container()[node_id];
292587
    styled_dom
292587
        .css_property_cache
292587
        .ptr
292587
        .get_user_select(node_data, &node_id, node_state)
292587
        .and_then(|v| v.get_property())
292587
        .is_none_or(|us| *us != StyleUserSelect::None) // Default: text is selectable
292587
}
/// Checks if a node has the `contenteditable` attribute set directly.
///
/// Returns `true` if:
/// - The node has `contenteditable: true` set via `.set_contenteditable(true)`
/// - OR the node has `contenteditable` attribute set to `true`
///
/// This does NOT check inheritance - use `is_node_contenteditable_inherited` for that.
13
#[must_use] pub fn is_node_contenteditable(styled_dom: &StyledDom, node_id: NodeId) -> bool {
    use azul_core::dom::AttributeType;
13
    let node_data = &styled_dom.node_data.as_container()[node_id];
    // First check the direct contenteditable field (primary method)
13
    if node_data.is_contenteditable() {
2
        return true;
11
    }
    // Also check the attribute for backwards compatibility
    // Only return true if the attribute value is explicitly true
11
    node_data
11
        .attributes()
11
        .as_ref()
11
        .iter()
11
        .any(|attr| matches!(attr, AttributeType::ContentEditable(true)))
13
}
// =============================================================================
// Additional ExtractPropertyValue impls (not in compact cache tier 1/2)
// =============================================================================
use azul_css::props::layout::table::{
    LayoutTableLayout, StyleBorderCollapse, StyleCaptionSide, StyleEmptyCells,
};
use azul_css::props::layout::text::LayoutTextJustify;
use azul_css::props::style::effects::StyleAspectRatio;
use azul_css::props::style::effects::StyleCursor;
use azul_css::props::style::effects::StyleObjectFit;
use azul_css::props::style::effects::StyleObjectPosition;
use azul_css::props::layout::overflow::StyleTextOverflow;
use azul_css::props::style::effects::StyleTextOrientation;
use azul_css::props::style::text::StyleHyphens;
use azul_css::props::style::text::StyleLineBreak;
use azul_css::props::style::text::StyleOverflowWrap;
use azul_css::props::style::text::StyleTextAlignLast;
use azul_css::props::style::text::StyleWordBreak;
impl ExtractPropertyValue<LayoutTextJustify> for CssProperty {
    fn extract(&self) -> Option<LayoutTextJustify> {
        match self {
            Self::TextJustify(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleHyphens> for CssProperty {
    fn extract(&self) -> Option<StyleHyphens> {
        match self {
            Self::Hyphens(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleWordBreak> for CssProperty {
    fn extract(&self) -> Option<StyleWordBreak> {
        match self {
            Self::WordBreak(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleOverflowWrap> for CssProperty {
    fn extract(&self) -> Option<StyleOverflowWrap> {
        match self {
            Self::OverflowWrap(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleLineBreak> for CssProperty {
    fn extract(&self) -> Option<StyleLineBreak> {
        match self {
            Self::LineBreak(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleTextAlignLast> for CssProperty {
    fn extract(&self) -> Option<StyleTextAlignLast> {
        match self {
            Self::TextAlignLast(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleObjectFit> for CssProperty {
    fn extract(&self) -> Option<StyleObjectFit> {
        match self {
            Self::ObjectFit(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleTextOverflow> for CssProperty {
    fn extract(&self) -> Option<StyleTextOverflow> {
        match self {
            Self::TextOverflow(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleTextOrientation> for CssProperty {
    fn extract(&self) -> Option<StyleTextOrientation> {
        match self {
            Self::TextOrientation(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleObjectPosition> for CssProperty {
    fn extract(&self) -> Option<StyleObjectPosition> {
        match self {
            Self::ObjectPosition(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleAspectRatio> for CssProperty {
    fn extract(&self) -> Option<StyleAspectRatio> {
        match self {
            Self::AspectRatio(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<LayoutTableLayout> for CssProperty {
    fn extract(&self) -> Option<LayoutTableLayout> {
        match self {
            Self::TableLayout(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleBorderCollapse> for CssProperty {
    fn extract(&self) -> Option<StyleBorderCollapse> {
        match self {
            Self::BorderCollapse(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleCaptionSide> for CssProperty {
    fn extract(&self) -> Option<StyleCaptionSide> {
        match self {
            Self::CaptionSide(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleEmptyCells> for CssProperty {
    fn extract(&self) -> Option<StyleEmptyCells> {
        match self {
            Self::EmptyCells(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleCursor> for CssProperty {
    fn extract(&self) -> Option<StyleCursor> {
        match self {
            Self::Cursor(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
// =============================================================================
// Additional macro-based getters (not covered by compact cache fast-path getters)
// =============================================================================
get_css_property!(
    get_text_justify,
    get_text_justify,
    LayoutTextJustify,
    CssPropertyType::TextJustify
);
get_css_property!(
    get_hyphens,
    get_hyphens,
    StyleHyphens,
    CssPropertyType::Hyphens
);
get_css_property!(
    get_word_break,
    get_word_break,
    StyleWordBreak,
    CssPropertyType::WordBreak
);
get_css_property!(
    get_overflow_wrap,
    get_overflow_wrap,
    StyleOverflowWrap,
    CssPropertyType::OverflowWrap
);
get_css_property!(
    get_line_break,
    get_line_break,
    StyleLineBreak,
    CssPropertyType::LineBreak
);
get_css_property!(
    get_text_align_last,
    get_text_align_last,
    StyleTextAlignLast,
    CssPropertyType::TextAlignLast
);
get_css_property!(
    get_table_layout,
    get_table_layout,
    LayoutTableLayout,
    CssPropertyType::TableLayout
);
get_css_property!(
    get_border_collapse,
    get_border_collapse,
    StyleBorderCollapse,
    CssPropertyType::BorderCollapse,
    compact = get_border_collapse
);
get_css_property!(
    get_caption_side,
    get_caption_side,
    StyleCaptionSide,
    CssPropertyType::CaptionSide
);
get_css_property!(
    get_empty_cells,
    get_empty_cells,
    StyleEmptyCells,
    CssPropertyType::EmptyCells
);
get_css_property!(
    get_cursor_property,
    get_cursor,
    StyleCursor,
    CssPropertyType::Cursor
);
// =============================================================================
// Handwritten getters (Option<T>, special logic, or non-standard returns)
// =============================================================================
/// Get height property value for IFC text layout height reference.
2
#[must_use] pub fn get_height_value(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<LayoutHeight> {
2
    let node_data = &styled_dom.node_data.as_container()[node_id];
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_height(node_data, &node_id, node_state)
2
        .and_then(|v| v.get_property())
2
        .cloned()
2
}
/// Get shape-inside property. Returns Option<ShapeInside> (cloned).
2
#[must_use] pub fn get_shape_inside(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<azul_css::props::layout::shape::ShapeInside> {
2
    let node_data = &styled_dom.node_data.as_container()[node_id];
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_shape_inside(node_data, &node_id, node_state)
2
        .and_then(|v| v.get_property())
2
        .cloned()
2
}
/// Get shape-outside property. Returns Option<ShapeOutside> (cloned).
2
#[must_use] pub fn get_shape_outside(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<azul_css::props::layout::shape::ShapeOutside> {
2
    let node_data = &styled_dom.node_data.as_container()[node_id];
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_shape_outside(node_data, &node_id, node_state)
2
        .and_then(|v| v.get_property())
2
        .cloned()
2
}
/// Get line-height as the full `StyleLineHeight` value for caller resolution.
2
#[must_use] pub fn get_line_height_value(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<azul_css::props::style::text::StyleLineHeight> {
2
    let node_data = &styled_dom.node_data.as_container()[node_id];
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_line_height(node_data, &node_id, node_state)
2
        .and_then(|v| v.get_property())
2
        .copied()
2
}
/// Get text-indent as the full `StyleTextIndent` value for caller resolution.
2
#[must_use] pub fn get_text_indent_value(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<azul_css::props::style::text::StyleTextIndent> {
2
    let node_data = &styled_dom.node_data.as_container()[node_id];
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_text_indent(node_data, &node_id, node_state)
2
        .and_then(|v| v.get_property())
2
        .copied()
2
}
/// Get column-count property. Returns Option<ColumnCount>.
2
#[must_use] pub fn get_column_count(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<azul_css::props::layout::column::ColumnCount> {
2
    let node_data = &styled_dom.node_data.as_container()[node_id];
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_column_count(node_data, &node_id, node_state)
2
        .and_then(|v| v.get_property())
2
        .copied()
2
}
/// Get initial-letter property. Returns Option<StyleInitialLetter>.
2
#[must_use] pub fn get_initial_letter(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<azul_css::props::style::text::StyleInitialLetter> {
2
    let node_data = &styled_dom.node_data.as_container()[node_id];
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_initial_letter(node_data, &node_id, node_state)
2
        .and_then(|v| v.get_property())
2
        .copied()
2
}
/// Get line-clamp property. Returns Option<StyleLineClamp>.
2
#[must_use] pub fn get_line_clamp(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<azul_css::props::style::text::StyleLineClamp> {
2
    let node_data = &styled_dom.node_data.as_container()[node_id];
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_line_clamp(node_data, &node_id, node_state)
2
        .and_then(|v| v.get_property())
2
        .copied()
2
}
/// Get hanging-punctuation property. Returns Option<StyleHangingPunctuation>.
2
#[must_use] pub fn get_hanging_punctuation(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<azul_css::props::style::text::StyleHangingPunctuation> {
2
    let node_data = &styled_dom.node_data.as_container()[node_id];
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_hanging_punctuation(node_data, &node_id, node_state)
2
        .and_then(|v| v.get_property())
2
        .copied()
2
}
/// Get text-combine-upright property. Returns Option<StyleTextCombineUpright>.
2
#[must_use] pub fn get_text_combine_upright(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<azul_css::props::style::text::StyleTextCombineUpright> {
2
    let node_data = &styled_dom.node_data.as_container()[node_id];
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_text_combine_upright(node_data, &node_id, node_state)
2
        .and_then(|v| v.get_property())
2
        .copied()
2
}
/// Get exclusion-margin value. Returns f32 (default 0.0).
2
#[must_use] pub fn get_exclusion_margin(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> f32 {
2
    let node_data = &styled_dom.node_data.as_container()[node_id];
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_exclusion_margin(node_data, &node_id, node_state)
2
        .and_then(|v| v.get_property())
2
        .map_or(0.0, |v| v.inner.get())
2
}
/// Get hyphenation-language property. Returns Option<StyleHyphenationLanguage>.
2
#[must_use] pub fn get_hyphenation_language(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<azul_css::props::style::exclusion::StyleHyphenationLanguage> {
2
    let node_data = &styled_dom.node_data.as_container()[node_id];
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_hyphenation_language(node_data, &node_id, node_state)
2
        .and_then(|v| v.get_property())
2
        .cloned()
2
}
/// Get border-spacing property.
2
#[must_use] pub fn get_border_spacing(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> azul_css::props::layout::table::LayoutBorderSpacing {
    use azul_css::props::basic::pixel::PixelValue;
    // FAST PATH: compact cache for normal state
2
    if node_state.is_normal() {
1
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1
            let h_raw = cc.get_border_spacing_h_raw(node_id.index());
1
            let v_raw = cc.get_border_spacing_v_raw(node_id.index());
            // Both 0 means no border-spacing set (default)
            // Sentinel means non-px unit → slow path
1
            if h_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
1
                && v_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
            {
1
                return azul_css::props::layout::table::LayoutBorderSpacing {
1
                    horizontal: PixelValue::px(f32::from(h_raw) / 10.0),
1
                    vertical: PixelValue::px(f32::from(v_raw) / 10.0),
1
                };
            }
        }
1
    }
    // SLOW PATH
1
    let node_data = &styled_dom.node_data.as_container()[node_id];
1
    styled_dom
1
        .css_property_cache
1
        .ptr
1
        .get_border_spacing(node_data, &node_id, node_state)
1
        .and_then(|v| v.get_property())
1
        .copied()
1
        .unwrap_or_default()
2
}
/// Get opacity value. Returns f32 (default 1.0).
///
/// GPU fast path: the compact cache encodes opacity as a u8 (0-254, 255 = unset).
/// Avoids the 4-pseudo-state × 6-layer cascade walk for animations reading opacity
/// across every node each frame.
1311610
#[must_use] pub fn get_opacity(styled_dom: &StyledDom, node_id: NodeId, node_state: &StyledNodeState) -> f32 {
    // FAST PATH: compact cache for normal state
1311610
    if node_state.is_normal() {
1311586
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1311585
            let raw = cc.get_opacity_raw(node_id.index());
1311585
            if raw == azul_css::compact_cache::OPACITY_SENTINEL {
1311531
                return 1.0;
54
            }
54
            return f32::from(raw) / 254.0;
1
        }
24
    }
    // SLOW PATH: fall back to cascade walk (state != normal, or no compact cache)
25
    let node_data = &styled_dom.node_data.as_container()[node_id];
25
    styled_dom
25
        .css_property_cache
25
        .ptr
25
        .get_opacity(node_data, &node_id, node_state)
25
        .and_then(|v| v.get_property())
25
        .map_or(1.0, |v| v.inner.normalized())
1311610
}
/// Get filter property. Returns Option with cloned filter list.
2
#[must_use] pub fn get_filter(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<azul_css::props::style::filter::StyleFilterVec> {
2
    if node_state.is_normal() {
1
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1
            if !cc.has_filter(node_id.index()) {
1
                return None;
            }
        }
1
    }
1
    let node_data = &styled_dom.node_data.as_container()[node_id];
1
    styled_dom
1
        .css_property_cache
1
        .ptr
1
        .get_filter(node_data, &node_id, node_state)
1
        .and_then(|v| v.get_property())
1
        .cloned()
2
}
/// Get backdrop-filter property. Returns Option with cloned filter list.
2
#[must_use] pub fn get_backdrop_filter(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<azul_css::props::style::filter::StyleFilterVec> {
2
    if node_state.is_normal() {
1
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1
            if !cc.has_backdrop_filter(node_id.index()) {
1
                return None;
            }
        }
1
    }
1
    let node_data = &styled_dom.node_data.as_container()[node_id];
1
    styled_dom
1
        .css_property_cache
1
        .ptr
1
        .get_backdrop_filter(node_data, &node_id, node_state)
1
        .and_then(|v| v.get_property())
1
        .cloned()
2
}
/// Compact-cache negative fast path for all 4 box-shadow sides.
/// Most nodes have no shadow; cheap to check one bit vs. 4 cascade walks.
#[inline]
1512880
fn box_shadow_fast_bail(
1512880
    styled_dom: &StyledDom,
1512880
    node_id: NodeId,
1512880
    node_state: &StyledNodeState,
1512880
) -> bool {
1512880
    if !node_state.is_normal() {
24
        return false;
1512856
    }
1512856
    if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1512852
        return !cc.has_box_shadow(node_id.index());
4
    }
4
    false
1512880
}
/// Get box-shadow for left side. Returns Option<StyleBoxShadow> (cloned).
378220
#[must_use] pub fn get_box_shadow_left(
378220
    styled_dom: &StyledDom,
378220
    node_id: NodeId,
378220
    node_state: &StyledNodeState,
378220
) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
378220
    if box_shadow_fast_bail(styled_dom, node_id, node_state) {
378177
        return None;
43
    }
43
    let node_data = &styled_dom.node_data.as_container()[node_id];
43
    styled_dom
43
        .css_property_cache
43
        .ptr
43
        .get_box_shadow_left(node_data, &node_id, node_state)
43
        .and_then(|v| v.get_property())
43
        .map(|v| (**v))
378220
}
/// Get box-shadow for right side. Returns Option<StyleBoxShadow> (cloned).
378220
#[must_use] pub fn get_box_shadow_right(
378220
    styled_dom: &StyledDom,
378220
    node_id: NodeId,
378220
    node_state: &StyledNodeState,
378220
) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
378220
    if box_shadow_fast_bail(styled_dom, node_id, node_state) {
378177
        return None;
43
    }
43
    let node_data = &styled_dom.node_data.as_container()[node_id];
43
    styled_dom
43
        .css_property_cache
43
        .ptr
43
        .get_box_shadow_right(node_data, &node_id, node_state)
43
        .and_then(|v| v.get_property())
43
        .map(|v| (**v))
378220
}
/// Get box-shadow for top side. Returns Option<StyleBoxShadow> (cloned).
378220
#[must_use] pub fn get_box_shadow_top(
378220
    styled_dom: &StyledDom,
378220
    node_id: NodeId,
378220
    node_state: &StyledNodeState,
378220
) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
378220
    if box_shadow_fast_bail(styled_dom, node_id, node_state) {
378177
        return None;
43
    }
43
    let node_data = &styled_dom.node_data.as_container()[node_id];
43
    styled_dom
43
        .css_property_cache
43
        .ptr
43
        .get_box_shadow_top(node_data, &node_id, node_state)
43
        .and_then(|v| v.get_property())
43
        .map(|v| (**v))
378220
}
/// Get box-shadow for bottom side. Returns Option<StyleBoxShadow> (cloned).
378220
#[must_use] pub fn get_box_shadow_bottom(
378220
    styled_dom: &StyledDom,
378220
    node_id: NodeId,
378220
    node_state: &StyledNodeState,
378220
) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
378220
    if box_shadow_fast_bail(styled_dom, node_id, node_state) {
378177
        return None;
43
    }
43
    let node_data = &styled_dom.node_data.as_container()[node_id];
43
    styled_dom
43
        .css_property_cache
43
        .ptr
43
        .get_box_shadow_bottom(node_data, &node_id, node_state)
43
        .and_then(|v| v.get_property())
43
        .map(|v| (**v))
378220
}
/// Get text-shadow property. Returns Option<StyleBoxShadow> (cloned).
2
#[must_use] pub fn get_text_shadow(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
2
    if node_state.is_normal() {
1
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1
            if !cc.has_text_shadow(node_id.index()) {
1
                return None;
            }
        }
1
    }
1
    let node_data = &styled_dom.node_data.as_container()[node_id];
1
    styled_dom
1
        .css_property_cache
1
        .ptr
1
        .get_text_shadow(node_data, &node_id, node_state)
1
        .and_then(|v| v.get_property())
1
        .map(|v| (**v))
2
}
/// Get transform property. Returns Option (non-empty transform list, cloned).
///
/// GPU fast path: the compact cache keeps a `has_transform` flag. If unset,
/// skips the cascade walk entirely — which is the overwhelming case since most
/// nodes have no transform. Only nodes that actually have a transform pay the
/// slow-walk cost to retrieve the parsed value.
1302378
#[must_use] pub fn get_transform(
1302378
    styled_dom: &StyledDom,
1302378
    node_id: NodeId,
1302378
    node_state: &StyledNodeState,
1302378
) -> Option<azul_css::props::style::transform::StyleTransformVec> {
    // FAST PATH: bit check in compact cache
1302378
    if node_state.is_normal() {
1302361
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1302361
            if !cc.has_transform(node_id.index()) {
1302359
                return None;
2
            }
            // has_transform set → fall through to cascade walk for the value
        }
17
    }
19
    let node_data = &styled_dom.node_data.as_container()[node_id];
19
    styled_dom
19
        .css_property_cache
19
        .ptr
19
        .get_transform(node_data, &node_id, node_state)
19
        .and_then(|v| v.get_property())
19
        .cloned()
1302378
}
/// Get counter-reset property. Returns Option<CounterReset> (cloned).
2
#[must_use] pub fn get_counter_reset(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<azul_css::props::style::content::CounterReset> {
2
    let node_data = &styled_dom.node_data.as_container()[node_id];
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_counter_reset(node_data, &node_id, node_state)
2
        .and_then(|v| v.get_property())
2
        .cloned()
2
}
/// Get counter-increment property. Returns Option<CounterIncrement> (cloned).
2
#[must_use] pub fn get_counter_increment(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<azul_css::props::style::content::CounterIncrement> {
2
    let node_data = &styled_dom.node_data.as_container()[node_id];
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_counter_increment(node_data, &node_id, node_state)
2
        .and_then(|v| v.get_property())
2
        .cloned()
2
}
/// W3C-conformant contenteditable inheritance check.
///
/// In the W3C model, the `contenteditable` attribute is **inherited**:
/// - A node is editable if it has `contenteditable="true"` set directly
/// - OR if its parent has `isContentEditable` as true
/// - UNLESS the node explicitly sets `contenteditable="false"`
///
/// This function traverses up the DOM tree to determine editability.
///
/// # Returns
///
/// - `true` if the node is editable (either directly or via inheritance)
/// - `false` if the node is not editable or has `contenteditable="false"`
///
/// # Example
///
/// ```html
/// <div contenteditable="true">
///   A                              <!-- editable (inherited) -->
///   <div contenteditable="false">
///     B                            <!-- NOT editable (explicitly false) -->
///   </div>
///   C                              <!-- editable (inherited) -->
/// </div>
/// ```
447443
#[must_use] pub fn is_node_contenteditable_inherited(styled_dom: &StyledDom, node_id: NodeId) -> bool {
    use azul_core::dom::AttributeType;
447443
    let node_data_container = styled_dom.node_data.as_container();
447443
    let hierarchy = styled_dom.node_hierarchy.as_container();
447443
    let mut current_node_id = Some(node_id);
1550603
    while let Some(nid) = current_node_id {
1110763
        let node_data = &node_data_container[nid];
        // First check the direct contenteditable field (set via set_contenteditable())
        // This takes precedence as it's the API-level setting
1110763
        if node_data.is_contenteditable() {
7603
            return true;
1103160
        }
        // Then check for explicit contenteditable attribute on this node
        // This handles HTML-style contenteditable="true" or contenteditable="false"
1103160
        for attr in node_data.attributes().as_ref() {
9819
            if let AttributeType::ContentEditable(is_editable) = attr {
                // If explicitly set to true, node is editable
                // If explicitly set to false, node is NOT editable (blocks inheritance)
                return *is_editable;
9819
            }
        }
        // No explicit setting on this node, check parent for inheritance
1103160
        current_node_id = hierarchy.get(nid).and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id);
    }
    // Reached root without finding contenteditable - not editable
439840
    false
447443
}
/// Find the contenteditable ancestor of a node.
///
/// When focus lands on a text node inside a contenteditable container,
/// we need to find the actual container that has the `contenteditable` attribute.
///
/// # Returns
///
/// - `Some(node_id)` of the contenteditable ancestor (may be the node itself)
/// - `None` if no contenteditable ancestor exists
6
#[must_use] pub fn find_contenteditable_ancestor(styled_dom: &StyledDom, node_id: NodeId) -> Option<NodeId> {
    use azul_core::dom::AttributeType;
6
    let node_data_container = styled_dom.node_data.as_container();
6
    let hierarchy = styled_dom.node_hierarchy.as_container();
6
    let mut current_node_id = Some(node_id);
13
    while let Some(nid) = current_node_id {
9
        let node_data = &node_data_container[nid];
        // First check the direct contenteditable field (set via set_contenteditable())
9
        if node_data.is_contenteditable() {
2
            return Some(nid);
7
        }
        // Then check for contenteditable attribute on this node
7
        for attr in node_data.attributes().as_ref() {
            if let AttributeType::ContentEditable(is_editable) = attr {
                if *is_editable {
                    return Some(nid);
                }
                // Explicitly not editable - stop search
                return None;
            }
        }
        // Check parent
7
        current_node_id = hierarchy.get(nid).and_then(azul_core::styled_dom::NodeHierarchyItem::parent_id);
    }
4
    None
6
}
// --- Taffy bridge property getters ---
//
// These getters return `Option<CssPropertyValue<T>>` (cloned from cache) for use
// by taffy_bridge.rs. The conversion from CssPropertyValue to taffy types is done
// in taffy_bridge.rs itself. Routing access through these functions centralizes
// all CSS property lookups for future cache optimizations (e.g., FxHash migration).
macro_rules! get_css_property_value {
    ($fn_name:ident, $cache_method:ident, $ret_type:ty) => {
54872
        #[must_use] pub fn $fn_name(
54872
            styled_dom: &StyledDom,
54872
            node_id: NodeId,
54872
            node_state: &StyledNodeState,
54872
        ) -> Option<$ret_type> {
54872
            let node_data = &styled_dom.node_data.as_container()[node_id];
54872
            styled_dom
54872
                .css_property_cache
54872
                .ptr
54872
                .$cache_method(node_data, &node_id, node_state)
54872
                .cloned()
54872
        }
    };
}
// Flexbox properties
get_css_property_value!(
    get_flex_direction_prop,
    get_flex_direction,
    LayoutFlexDirectionValue
);
get_css_property_value!(get_flex_wrap_prop, get_flex_wrap, LayoutFlexWrapValue);
get_css_property_value!(get_flex_grow_prop, get_flex_grow, LayoutFlexGrowValue);
get_css_property_value!(get_flex_shrink_prop, get_flex_shrink, LayoutFlexShrinkValue);
get_css_property_value!(get_flex_basis_prop, get_flex_basis, LayoutFlexBasisValue);
// Alignment properties
get_css_property_value!(get_align_items_prop, get_align_items, LayoutAlignItemsValue);
get_css_property_value!(get_align_self_prop, get_align_self, LayoutAlignSelfValue);
get_css_property_value!(
    get_align_content_prop,
    get_align_content,
    LayoutAlignContentValue
);
get_css_property_value!(
    get_justify_content_prop,
    get_justify_content,
    LayoutJustifyContentValue
);
get_css_property_value!(
    get_justify_items_prop,
    get_justify_items,
    LayoutJustifyItemsValue
);
get_css_property_value!(
    get_justify_self_prop,
    get_justify_self,
    LayoutJustifySelfValue
);
// Gap
get_css_property_value!(get_gap_prop, get_gap, LayoutGapValue);
// Grid properties
get_css_property_value!(
    get_grid_template_rows_prop,
    get_grid_template_rows,
    LayoutGridTemplateRowsValue
);
get_css_property_value!(
    get_grid_template_columns_prop,
    get_grid_template_columns,
    LayoutGridTemplateColumnsValue
);
get_css_property_value!(
    get_grid_auto_rows_prop,
    get_grid_auto_rows,
    LayoutGridAutoRowsValue
);
get_css_property_value!(
    get_grid_auto_columns_prop,
    get_grid_auto_columns,
    LayoutGridAutoColumnsValue
);
get_css_property_value!(
    get_grid_auto_flow_prop,
    get_grid_auto_flow,
    LayoutGridAutoFlowValue
);
get_css_property_value!(get_grid_column_prop, get_grid_column, LayoutGridColumnValue);
get_css_property_value!(get_grid_row_prop, get_grid_row, LayoutGridRowValue);
/// Get grid-template-areas property.
///
/// Uses the generic `get_property()` since `CssPropertyCache` lacks a specific getter.
/// Returns the inner `GridTemplateAreas` value (already unwrapped from `CssPropertyValue`).
2
#[must_use] pub fn get_grid_template_areas_prop(
2
    styled_dom: &StyledDom,
2
    node_id: NodeId,
2
    node_state: &StyledNodeState,
2
) -> Option<GridTemplateAreas> {
2
    let node_data = &styled_dom.node_data.as_container()[node_id];
2
    styled_dom
2
        .css_property_cache
2
        .ptr
2
        .get_property(
2
            node_data,
2
            &node_id,
2
            node_state,
2
            &CssPropertyType::GridTemplateAreas,
2
        )
2
        .and_then(|p| {
            if let CssProperty::GridTemplateAreas(v) = p {
                v.get_property().cloned()
            } else {
                None
            }
        })
2
}
/// Get clip-path property. Returns the `ClipPath` value for the node.
///
/// CSS Masking Module Level 1, section 3:
/// The clip-path property creates a clipping region that determines which parts
/// of an element are visible. Returns None for `clip-path: none` (default).
666062
#[must_use] pub fn get_clip_path(
666062
    styled_dom: &StyledDom,
666062
    node_id: NodeId,
666062
    node_state: &StyledNodeState,
666062
) -> Option<azul_css::props::layout::shape::ClipPath> {
    // Negative fast path: most nodes have `clip-path: none`.
666062
    if node_state.is_normal() {
666051
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
666050
            if !cc.has_clip_path(node_id.index()) {
666050
                return None;
            }
1
        }
11
    }
12
    let node_data = &styled_dom.node_data.as_container()[node_id];
12
    styled_dom
12
        .css_property_cache
12
        .ptr
12
        .get_clip_path(node_data, &node_id, node_state)
12
        .and_then(|v| v.get_property())
12
        .cloned()
666062
}
#[cfg(test)]
#[allow(clippy::float_cmp, clippy::too_many_lines)]
mod autotest_generated {
    use azul_core::{dom::Dom, ua_css::ResolvedUaScrollbar};
    use azul_css::{
        css::Css,
        props::style::{
            background::StyleBackgroundContent,
            scrollbar::{ScrollbarColorCustom, ScrollbarFadeDelay, ScrollbarFadeDuration},
        },
    };
    use rust_fontconfig::{CssFallbackGroup, FontMatch};
    use super::*;
    // ---------------------------------------------------------------------
    // helpers
    // ---------------------------------------------------------------------
    /// Every `LayoutOverflow` variant.
    const ALL_OVERFLOW: [LayoutOverflow; 5] = [
        LayoutOverflow::Scroll,
        LayoutOverflow::Auto,
        LayoutOverflow::Hidden,
        LayoutOverflow::Visible,
        LayoutOverflow::Clip,
    ];
    /// Every `LayoutDisplay` variant.
    const ALL_DISPLAY: [LayoutDisplay; 23] = [
        LayoutDisplay::None,
        LayoutDisplay::Block,
        LayoutDisplay::Inline,
        LayoutDisplay::InlineBlock,
        LayoutDisplay::Flex,
        LayoutDisplay::InlineFlex,
        LayoutDisplay::Table,
        LayoutDisplay::InlineTable,
        LayoutDisplay::TableRowGroup,
        LayoutDisplay::TableHeaderGroup,
        LayoutDisplay::TableFooterGroup,
        LayoutDisplay::TableRow,
        LayoutDisplay::TableColumnGroup,
        LayoutDisplay::TableColumn,
        LayoutDisplay::TableCell,
        LayoutDisplay::TableCaption,
        LayoutDisplay::FlowRoot,
        LayoutDisplay::ListItem,
        LayoutDisplay::RunIn,
        LayoutDisplay::Marker,
        LayoutDisplay::Grid,
        LayoutDisplay::InlineGrid,
        LayoutDisplay::Contents,
    ];
    /// Every `PageBreak` variant.
    const ALL_PAGE_BREAK: [PageBreak; 12] = [
        PageBreak::Auto,
        PageBreak::Avoid,
        PageBreak::Always,
        PageBreak::All,
        PageBreak::Page,
        PageBreak::AvoidPage,
        PageBreak::Left,
        PageBreak::Right,
        PageBreak::Recto,
        PageBreak::Verso,
        PageBreak::Column,
        PageBreak::AvoidColumn,
    ];
    /// Every `BreakInside` variant.
    const ALL_BREAK_INSIDE: [BreakInside; 4] = [
        BreakInside::Auto,
        BreakInside::Avoid,
        BreakInside::AvoidPage,
        BreakInside::AvoidColumn,
    ];
    /// Every `LayoutScrollbarWidth` variant.
    const ALL_SCROLLBAR_WIDTH: [LayoutScrollbarWidth; 3] = [
        LayoutScrollbarWidth::Auto,
        LayoutScrollbarWidth::Thin,
        LayoutScrollbarWidth::None,
    ];
    /// Every `ScrollbarVisibilityMode` variant.
    const ALL_VISIBILITY: [ScrollbarVisibilityMode; 3] = [
        ScrollbarVisibilityMode::Always,
        ScrollbarVisibilityMode::WhenScrolling,
        ScrollbarVisibilityMode::Auto,
    ];
    fn parse(css: &str) -> Css {
        azul_css::parser2::new_from_str(css).0
    }
    /// `<body>` with `n` `<div>` children, cascaded against `css`.
    /// Node ids are pre-order: `0` = body, `1..=n` = the children.
    fn body_with_divs(n: usize, css: &str) -> StyledDom {
        let children: Vec<Dom> = (0..n).map(|_| Dom::create_div()).collect();
        let mut dom = Dom::create_body().with_children(children.into());
        StyledDom::create(&mut dom, parse(css))
    }
    /// `<body>` with a single text child.
    fn body_with_text(text: &str) -> StyledDom {
        let mut dom = Dom::create_body().with_children(vec![Dom::create_text_do_not_use_without_block_level_wrapper(text)].into());
        StyledDom::create(&mut dom, Css::empty())
    }
    fn normal() -> StyledNodeState {
        StyledNodeState::default()
    }
    /// A non-`Normal` pseudo-state; forces every getter off its compact-cache
    /// fast path and onto the full cascade walk.
    fn hovered() -> StyledNodeState {
        StyledNodeState {
            hover: true,
            ..StyledNodeState::default()
        }
    }
    fn state_of(sd: &StyledDom, id: NodeId) -> StyledNodeState {
        sd.get_styled_node_state(&id)
    }
    fn empty_chains() -> ResolvedFontChains {
        ResolvedFontChains {
            chains: HashMap::new(),
            ..Default::default()
        }
    }
    fn chain_key(family: &str) -> FontChainKey {
        FontChainKey {
            font_families: vec![family.to_string()],
            weight: FcWeight::Normal,
            italic: false,
            oblique: false,
        }
    }
    /// A `FontMatch` covering exactly `ranges`.
    fn font_match(id: u128, ranges: &[(u32, u32)]) -> FontMatch {
        FontMatch {
            id: FontId(id),
            unicode_ranges: ranges
                .iter()
                .map(|&(start, end)| UnicodeRange { start, end })
                .collect(),
            fallbacks: Vec::new(),
        }
    }
    fn chain_with(groups: Vec<CssFallbackGroup>, unicode: Vec<FontMatch>) -> FontFallbackChain {
        FontFallbackChain {
            css_fallbacks: groups,
            unicode_fallbacks: unicode,
            original_stack: Vec::new(),
        }
    }
    /// A `LayoutNode` carrying nothing but the `scrollbar_info` under test.
    fn bare_layout_node(scrollbar_info: Option<ScrollbarRequirements>) -> LayoutNode {
        use azul_core::{diff::NodeDataFingerprint, dom::FormattingContext};
        use crate::solver3::{
            geometry::{BoxProps, UnresolvedBoxProps},
            layout_tree::{ComputedLayoutStyle, DirtyFlag, SubtreeHash},
        };
        LayoutNode {
            box_props: BoxProps::default(),
            dom_node_id: None,
            children: Vec::new(),
            used_size: None,
            formatting_context: FormattingContext::Inline,
            parent: None,
            intrinsic_sizes: None,
            baseline: None,
            inline_layout_result: None,
            inline_content_cache: None,
            scrollbar_info,
            relative_position: None,
            overflow_content_size: None,
            taffy_cache: taffy::Cache::new(),
            measured_content_sizes: (None, None),
            computed_style: ComputedLayoutStyle::default(),
            pseudo_element: None,
            escaped_top_margin: None,
            escaped_bottom_margin: None,
            parent_formatting_context: None,
            ifc_membership: None,
            containing_block_index: None,
            anonymous_type: None,
            preview_byte_range: None,
            node_data_fingerprint: NodeDataFingerprint::default(),
            subtree_hash: SubtreeHash(0),
            dirty_flag: DirtyFlag::Layout,
            unresolved_box_props: UnresolvedBoxProps::default(),
            ifc_id: None,
        }
    }
    // =====================================================================
    // MultiValue<T> — generic predicates and combinators
    // =====================================================================
    #[test]
    fn multivalue_default_is_auto() {
        let v: MultiValue<i32> = MultiValue::default();
        assert!(v.is_auto());
        assert!(!v.is_exact());
    }
    #[test]
    fn multivalue_is_auto_and_is_exact_are_mutually_exclusive() {
        let cases: [MultiValue<i32>; 4] = [
            MultiValue::Auto,
            MultiValue::Initial,
            MultiValue::Inherit,
            MultiValue::Exact(7),
        ];
        for v in cases {
            assert!(
                !(v.is_auto() && v.is_exact()),
                "a value cannot be both Auto and Exact: {v:?}"
            );
        }
        assert!(MultiValue::<i32>::Auto.is_auto());
        assert!(!MultiValue::<i32>::Initial.is_auto());
        assert!(!MultiValue::<i32>::Inherit.is_auto());
        assert!(!MultiValue::Exact(7).is_auto());
        assert!(MultiValue::Exact(7).is_exact());
        assert!(!MultiValue::<i32>::Auto.is_exact());
        assert!(!MultiValue::<i32>::Initial.is_exact());
        assert!(!MultiValue::<i32>::Inherit.is_exact());
    }
    #[test]
    fn multivalue_exact_returns_some_only_for_the_exact_variant() {
        assert_eq!(MultiValue::Exact(42_i32).exact(), Some(42));
        assert_eq!(MultiValue::<i32>::Auto.exact(), None);
        assert_eq!(MultiValue::<i32>::Initial.exact(), None);
        assert_eq!(MultiValue::<i32>::Inherit.exact(), None);
    }
    #[test]
    fn multivalue_exact_round_trips_extreme_payloads() {
        // Boundary integers survive Exact() → exact() unchanged.
        for probe in [i32::MIN, -1, 0, 1, i32::MAX] {
            assert_eq!(MultiValue::Exact(probe).exact(), Some(probe));
        }
        // NaN is not equal to itself: assert the *shape*, not equality.
        let nan = MultiValue::Exact(f32::NAN).exact().unwrap();
        assert!(nan.is_nan());
        assert_eq!(MultiValue::Exact(f32::INFINITY).exact(), Some(f32::INFINITY));
        assert_eq!(
            MultiValue::Exact(f32::NEG_INFINITY).exact(),
            Some(f32::NEG_INFINITY)
        );
    }
    #[test]
    fn multivalue_unwrap_or_uses_the_default_for_every_non_exact_variant() {
        assert_eq!(MultiValue::Exact(5_i32).unwrap_or(99), 5);
        assert_eq!(MultiValue::<i32>::Auto.unwrap_or(99), 99);
        assert_eq!(MultiValue::<i32>::Initial.unwrap_or(99), 99);
        assert_eq!(MultiValue::<i32>::Inherit.unwrap_or(99), 99);
        // The default is returned verbatim, even when it is a degenerate float.
        assert!(MultiValue::<f32>::Auto.unwrap_or(f32::NAN).is_nan());
    }
    #[test]
    fn multivalue_unwrap_or_default_falls_back_to_t_default() {
        assert_eq!(MultiValue::Exact(5_i32).unwrap_or_default(), 5);
        assert_eq!(MultiValue::<i32>::Auto.unwrap_or_default(), 0);
        assert_eq!(MultiValue::<i32>::Initial.unwrap_or_default(), 0);
        assert_eq!(MultiValue::<i32>::Inherit.unwrap_or_default(), 0);
        // T = LayoutOverflow → Default is Visible (the CSS initial value).
        assert_eq!(
            MultiValue::<LayoutOverflow>::Inherit.unwrap_or_default(),
            LayoutOverflow::Visible
        );
    }
    #[test]
    fn multivalue_map_transforms_exact_and_preserves_the_keyword_variants() {
        assert_eq!(MultiValue::Exact(2_i32).map(|v| v * 2), MultiValue::Exact(4));
        assert_eq!(MultiValue::<i32>::Auto.map(|v| v * 2), MultiValue::Auto);
        assert_eq!(
            MultiValue::<i32>::Initial.map(|v| v * 2),
            MultiValue::Initial
        );
        assert_eq!(
            MultiValue::<i32>::Inherit.map(|v| v * 2),
            MultiValue::Inherit
        );
    }
    #[test]
    fn multivalue_map_never_invokes_the_closure_for_keyword_variants() {
        // A keyword variant carries no T, so the mapper must not be called at all.
        let auto: MultiValue<i32> = MultiValue::Auto;
        let _ = auto.map(|_| -> i32 { panic!("map() called f() on MultiValue::Auto") });
        let initial: MultiValue<i32> = MultiValue::Initial;
        let _ = initial.map(|_| -> i32 { panic!("map() called f() on MultiValue::Initial") });
        let inherit: MultiValue<i32> = MultiValue::Inherit;
        let _ = inherit.map(|_| -> i32 { panic!("map() called f() on MultiValue::Inherit") });
    }
    #[test]
    fn multivalue_map_can_change_the_payload_type() {
        let mapped: MultiValue<usize> = MultiValue::Exact("hello").map(str::len);
        assert_eq!(mapped, MultiValue::Exact(5));
        // Overflow-adjacent payload: i32::MIN mapped to its (wrapping) absolute value
        // must not debug-panic inside map itself.
        let abs: MultiValue<i32> = MultiValue::Exact(i32::MIN).map(i32::wrapping_abs);
        assert_eq!(abs, MultiValue::Exact(i32::MIN));
    }
    // =====================================================================
    // MultiValue<LayoutOverflow> — overflow predicates
    // =====================================================================
    #[test]
    fn overflow_predicates_match_the_spec_for_every_exact_variant() {
        for o in ALL_OVERFLOW {
            let v = MultiValue::Exact(o);
            assert_eq!(
                v.is_clipped(),
                o != LayoutOverflow::Visible,
                "is_clipped is every value except Visible ({o:?})"
            );
            assert_eq!(
                v.is_scroll(),
                matches!(o, LayoutOverflow::Scroll | LayoutOverflow::Auto),
                "is_scroll ({o:?})"
            );
            assert_eq!(
                v.is_auto_overflow(),
                o == LayoutOverflow::Auto,
                "is_auto_overflow ({o:?})"
            );
            assert_eq!(
                v.is_hidden(),
                o == LayoutOverflow::Hidden,
                "is_hidden ({o:?})"
            );
            assert_eq!(
                v.is_hidden_or_clip(),
                matches!(o, LayoutOverflow::Hidden | LayoutOverflow::Clip),
                "is_hidden_or_clip ({o:?})"
            );
            assert_eq!(
                v.is_scroll_explicit(),
                o == LayoutOverflow::Scroll,
                "is_scroll_explicit ({o:?})"
            );
            assert_eq!(v.is_clip(), o == LayoutOverflow::Clip, "is_clip ({o:?})");
            assert_eq!(
                v.is_visible_or_clip(),
                matches!(o, LayoutOverflow::Visible | LayoutOverflow::Clip),
                "is_visible_or_clip ({o:?})"
            );
            assert_eq!(
                v.establishes_bfc(),
                matches!(
                    o,
                    LayoutOverflow::Hidden | LayoutOverflow::Scroll | LayoutOverflow::Auto
                ),
                "establishes_bfc ({o:?})"
            );
        }
    }
    #[test]
    fn overflow_predicates_are_false_for_every_keyword_variant() {
        // Gotcha guard: MultiValue::Auto is the CSS *keyword* `auto`, which is NOT
        // the same thing as Exact(LayoutOverflow::Auto). None of the LayoutOverflow
        // predicates may fire for a keyword variant.
        let keywords: [MultiValue<LayoutOverflow>; 3] = [
            MultiValue::Auto,
            MultiValue::Initial,
            MultiValue::Inherit,
        ];
        for v in keywords {
            assert!(!v.is_clipped(), "{v:?}");
            assert!(!v.is_scroll(), "{v:?}");
            assert!(!v.is_auto_overflow(), "{v:?}");
            assert!(!v.is_hidden(), "{v:?}");
            assert!(!v.is_hidden_or_clip(), "{v:?}");
            assert!(!v.is_scroll_explicit(), "{v:?}");
            assert!(!v.is_clip(), "{v:?}");
            assert!(!v.is_visible_or_clip(), "{v:?}");
            // The unset/initial/inherit sentinel is `visible` (initial) => no BFC.
            assert!(!v.establishes_bfc(), "{v:?}");
        }
    }
    #[test]
    fn overflow_scroll_implies_clipped_and_clip_implies_hidden_or_clip() {
        for o in ALL_OVERFLOW {
            let v = MultiValue::Exact(o);
            assert!(
                !v.is_scroll() || v.is_clipped(),
                "anything that scrolls also clips ({o:?})"
            );
            assert!(
                !v.is_clip() || v.is_hidden_or_clip(),
                "clip is a subset of hidden_or_clip ({o:?})"
            );
            assert!(
                !v.is_scroll_explicit() || v.is_scroll(),
                "explicit scroll is a subset of scroll ({o:?})"
            );
        }
    }
    /// Exercises the rule tagged `+spec:overflow:833078` on `resolve_computed`.
    #[test]
    fn overflow_resolve_computed_matches_css_overflow_3_section_3_1() {
        for this in ALL_OVERFLOW {
            for other in ALL_OVERFLOW {
                let got = MultiValue::Exact(this).resolve_computed(&MultiValue::Exact(other));
                let other_is_scrollable =
                    !matches!(other, LayoutOverflow::Visible | LayoutOverflow::Clip);
                let want = if other_is_scrollable {
                    match this {
                        LayoutOverflow::Visible => LayoutOverflow::Auto,
                        LayoutOverflow::Clip => LayoutOverflow::Hidden,
                        keep => keep,
                    }
                } else {
                    this
                };
                assert_eq!(
                    got,
                    MultiValue::Exact(want),
                    "resolve_computed({this:?}, {other:?})"
                );
            }
        }
    }
    #[test]
    fn overflow_resolve_computed_treats_unset_as_the_initial_visible() {
        let keywords: [MultiValue<LayoutOverflow>; 3] = [
            MultiValue::Auto,
            MultiValue::Initial,
            MultiValue::Inherit,
        ];
        // css-overflow-3 §3.1 applies to the UNSET sentinel too (it means
        // the initial `visible`): an unset axis computes to `auto` when the
        // other axis is scrollable, and stays the sentinel otherwise.
        for v in keywords {
            for other in ALL_OVERFLOW {
                let resolved = v.resolve_computed(&MultiValue::Exact(other));
                let expected_upgrade = !matches!(
                    other,
                    LayoutOverflow::Visible | LayoutOverflow::Clip
                );
                if expected_upgrade {
                    assert_eq!(
                        resolved,
                        MultiValue::Exact(LayoutOverflow::Auto),
                        "unset + scrollable {other:?} computes to auto"
                    );
                } else {
                    assert_eq!(resolved, v, "unset + {other:?} stays unset");
                }
            }
            assert_eq!(v.resolve_computed(&MultiValue::Auto), v);
        }
        // An unset OTHER axis acts as the initial `visible`: an Exact self
        // resolves exactly as it would against Exact(Visible).
        for this in ALL_OVERFLOW {
            let v = MultiValue::Exact(this);
            for other in keywords {
                assert_eq!(
                    v.resolve_computed(&other),
                    v.resolve_computed(&MultiValue::Exact(LayoutOverflow::Visible)),
                    "{this:?} vs unset {other:?}"
                );
            }
        }
    }
    #[test]
    fn overflow_resolve_computed_is_idempotent() {
        for this in ALL_OVERFLOW {
            for other in ALL_OVERFLOW {
                let other_mv = MultiValue::Exact(other);
                let once = MultiValue::Exact(this).resolve_computed(&other_mv);
                let twice = once.resolve_computed(&other_mv);
                assert_eq!(once, twice, "resolve_computed({this:?}, {other:?}) twice");
            }
        }
    }
    // =====================================================================
    // MultiValue<LayoutPosition> / MultiValue<LayoutFloat>
    // =====================================================================
    #[test]
    fn position_is_absolute_or_fixed_only_for_absolute_and_fixed() {
        let all = [
            LayoutPosition::Static,
            LayoutPosition::Relative,
            LayoutPosition::Absolute,
            LayoutPosition::Fixed,
            LayoutPosition::Sticky,
        ];
        for p in all {
            assert_eq!(
                MultiValue::Exact(p).is_absolute_or_fixed(),
                matches!(p, LayoutPosition::Absolute | LayoutPosition::Fixed),
                "{p:?}"
            );
        }
        // Keyword variants carry no position → never out-of-flow.
        assert!(!MultiValue::<LayoutPosition>::Auto.is_absolute_or_fixed());
        assert!(!MultiValue::<LayoutPosition>::Initial.is_absolute_or_fixed());
        assert!(!MultiValue::<LayoutPosition>::Inherit.is_absolute_or_fixed());
    }
    #[test]
    fn float_is_none_treats_every_keyword_variant_as_not_floated() {
        assert!(MultiValue::Exact(LayoutFloat::None).is_none());
        assert!(!MultiValue::Exact(LayoutFloat::Left).is_none());
        assert!(!MultiValue::Exact(LayoutFloat::Right).is_none());
        // Unlike the overflow predicates, `is_none` deliberately folds the keyword
        // variants in: an unset float is not a float.
        assert!(MultiValue::<LayoutFloat>::Auto.is_none());
        assert!(MultiValue::<LayoutFloat>::Initial.is_none());
        assert!(MultiValue::<LayoutFloat>::Inherit.is_none());
        assert!(MultiValue::<LayoutFloat>::default().is_none());
    }
    // =====================================================================
    // blockify_display / get_computed_display
    // =====================================================================
    #[test]
    fn blockify_display_follows_the_css_display_3_table() {
        for d in ALL_DISPLAY {
            let want = match d {
                LayoutDisplay::Inline | LayoutDisplay::InlineBlock => LayoutDisplay::Block,
                LayoutDisplay::InlineFlex => LayoutDisplay::Flex,
                LayoutDisplay::InlineTable => LayoutDisplay::Table,
                LayoutDisplay::InlineGrid => LayoutDisplay::Grid,
                LayoutDisplay::TableRowGroup
                | LayoutDisplay::TableColumn
                | LayoutDisplay::TableColumnGroup
                | LayoutDisplay::TableHeaderGroup
                | LayoutDisplay::TableFooterGroup
                | LayoutDisplay::TableRow
                | LayoutDisplay::TableCell
                | LayoutDisplay::TableCaption => LayoutDisplay::Block,
                // css-display-3 §2.7: run-in blockifies to block.
                LayoutDisplay::RunIn => LayoutDisplay::Block,
                other => other,
            };
            assert_eq!(blockify_display(d), want, "blockify_display({d:?})");
        }
    }
    #[test]
    fn blockify_display_is_idempotent_and_never_produces_an_inline_level_value() {
        for d in ALL_DISPLAY {
            let once = blockify_display(d);
            assert_eq!(
                blockify_display(once),
                once,
                "blockify_display is not idempotent for {d:?}"
            );
            assert!(
                !matches!(
                    once,
                    LayoutDisplay::Inline
                        | LayoutDisplay::InlineBlock
                        | LayoutDisplay::InlineFlex
                        | LayoutDisplay::InlineTable
                        | LayoutDisplay::InlineGrid
                ),
                "blockified {d:?} is still inline-level: {once:?}"
            );
        }
    }
    #[test]
    fn get_computed_display_keeps_none_regardless_of_the_flags() {
        // display:none boxes are never generated, so no flag may resurrect them.
        for flags in 0_u8..16 {
            let got = get_computed_display(
                LayoutDisplay::None,
                flags & 1 != 0,
                flags & 2 != 0,
                flags & 4 != 0,
                flags & 8 != 0,
            );
            assert_eq!(got, LayoutDisplay::None, "flags={flags:#06b}");
        }
    }
    #[test]
    fn get_computed_display_is_the_identity_when_no_flag_is_set() {
        for d in ALL_DISPLAY {
            assert_eq!(
                get_computed_display(d, false, false, false, false),
                d,
                "an in-flow, non-root, non-flex-child box keeps its specified display ({d:?})"
            );
        }
    }
    #[test]
    fn get_computed_display_blockifies_whenever_any_flag_is_set() {
        for d in ALL_DISPLAY {
            if d == LayoutDisplay::None {
                continue; // covered by the dedicated None test
            }
            // Each of the four flags on its own must blockify, and so must every
            // combination of them.
            for flags in 1_u8..16 {
                let got = get_computed_display(
                    d,
                    flags & 1 != 0,
                    flags & 2 != 0,
                    flags & 4 != 0,
                    flags & 8 != 0,
                );
                assert_eq!(
                    got,
                    blockify_display(d),
                    "get_computed_display({d:?}, flags={flags:#06b})"
                );
            }
        }
    }
    // =====================================================================
    // Fragmentation predicates
    // =====================================================================
    #[test]
    fn is_forced_page_break_covers_exactly_the_forcing_keywords() {
        for pb in ALL_PAGE_BREAK {
            let want = matches!(
                pb,
                PageBreak::Always
                    | PageBreak::Page
                    | PageBreak::Left
                    | PageBreak::Right
                    | PageBreak::Recto
                    | PageBreak::Verso
                    | PageBreak::All
            );
            assert_eq!(is_forced_page_break(pb), want, "{pb:?}");
        }
        // `column` forces a *column* break, not a page break.
        assert!(!is_forced_page_break(PageBreak::Column));
        assert!(!is_forced_page_break(PageBreak::Auto));
        assert!(!is_forced_page_break(PageBreak::default()));
    }
    #[test]
    fn is_avoid_page_break_covers_exactly_avoid_and_avoid_page() {
        for pb in ALL_PAGE_BREAK {
            let want = matches!(pb, PageBreak::Avoid | PageBreak::AvoidPage);
            assert_eq!(is_avoid_page_break(&pb), want, "{pb:?}");
        }
        // `avoid-column` avoids a column break, not a page break.
        assert!(!is_avoid_page_break(&PageBreak::AvoidColumn));
    }
    #[test]
    fn forced_and_avoid_page_break_are_never_both_true() {
        for pb in ALL_PAGE_BREAK {
            assert!(
                !(is_forced_page_break(pb) && is_avoid_page_break(&pb)),
                "{pb:?} is simultaneously forced and avoided"
            );
        }
    }
    #[test]
    fn is_avoid_break_inside_is_true_for_every_variant_except_auto() {
        for bi in ALL_BREAK_INSIDE {
            assert_eq!(is_avoid_break_inside(&bi), bi != BreakInside::Auto, "{bi:?}");
        }
        assert!(!is_avoid_break_inside(&BreakInside::default()));
    }
    // =====================================================================
    // ComputedScrollbarStyle::from_ua_resolved
    // =====================================================================
    fn ua(
        width: LayoutScrollbarWidth,
        visibility: ScrollbarVisibilityMode,
        color: StyleScrollbarColor,
        delay_ms: u32,
        duration_ms: u32,
    ) -> ResolvedUaScrollbar {
        ResolvedUaScrollbar {
            color,
            width,
            visibility,
            fade_delay: ScrollbarFadeDelay { ms: delay_ms },
            fade_duration: ScrollbarFadeDuration { ms: duration_ms },
        }
    }
    #[test]
    fn from_ua_resolved_holds_its_invariants_across_the_whole_width_visibility_matrix() {
        for width in ALL_SCROLLBAR_WIDTH {
            for visibility in ALL_VISIBILITY {
                let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
                    width,
                    visibility,
                    StyleScrollbarColor::Auto,
                    0,
                    0,
                ));
                assert_eq!(s.width_mode, width);
                assert_eq!(s.visibility, visibility);
                let expected_visual = match width {
                    LayoutScrollbarWidth::Thin => SCROLLBAR_WIDTH_THIN,
                    LayoutScrollbarWidth::Auto => SCROLLBAR_WIDTH_AUTO,
                    LayoutScrollbarWidth::None => 0.0,
                };
                assert_eq!(s.visual_width_px, expected_visual, "{width:?}");
                // Only `WhenScrolling` is an overlay scrollbar. `Auto` is NOT.
                let is_overlay = visibility == ScrollbarVisibilityMode::WhenScrolling;
                assert_eq!(s.clip_to_container_border, is_overlay);
                assert_eq!(s.show_scroll_buttons, !is_overlay);
                assert_eq!(s.show_corner_rect, !is_overlay);
                if is_overlay {
                    assert_eq!(s.reserve_width_px, 0.0, "overlay reserves no layout space");
                    assert_eq!(s.scroll_button_size_px, 0.0);
                } else {
                    assert_eq!(s.reserve_width_px, s.visual_width_px);
                    assert_eq!(s.scroll_button_size_px, s.visual_width_px);
                }
                // Hover/active widths are always the visual width plus the expand delta.
                assert_eq!(
                    s.visual_width_px_hover,
                    Some(s.visual_width_px + SCROLLBAR_HOVER_EXPAND_PX)
                );
                assert_eq!(
                    s.visual_width_px_active,
                    Some(s.visual_width_px + SCROLLBAR_HOVER_EXPAND_PX)
                );
                assert!(s.reserve_width_px <= s.visual_width_px);
                assert!(s.visual_width_px.is_finite());
            }
        }
    }
    #[test]
    fn from_ua_resolved_saturates_the_hover_and_active_colour_maths_at_the_u8_boundaries() {
        // Max channels: +30 lighten / +40 alpha must saturate, not wrap or panic.
        let white = ColorU {
            r: 255,
            g: 255,
            b: 255,
            a: 255,
        };
        let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
            LayoutScrollbarWidth::Auto,
            ScrollbarVisibilityMode::Always,
            StyleScrollbarColor::Custom(ScrollbarColorCustom {
                thumb: white,
                track: white,
            }),
            0,
            0,
        ));
        let hover = s.thumb_color_hover.expect("hover thumb colour");
        assert_eq!((hover.r, hover.g, hover.b, hover.a), (255, 255, 255, 255));
        let track_hover = s.track_color_hover.expect("hover track colour");
        assert_eq!(track_hover.a, 255);
        // Min channels: -15 darken must saturate at 0, and the active alpha is pinned
        // to 255 regardless of the source alpha.
        let black0 = ColorU {
            r: 0,
            g: 0,
            b: 0,
            a: 0,
        };
        let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
            LayoutScrollbarWidth::Auto,
            ScrollbarVisibilityMode::Always,
            StyleScrollbarColor::Custom(ScrollbarColorCustom {
                thumb: black0,
                track: black0,
            }),
            0,
            0,
        ));
        let active = s.thumb_color_active.expect("active thumb colour");
        assert_eq!((active.r, active.g, active.b), (0, 0, 0));
        assert_eq!(active.a, 255, "the active thumb is always fully opaque");
        let hover = s.thumb_color_hover.expect("hover thumb colour");
        assert_eq!(
            (hover.r, hover.g, hover.b, hover.a),
            (
                THUMB_HOVER_LIGHTEN,
                THUMB_HOVER_LIGHTEN,
                THUMB_HOVER_LIGHTEN,
                THUMB_HOVER_ALPHA_ADD
            )
        );
    }
    #[test]
    fn from_ua_resolved_passes_extreme_fade_timings_through_without_overflow() {
        let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
            LayoutScrollbarWidth::Thin,
            ScrollbarVisibilityMode::WhenScrolling,
            StyleScrollbarColor::Auto,
            u32::MAX,
            u32::MAX,
        ));
        assert_eq!(s.fade_delay_ms, u32::MAX);
        assert_eq!(s.fade_duration_ms, u32::MAX);
        let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
            LayoutScrollbarWidth::Thin,
            ScrollbarVisibilityMode::WhenScrolling,
            StyleScrollbarColor::Auto,
            0,
            0,
        ));
        assert_eq!(s.fade_delay_ms, 0);
        assert_eq!(s.fade_duration_ms, 0);
    }
    #[test]
    fn from_ua_resolved_maps_scrollbar_color_auto_to_transparent() {
        let s = ComputedScrollbarStyle::from_ua_resolved(&ua(
            LayoutScrollbarWidth::Auto,
            ScrollbarVisibilityMode::Always,
            StyleScrollbarColor::Auto,
            0,
            0,
        ));
        assert_eq!(s.thumb_color, ColorU::TRANSPARENT);
        assert_eq!(s.track_color, ColorU::TRANSPARENT);
        assert_eq!(s.button_color, ColorU::TRANSPARENT);
        assert_eq!(s.corner_color, ColorU::TRANSPARENT);
    }
    #[test]
    fn computed_scrollbar_style_default_is_internally_consistent() {
        let d = ComputedScrollbarStyle::default();
        assert!(d.visual_width_px.is_finite() && d.visual_width_px >= 0.0);
        assert!(d.reserve_width_px.is_finite() && d.reserve_width_px >= 0.0);
        assert!(d.reserve_width_px <= d.visual_width_px);
        let overlay = d.visibility == ScrollbarVisibilityMode::WhenScrolling;
        assert_eq!(d.show_scroll_buttons, !overlay);
        assert_eq!(d.clip_to_container_border, overlay);
    }
    // =====================================================================
    // extract_color_from_background
    // =====================================================================
    #[test]
    fn extract_color_from_background_returns_the_solid_colour_verbatim() {
        for probe in [
            ColorU::TRANSPARENT,
            ColorU::BLACK,
            ColorU::WHITE,
            ColorU {
                r: 1,
                g: 2,
                b: 3,
                a: 4,
            },
            ColorU {
                r: 255,
                g: 0,
                b: 255,
                a: 0,
            },
        ] {
            assert_eq!(
                extract_color_from_background(&StyleBackgroundContent::Color(probe)),
                probe
            );
        }
    }
    #[test]
    fn extract_color_from_background_falls_back_to_transparent_for_non_colour_layers() {
        // An image layer has no solid colour to extract → transparent, not a panic.
        let img = StyleBackgroundContent::Image("does-not-exist.png".into());
        assert_eq!(extract_color_from_background(&img), ColorU::TRANSPARENT);
        // Empty / unicode image names are still just "not a colour".
        let empty = StyleBackgroundContent::Image(String::new().into());
        assert_eq!(extract_color_from_background(&empty), ColorU::TRANSPARENT);
        let unicode = StyleBackgroundContent::Image("картинка-🎉.png".into());
        assert_eq!(extract_color_from_background(&unicode), ColorU::TRANSPARENT);
    }
    // =====================================================================
    // get_scrollbar_info_from_layout
    // =====================================================================
    #[test]
    fn get_scrollbar_info_from_layout_defaults_to_no_scrollbars_when_layout_never_set_it() {
        let node = bare_layout_node(None);
        let got = get_scrollbar_info_from_layout(&node);
        assert!(!got.needs_horizontal);
        assert!(!got.needs_vertical);
        assert_eq!(got.scrollbar_width, 0.0);
        assert_eq!(got.scrollbar_height, 0.0);
        assert_eq!(got.visual_width_px, 0.0);
    }
    #[test]
    fn get_scrollbar_info_from_layout_returns_whatever_layout_stored_including_degenerate_floats() {
        let stored = ScrollbarRequirements {
            needs_horizontal: true,
            needs_vertical: true,
            scrollbar_width: f32::NAN,
            scrollbar_height: f32::INFINITY,
            visual_width_px: -1.0,
        };
        let got = get_scrollbar_info_from_layout(&bare_layout_node(Some(stored)));
        assert!(got.needs_horizontal && got.needs_vertical);
        assert!(got.scrollbar_width.is_nan(), "the getter must not sanitise");
        assert_eq!(got.scrollbar_height, f32::INFINITY);
        assert_eq!(got.visual_width_px, -1.0);
    }
    // =====================================================================
    // ResolvedFontChains
    // =====================================================================
    #[test]
    fn resolved_font_chains_empty_instance_answers_every_query_with_none() {
        let r = empty_chains();
        assert_eq!(r.len(), 0);
        assert!(r.is_empty());
        assert_eq!(r.font_refs_len(), 0);
        assert!(r.get(&FontChainKeyOrRef::Ref(0)).is_none());
        assert!(r.get_by_chain_key(&chain_key("Arial")).is_none());
        assert!(r.get_for_font_stack(&[]).is_none());
        assert!(r.get_for_font_ref(0).is_none());
        assert!(r.get_for_font_ref(usize::MAX).is_none());
        assert!(r.get_for_font_ref(usize::MAX / 2).is_none());
        assert!(r.clone().into_inner().is_empty());
        assert!(r.into_fontconfig_chains().is_empty());
    }
    #[test]
    fn resolved_font_chains_get_by_chain_key_round_trips_the_inserted_key() {
        let key = chain_key("Iosevka");
        let mut chains = HashMap::new();
        chains.insert(
            FontChainKeyOrRef::Chain(key.clone()),
            chain_with(Vec::new(), Vec::new()),
        );
        let r = ResolvedFontChains { chains, ..Default::default() };
        assert!(r.get_by_chain_key(&key).is_some());
        assert!(r.get(&FontChainKeyOrRef::Chain(key.clone())).is_some());
        // A key that differs only in weight is a different key.
        let heavier = FontChainKey {
            weight: FcWeight::Bold,
            ..key.clone()
        };
        assert!(r.get_by_chain_key(&heavier).is_none());
        // …and so is one that differs only in the italic flag.
        let italic = FontChainKey {
            italic: true,
            ..key
        };
        assert!(r.get_by_chain_key(&italic).is_none());
    }
    #[test]
    fn resolved_font_chains_counts_and_filters_ref_entries() {
        let mut chains = HashMap::new();
        chains.insert(
            FontChainKeyOrRef::Chain(chain_key("Arial")),
            chain_with(Vec::new(), Vec::new()),
        );
        chains.insert(
            FontChainKeyOrRef::Ref(0xDEAD_BEEF),
            chain_with(Vec::new(), Vec::new()),
        );
        chains.insert(
            FontChainKeyOrRef::Ref(usize::MAX),
            chain_with(Vec::new(), Vec::new()),
        );
        let r = ResolvedFontChains { chains, ..Default::default() };
        assert_eq!(r.len(), 3);
        assert!(!r.is_empty());
        assert_eq!(r.font_refs_len(), 2, "two Ref keys, one Chain key");
        assert!(r.get_for_font_ref(0xDEAD_BEEF).is_some());
        assert!(r.get_for_font_ref(usize::MAX).is_some());
        assert!(r.get_for_font_ref(0).is_none());
        // into_fontconfig_chains drops every Ref entry.
        let fc_only = r.into_fontconfig_chains();
        assert_eq!(fc_only.len(), 1);
        assert!(fc_only.contains_key(&chain_key("Arial")));
    }
    #[test]
    fn resolved_font_chains_get_for_font_stack_uses_the_canonical_selector_key() {
        let selectors = vec![FontSelector {
            family: "Arial".to_string(),
            weight: FcWeight::Normal,
            style: FontStyle::Normal,
            unicode_ranges: Vec::new(),
        }];
        let key = FontChainKey::from_selectors(&selectors);
        let mut chains = HashMap::new();
        chains.insert(
            FontChainKeyOrRef::Chain(key),
            chain_with(Vec::new(), Vec::new()),
        );
        let r = ResolvedFontChains { chains, ..Default::default() };
        assert!(r.get_for_font_stack(&selectors).is_some());
        // An empty stack must not accidentally alias the "Arial" key.
        assert!(r.get_for_font_stack(&[]).is_none());
    }
    // =====================================================================
    // collect_font_ids_from_chains / compute_fonts_to_load
    // =====================================================================
    #[test]
    fn collect_font_ids_from_chains_dedupes_across_groups_and_unicode_fallbacks() {
        let mut chains = HashMap::new();
        chains.insert(
            FontChainKeyOrRef::Chain(chain_key("Arial")),
            chain_with(
                vec![
                    CssFallbackGroup {
                        css_name: "Arial".to_string(),
                        fonts: vec![font_match(1, &[]), font_match(2, &[])],
                    },
                    CssFallbackGroup {
                        css_name: "sans-serif".to_string(),
                        // FontId(1) also appears in the first group.
                        fonts: vec![font_match(1, &[]), font_match(3, &[])],
                    },
                ],
                vec![font_match(3, &[]), font_match(u128::MAX, &[])],
            ),
        );
        let ids = collect_font_ids_from_chains(&ResolvedFontChains { chains, ..Default::default() });
        assert_eq!(ids.len(), 4, "ids 1, 2, 3 and u128::MAX, each exactly once");
        for probe in [1_u128, 2, 3, u128::MAX] {
            assert!(ids.contains(&FontId(probe)), "missing FontId({probe})");
        }
        assert!(!ids.contains(&FontId(0)));
    }
    #[test]
    fn collect_font_ids_from_chains_returns_empty_for_an_empty_or_fontless_chain_set() {
        assert!(collect_font_ids_from_chains(&empty_chains()).is_empty());
        // A chain that exists but carries no fonts at all (the empty-fc_cache result).
        let mut chains = HashMap::new();
        chains.insert(
            FontChainKeyOrRef::Chain(chain_key("Nonexistent")),
            chain_with(
                vec![CssFallbackGroup {
                    css_name: "Nonexistent".to_string(),
                    fonts: Vec::new(),
                }],
                Vec::new(),
            ),
        );
        assert!(collect_font_ids_from_chains(&ResolvedFontChains { chains, ..Default::default() }).is_empty());
    }
    #[test]
    fn compute_fonts_to_load_is_the_set_difference_and_bails_early_on_an_empty_requirement() {
        let a = FontId(0);
        let b = FontId(1);
        let c = FontId(u128::MAX);
        let empty: HashSet<FontId> = HashSet::new();
        let all: HashSet<FontId> = [a, b, c].into_iter().collect();
        let loaded_b: HashSet<FontId> = [b].into_iter().collect();
        // Nothing required → nothing to load, regardless of what is loaded.
        assert!(compute_fonts_to_load(&empty, &empty).is_empty());
        assert!(compute_fonts_to_load(&empty, &all).is_empty());
        // Nothing loaded → load everything.
        assert_eq!(compute_fonts_to_load(&all, &empty), all);
        // Partial overlap → only the missing ones.
        let todo = compute_fonts_to_load(&all, &loaded_b);
        assert_eq!(todo.len(), 2);
        assert!(todo.contains(&a) && todo.contains(&c));
        assert!(!todo.contains(&b));
        // Already-loaded is a superset → nothing to do (and no underflow).
        assert!(compute_fonts_to_load(&loaded_b, &all).is_empty());
        assert!(compute_fonts_to_load(&all, &all).is_empty());
    }
    // =====================================================================
    // prune_chain_to_used_chars
    // =====================================================================
    #[test]
    fn prune_chain_to_used_chars_keeps_the_first_match_of_every_group_when_nothing_is_needed() {
        let mut chain = chain_with(
            vec![
                CssFallbackGroup {
                    css_name: "A".to_string(),
                    fonts: vec![font_match(1, &[(0, 0x10_FFFF)]), font_match(2, &[]), font_match(3, &[])],
                },
                CssFallbackGroup {
                    css_name: "B".to_string(),
                    fonts: vec![font_match(4, &[]), font_match(5, &[])],
                },
            ],
            vec![font_match(6, &[(0x4E00, 0x9FFF)])],
        );
        prune_chain_to_used_chars(&mut chain, &std::collections::BTreeSet::new());
        // Nothing to cover → every group collapses to its single best match…
        assert_eq!(chain.css_fallbacks[0].fonts.len(), 1);
        assert_eq!(chain.css_fallbacks[0].fonts[0].id, FontId(1));
        assert_eq!(chain.css_fallbacks[1].fonts.len(), 1);
        assert_eq!(chain.css_fallbacks[1].fonts[0].id, FontId(4));
        // …and no unicode fallback can intersect an empty codepoint set.
        assert!(chain.unicode_fallbacks.is_empty());
    }
    #[test]
    fn prune_chain_to_used_chars_keeps_walking_until_every_codepoint_is_covered() {
        // 'é' (U+00E9) is only covered by the *second* font in the group.
        let mut chain = chain_with(
            vec![CssFallbackGroup {
                css_name: "A".to_string(),
                fonts: vec![
                    font_match(1, &[(0x20, 0x7F)]),   // ASCII only
                    font_match(2, &[(0x80, 0x24F)]),  // Latin-1 supplement + extended
                    font_match(3, &[(0x0, 0x10_FFFF)]), // everything (must be dropped)
                ],
            }],
            Vec::new(),
        );
        let used: std::collections::BTreeSet<u32> = [0xE9_u32].into_iter().collect();
        prune_chain_to_used_chars(&mut chain, &used);
        assert_eq!(
            chain.css_fallbacks[0].fonts.len(),
            2,
            "walk stops as soon as the needed codepoints are covered"
        );
        assert_eq!(chain.css_fallbacks[0].fonts[1].id, FontId(2));
    }
    #[test]
    fn prune_chain_to_used_chars_keeps_the_whole_group_when_nothing_ever_covers_the_codepoint() {
        let mut chain = chain_with(
            vec![CssFallbackGroup {
                css_name: "A".to_string(),
                fonts: vec![font_match(1, &[(0x20, 0x7F)]), font_match(2, &[(0x20, 0x7F)])],
            }],
            vec![font_match(3, &[(0x20, 0x7F)])],
        );
        // A codepoint no font claims — and the numeric boundary of the u32 space.
        let used: std::collections::BTreeSet<u32> = [u32::MAX].into_iter().collect();
        prune_chain_to_used_chars(&mut chain, &used);
        assert_eq!(
            chain.css_fallbacks[0].fonts.len(),
            2,
            "an uncoverable codepoint must not silently drop CSS fonts"
        );
        assert!(
            chain.unicode_fallbacks.is_empty(),
            "no unicode fallback intersects U+FFFFFFFF"
        );
    }
    #[test]
    fn prune_chain_to_used_chars_treats_unicode_ranges_as_inclusive_on_both_ends() {
        for probe in [0x4E00_u32, 0x9FFF] {
            let mut chain = chain_with(
                Vec::new(),
                vec![font_match(1, &[(0x4E00, 0x9FFF)]), font_match(2, &[(0x20, 0x7F)])],
            );
            let used: std::collections::BTreeSet<u32> = [probe].into_iter().collect();
            prune_chain_to_used_chars(&mut chain, &used);
            assert_eq!(
                chain.unicode_fallbacks.len(),
                1,
                "U+{probe:04X} is inside the inclusive CJK range"
            );
            assert_eq!(chain.unicode_fallbacks[0].id, FontId(1));
        }
        // One past each end of the range → no intersection.
        for probe in [0x4DFF_u32, 0xA000] {
            let mut chain = chain_with(Vec::new(), vec![font_match(1, &[(0x4E00, 0x9FFF)])]);
            let used: std::collections::BTreeSet<u32> = [probe].into_iter().collect();
            prune_chain_to_used_chars(&mut chain, &used);
            assert!(chain.unicode_fallbacks.is_empty(), "U+{probe:04X}");
        }
    }
    #[test]
    fn prune_chain_to_used_chars_survives_empty_chains_and_empty_groups() {
        let mut empty = chain_with(Vec::new(), Vec::new());
        prune_chain_to_used_chars(&mut empty, &std::collections::BTreeSet::new());
        assert!(empty.css_fallbacks.is_empty());
        assert!(empty.unicode_fallbacks.is_empty());
        // A group with zero fonts is skipped rather than truncated to a phantom entry.
        let mut fontless = chain_with(
            vec![CssFallbackGroup {
                css_name: "A".to_string(),
                fonts: Vec::new(),
            }],
            Vec::new(),
        );
        let used: std::collections::BTreeSet<u32> = [0x1F389_u32].into_iter().collect();
        prune_chain_to_used_chars(&mut fontless, &used);
        assert_eq!(fontless.css_fallbacks.len(), 1);
        assert!(fontless.css_fallbacks[0].fonts.is_empty());
    }
    // =====================================================================
    // build_font_selector_stack
    // =====================================================================
    /// Strip the machine-dependent fontconfig alias insertions (the
    /// families push_family_with_system_aliases prepends before each CSS
    /// generic) so stack-shape assertions stay portable across systems.
    fn without_system_alias_prefs(stack: &[FontSelector]) -> Vec<FontSelector> {
        #[cfg(all(target_os = "linux", feature = "std"))]
        {
            let aliases = fontconfig_generic_aliases();
            let alias_names: std::collections::BTreeSet<String> = aliases
                .values()
                .flatten()
                .map(|s| s.to_ascii_lowercase())
                .collect();
            stack
                .iter()
                .filter(|s| !alias_names.contains(&s.family.to_ascii_lowercase()))
                .cloned()
                .collect()
        }
        #[cfg(not(all(target_os = "linux", feature = "std")))]
        {
            stack.to_vec()
        }
    }
    #[test]
    fn build_font_selector_stack_always_appends_the_three_generic_fallbacks() {
        let families = StyleFontFamilyVec::from_vec(Vec::new());
        let stack = build_font_selector_stack(&families, None, FcWeight::Normal, FontStyle::Normal);
        let stack = without_system_alias_prefs(&stack);
        let names: Vec<&str> = stack.iter().map(|s| s.family.as_str()).collect();
        assert_eq!(names, ["sans-serif", "serif", "monospace"]);
        for s in &stack {
            assert_eq!(s.weight, FcWeight::Normal);
            assert_eq!(s.style, FontStyle::Normal);
        }
    }
    /// Two DIFFERENT documents must not share a style answer for the same
    /// `NodeId`.
    ///
    /// This exists because of a bug it would have caught. An
    /// `Arc<StyleProperties>` memo (worth a measured 6.3 MB of peak heap)
    /// was written keyed on `ptr::from_ref(styled_dom) as usize` as the
    /// document identity. A pointer address is not an identity: a
    /// `StyledDom` is created, rendered, dropped, and the next one lands at
    /// the same address — so the second document was served the first
    /// one's cached styles. Sixteen reftests went red.
    ///
    /// Every per-node style cache added here must keep this green. It
    /// asserts CONCRETE VALUES (11px, 29px) rather than mere inequality, so
    /// it cannot pass by both sides being equally wrong.
    ///
    /// Its control is the incident itself rather than a synthetic break:
    /// the pointer-keyed memo really was written, really did compile and
    /// pass every lib test, and really did turn sixteen reftests red. This
    /// asserts the axis those reftests were the only thing watching.
    #[test]
    fn two_documents_do_not_share_a_style_answer_for_the_same_node_id() {
        use azul_core::dom::Dom;
        use azul_css::props::basic::PhysicalSize;
        fn resolve(css: &str) -> f32 {
            let mut dom = Dom::create_body().with_child(Dom::create_div());
            let styled = StyledDom::create(
                &mut dom,
                Css::from_string(css.into()),
            );
            get_style_properties(
                &styled,
                NodeId::new(1),
                None,
                PhysicalSize::new(800.0, 600.0),
            )
            .font_size_px
            // `styled` is dropped here on purpose: the NEXT call may well
            // reuse this exact allocation, which is the aliasing hazard.
        }
        let a = resolve("div { font-size: 11px; }");
        let b = resolve("div { font-size: 29px; }");
        assert!(
            (a - 11.0).abs() < 0.5,
            "first document must resolve to its OWN 11px, got {a}"
        );
        assert!(
            (b - 29.0).abs() < 0.5,
            "second document must resolve to its OWN 29px, got {b} — if this \
             is 11 the second document was served the first one's cached \
             style, which is what a pointer-address cache key does once the \
             allocator reuses the address"
        );
    }
    #[test]
    fn build_font_selector_stack_puts_the_authored_families_first() {
        let families = StyleFontFamilyVec::from_vec(vec![
            StyleFontFamily::System("Iosevka".to_string().into()),
            StyleFontFamily::System("Menlo".to_string().into()),
        ]);
        let stack = build_font_selector_stack(&families, None, FcWeight::Bold, FontStyle::Italic);
        let stack = without_system_alias_prefs(&stack);
        assert_eq!(stack.len(), 5, "2 authored + 3 generic fallbacks");
        assert_eq!(stack[0].family, "Iosevka");
        assert_eq!(stack[1].family, "Menlo");
        // Authored families carry the requested weight/style…
        assert_eq!(stack[0].weight, FcWeight::Bold);
        assert_eq!(stack[0].style, FontStyle::Italic);
        // …while the appended generics are always the neutral Normal/Normal.
        assert_eq!(stack[4].family, "monospace");
        assert_eq!(stack[4].weight, FcWeight::Normal);
        assert_eq!(stack[4].style, FontStyle::Normal);
    }
    #[test]
    fn font_stack_memo_returns_what_the_builder_would_have_built() {
        let families = StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System(
            "Iosevka".to_string().into(),
        )]);
        let direct = build_font_selector_stack(&families, None, FcWeight::Normal, FontStyle::Normal);
        // Twice: the first call fills the memo, the second must be served
        // from it — and both must equal the unmemoised build.
        for _ in 0..2 {
            let memoed =
                build_font_selector_stack_memo(&families, None, FcWeight::Normal, FontStyle::Normal);
            assert_eq!(memoed, direct, "the memo must not alter the stack");
        }
    }
    /// NEGATIVE CONTROL for the memo key.
    ///
    /// A memo whose key omits an input silently serves the previous
    /// document's fonts — the same class of defect as the font-resolver
    /// skip (see `changing_the_font_family_still_resolves_after_the_skip`).
    /// Each case below changes exactly ONE input and requires the result to
    /// change; if the key ever loses a field, the corresponding case fails.
    #[test]
    fn font_stack_memo_is_keyed_on_every_input_it_reads() {
        let iosevka = StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System(
            "Iosevka".to_string().into(),
        )]);
        let menlo = StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System(
            "Menlo".to_string().into(),
        )]);
        let base =
            build_font_selector_stack_memo(&iosevka, None, FcWeight::Normal, FontStyle::Normal);
        assert_eq!(base[0].family, "Iosevka");
        // 1. different FAMILY
        let other_family =
            build_font_selector_stack_memo(&menlo, None, FcWeight::Normal, FontStyle::Normal);
        assert_eq!(
            other_family[0].family, "Menlo",
            "a different family must not be served the memoised stack"
        );
        // 2. different WEIGHT
        let bold = build_font_selector_stack_memo(&iosevka, None, FcWeight::Bold, FontStyle::Normal);
        assert_eq!(
            bold[0].weight,
            FcWeight::Bold,
            "weight is part of every FontSelector, so it must be part of the key"
        );
        // 3. different STYLE
        let italic =
            build_font_selector_stack_memo(&iosevka, None, FcWeight::Normal, FontStyle::Italic);
        assert_eq!(
            italic[0].style,
            FontStyle::Italic,
            "style is part of every FontSelector, so it must be part of the key"
        );
        // 4. different PLATFORM — only observable through a system font,
        //    whose fallback chain is platform-specific.
        let sys = StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System(
            "system:ui".to_string().into(),
        )]);
        let mac = build_font_selector_stack_memo(
            &sys,
            Some(&azul_css::system::Platform::MacOs),
            FcWeight::Normal,
            FontStyle::Normal,
        );
        let win = build_font_selector_stack_memo(
            &sys,
            Some(&azul_css::system::Platform::Windows),
            FcWeight::Normal,
            FontStyle::Normal,
        );
        assert_ne!(
            mac[0].family, win[0].family,
            "the platform selects the system fallback chain, so it must be \
             part of the key — got {:?} for both",
            mac[0].family
        );
    }
    #[test]
    fn build_font_selector_stack_expands_the_system_magic_string_like_the_typed_variant() {
        // "system:ui" as a plain family STRING (the widget const-table idiom)
        // must expand to the same platform chain as SystemType(Ui) — no
        // fontconfig database knows a family literally called "system:ui".
        let typed = StyleFontFamilyVec::from_vec(vec![StyleFontFamily::SystemType(
            azul_css::system::SystemFontType::Ui,
        )]);
        let magic = StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System(
            "system:ui".to_string().into(),
        )]);
        let typed_stack =
            build_font_selector_stack(&typed, None, FcWeight::Normal, FontStyle::Normal);
        let magic_stack =
            build_font_selector_stack(&magic, None, FcWeight::Normal, FontStyle::Normal);
        let typed_families: Vec<&str> = typed_stack.iter().map(|s| s.family.as_str()).collect();
        let magic_families: Vec<&str> = magic_stack.iter().map(|s| s.family.as_str()).collect();
        assert_eq!(magic_families, typed_families, "both spellings expand identically");
        assert!(
            !magic_families.contains(&"system:ui"),
            "the literal magic string must never reach fontconfig: {magic_families:?}"
        );
        // "system:ui:bold" carries the bold weight exactly like the variant.
        let bold = StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System(
            "system:ui:bold".to_string().into(),
        )]);
        let bold_stack =
            build_font_selector_stack(&bold, None, FcWeight::Normal, FontStyle::Normal);
        assert_eq!(bold_stack[0].weight, FcWeight::Bold);
    }
    #[test]
    fn build_font_selector_stack_does_not_duplicate_a_generic_the_author_already_listed() {
        // Case-insensitive: "MONOSPACE" must suppress the "monospace" fallback.
        let families =
            StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("MONOSPACE".to_string().into())]);
        let stack = build_font_selector_stack(&families, None, FcWeight::Normal, FontStyle::Normal);
        let stack = without_system_alias_prefs(&stack);
        assert_eq!(stack.len(), 3, "MONOSPACE + sans-serif + serif");
        assert_eq!(stack[0].family, "MONOSPACE");
        let lower: Vec<String> = stack.iter().map(|s| s.family.to_lowercase()).collect();
        assert_eq!(
            lower.iter().filter(|f| f.as_str() == "monospace").count(),
            1,
            "the generic must appear exactly once"
        );
        // All three generics authored → nothing is appended.
        let families = StyleFontFamilyVec::from_vec(vec![
            StyleFontFamily::System("serif".to_string().into()),
            StyleFontFamily::System("Sans-Serif".to_string().into()),
            StyleFontFamily::System("monospace".to_string().into()),
        ]);
        let stack = build_font_selector_stack(&families, None, FcWeight::Normal, FontStyle::Normal);
        let stack = without_system_alias_prefs(&stack);
        assert_eq!(stack.len(), 3);
    }
    #[test]
    fn build_font_selector_stack_passes_hostile_family_names_through_untouched() {
        let huge = "A".repeat(10_000);
        let families = StyleFontFamilyVec::from_vec(vec![
            StyleFontFamily::System(String::new().into()),
            StyleFontFamily::System("  \t\n  ".to_string().into()),
            StyleFontFamily::System("M🎉 ǝɔɐɟdʎʇ — «Шрифт»".to_string().into()),
            StyleFontFamily::System(huge.clone().into()),
        ]);
        let stack = build_font_selector_stack(&families, None, FcWeight::Normal, FontStyle::Normal);
        let stack = without_system_alias_prefs(&stack);
        assert_eq!(stack.len(), 7, "4 authored + 3 generic fallbacks");
        assert_eq!(stack[0].family, "");
        assert_eq!(stack[2].family, "M🎉 ǝɔɐɟdʎʇ — «Шрифт»");
        assert_eq!(stack[3].family.len(), huge.len());
        assert_eq!(stack[6].family, "monospace");
    }
    // =====================================================================
    // Font chain resolution against an empty FcFontCache
    // =====================================================================
    #[test]
    fn resolve_font_chains_yields_nothing_for_an_empty_or_degenerate_collection() {
        let fc = FcFontCache::default();
        let collected = CollectedFontStacks {
            font_stacks: Vec::new(),
            hash_to_index: HashMap::new(),
            font_refs: HashMap::new(),
        };
        assert!(resolve_font_chains(&collected, &fc, Some(&[])).is_empty());
        // An empty *inner* stack is skipped, not turned into a phantom chain.
        let collected = CollectedFontStacks {
            font_stacks: vec![Vec::new()],
            hash_to_index: HashMap::new(),
            font_refs: HashMap::new(),
        };
        assert!(resolve_font_chains(&collected, &fc, Some(&[])).is_empty());
    }
    // =====================================================================
    // Font-size resolution against a real StyledDom
    // =====================================================================
    #[test]
    fn font_size_getters_return_the_default_for_an_unstyled_dom() {
        let sd = StyledDom::default();
        let root = NodeId::new(0);
        let st = normal();
        assert_eq!(get_element_font_size(&sd, root, &st), DEFAULT_FONT_SIZE);
        assert_eq!(get_root_font_size(&sd, &st), DEFAULT_FONT_SIZE);
        // The root has no parent → the parent size falls back to the default.
        assert_eq!(get_parent_font_size(&sd, root, &st), DEFAULT_FONT_SIZE);
        assert_eq!(
            resolve_font_size_slow(&sd, root, &st),
            DEFAULT_FONT_SIZE,
            "the slow path must agree with the memoised one"
        );
    }
    #[test]
    fn font_size_resolution_is_identical_on_the_normal_and_the_pseudo_state_paths() {
        // A non-normal state skips every compact-cache fast path; with no :hover rule
        // in the stylesheet it must still land on exactly the same pixel value.
        let sd = body_with_divs(1, "body { font-size: 32px; }");
        let child = NodeId::new(1);
        assert_eq!(
            get_element_font_size(&sd, child, &normal()),
            get_element_font_size(&sd, child, &hovered()),
        );
    }
    #[test]
    fn font_size_em_resolves_against_the_parent_and_not_the_default() {
        let sd = body_with_divs(1, "body { font-size: 32px; } div { font-size: 2em; }");
        let root = NodeId::new(0);
        let child = NodeId::new(1);
        assert_eq!(get_element_font_size(&sd, root, &state_of(&sd, root)), 32.0);
        assert_eq!(
            get_element_font_size(&sd, child, &state_of(&sd, child)),
            64.0,
            "2em under a 32px parent is 64px — resolving against DEFAULT_FONT_SIZE would give 32"
        );
        assert_eq!(
            get_parent_font_size(&sd, child, &state_of(&sd, child)),
            32.0
        );
        assert_eq!(get_root_font_size(&sd, &state_of(&sd, child)), 32.0);
    }
    #[test]
    fn font_size_getters_stay_finite_for_hostile_stylesheet_values() {
        // Zero, huge and negative authored font sizes must not produce NaN/inf, and
        // must not panic on the em-inheritance walk.
        for css in [
            "body { font-size: 0px; }",
            "body { font-size: 0; }",
            "body { font-size: 999999px; }",
            "body { font-size: -10px; }",
            "body { font-size: 1e30px; }",
            "div { font-size: 1000em; }",
            "div { font-size: 0em; }",
        ] {
            let sd = body_with_divs(1, css);
            for id in [NodeId::new(0), NodeId::new(1)] {
                let st = state_of(&sd, id);
                let px = get_element_font_size(&sd, id, &st);
                assert!(
                    px.is_finite(),
                    "{css:?} produced a non-finite font-size ({px}) on node {id:?}"
                );
                assert_eq!(
                    px,
                    resolve_font_size_slow(&sd, id, &st),
                    "memoised and slow paths disagree for {css:?}"
                );
            }
        }
    }
    #[test]
    fn font_size_resolution_walks_a_deep_ancestor_chain_without_recursing() {
        // resolve_font_size_slow used to self-recurse up the parent chain and blow the
        // stack. Build a 64-deep chain and check the iterative walk survives it.
        const DEPTH: usize = 64;
        let mut dom = Dom::create_div();
        for _ in 0..DEPTH {
            dom = Dom::create_div().with_children(vec![dom].into());
        }
        let mut root = Dom::create_body().with_children(vec![dom].into());
        let sd = StyledDom::create(&mut root, parse("body { font-size: 20px; }"));
        let deepest = NodeId::new(sd.node_data.len() - 1);
        let st = state_of(&sd, deepest);
        let px = get_element_font_size(&sd, deepest, &st);
        assert!(px.is_finite() && px > 0.0);
        assert_eq!(px, resolve_font_size_slow(&sd, deepest, &st));
    }
    #[test]
    fn resolve_font_size_one_is_stable_under_nan_and_infinite_context_sizes() {
        // parent/root font sizes are f32 inputs the caller supplies; degenerate values
        // must not panic, and (with no authored font-size) must not leak into the result.
        let sd = StyledDom::default();
        let root = NodeId::new(0);
        let st = normal();
        for (parent, rootsz) in [
            (0.0_f32, 0.0_f32),
            (f32::NAN, f32::NAN),
            (f32::INFINITY, f32::NEG_INFINITY),
            (f32::MAX, f32::MIN),
            (-1.0, -1.0),
        ] {
            let px = resolve_font_size_one(&sd, root, &st, parent, rootsz);
            assert_eq!(
                px, DEFAULT_FONT_SIZE,
                "an unstyled node ignores the context and falls back to the default \
                 (parent={parent}, root={rootsz})"
            );
        }
    }
    // =====================================================================
    // Option<NodeId> getters — the None branch
    // =====================================================================
    #[test]
    fn optional_node_getters_return_their_documented_defaults_for_none() {
        let sd = StyledDom::default();
        assert_eq!(get_z_index(&sd, None), 0);
        assert!(is_z_index_auto(&sd, None));
        assert_eq!(get_break_before(&sd, None), PageBreak::Auto);
        assert_eq!(get_break_after(&sd, None), PageBreak::Auto);
        assert_eq!(get_break_inside(&sd, None), BreakInside::Auto);
        assert_eq!(get_orphans(&sd, None), 2);
        assert_eq!(get_widows(&sd, None), 2);
        assert_eq!(
            get_box_decoration_break(&sd, None),
            BoxDecorationBreak::Slice
        );
        assert_eq!(
            get_display_property(&sd, None),
            MultiValue::Exact(LayoutDisplay::Inline),
            "a missing node is treated as anonymous inline content"
        );
        assert_eq!(get_list_style_type(&sd, None), StyleListStyleType::default());
        assert_eq!(
            get_list_style_position(&sd, None),
            StyleListStylePosition::default()
        );
        assert_eq!(get_caret_style(&sd, None).width, DEFAULT_CARET_WIDTH_PX);
        assert_eq!(
            get_caret_style(&sd, None).animation_duration,
            CssDuration::from_millis(DEFAULT_CARET_BLINK_MS)
        );
        let sel = get_selection_style(&sd, None, None);
        assert_eq!(sel.radius, 0.0);
        assert_eq!(sel.text_color, None);
    }
    /// `caret-animation-duration` has to reach the caret style in the UNIT the
    /// stylesheet used. A `5t` that arrived here as 5 *milliseconds* would blink
    /// ~16x too fast and would be indistinguishable, at this layer, from a
    /// deliberate wall-clock value.
    #[test]
    fn caret_animation_duration_preserves_the_unit_the_stylesheet_used() {
        let child = Some(NodeId::new(1));
        let sd = body_with_divs(1, "div { caret-animation-duration: 5t; }");
        assert_eq!(
            get_caret_style(&sd, child).animation_duration,
            CssDuration::from_ticks(5)
        );
        let sd = body_with_divs(1, "div { caret-animation-duration: 250ms; }");
        assert_eq!(
            get_caret_style(&sd, child).animation_duration,
            CssDuration::from_millis(250)
        );
        let sd = body_with_divs(1, "div { caret-animation-duration: 1s; }");
        assert_eq!(
            get_caret_style(&sd, child).animation_duration,
            CssDuration::from_millis(1000)
        );
        // 60 frames and 1000ms are the same span but NOT the same value: the
        // unit is preserved, not normalised.
        let sd = body_with_divs(1, "div { caret-animation-duration: 60t; }");
        assert_ne!(
            get_caret_style(&sd, child).animation_duration,
            CssDuration::from_millis(1000)
        );
    }
    /// `text_color` is the `::selection` colour the text pass must paint
    /// selected glyphs in. Both authorities have to arrive intact — a stylesheet
    /// `-azul-selection-color`, and the OS highlight colour when the stylesheet
    /// is silent — because the alternative (leaving glyphs their normal colour
    /// under an OPAQUE highlight) is dark-on-dark.
    #[test]
    fn selection_style_carries_the_css_text_colour_and_the_system_fallback() {
        let child = Some(NodeId::new(1));
        let red = ColorU { r: 255, g: 0, b: 0, a: 255 };
        let green = ColorU { r: 0, g: 255, b: 0, a: 255 };
        let sd = body_with_divs(
            1,
            "div { -azul-selection-color: #ff0000; -azul-selection-background-color: #00ff00; }",
        );
        let sel = get_selection_style(&sd, child, None);
        assert_eq!(sel.text_color, Some(red), "the stylesheet is the top authority");
        assert_eq!(sel.bg_color, green);
        assert_eq!(sel.text_color_or(ColorU::BLACK), red);
        // No stylesheet opinion: the system style answers for BOTH halves.
        let system = std::sync::Arc::new(azul_css::system::defaults::windows_11_light());
        let sd = body_with_divs(1, "");
        let sel = get_selection_style(&sd, child, Some(&system));
        assert_eq!(
            sel.text_color,
            Some(ColorU::new_rgb(255, 255, 255)),
            "the OS highlight text colour"
        );
        assert_eq!(sel.bg_color, ColorU::new_rgb(0, 120, 215));
        // The stylesheet still wins over the system style.
        let sd = body_with_divs(1, "div { -azul-selection-color: #ff0000; }");
        assert_eq!(
            get_selection_style(&sd, child, Some(&system)).text_color,
            Some(red)
        );
    }
    /// No authority at all ⇒ `None`, and the painter must leave the glyph's own
    /// colour untouched rather than substituting a default.
    #[test]
    fn selection_style_without_any_text_colour_keeps_the_normal_glyph_colour() {
        let sd = body_with_divs(1, "");
        let sel = get_selection_style(&sd, Some(NodeId::new(1)), None);
        assert_eq!(sel.text_color, None);
        let normal = ColorU { r: 17, g: 34, b: 51, a: 255 };
        assert_eq!(sel.text_color_or(normal), normal);
    }
    #[test]
    fn z_index_defaults_to_auto_and_reads_back_explicit_integers() {
        let sd = body_with_divs(1, "");
        let root = NodeId::new(0);
        assert_eq!(get_z_index(&sd, Some(root)), 0);
        assert!(is_z_index_auto(&sd, Some(root)));
        for (css, want) in [
            ("div { z-index: 0; }", 0_i32),
            ("div { z-index: 7; }", 7),
            ("div { z-index: -7; }", -7),
        ] {
            let sd = body_with_divs(1, css);
            let child = Some(NodeId::new(1));
            assert_eq!(get_z_index(&sd, child), want, "{css:?}");
            assert!(
                !is_z_index_auto(&sd, child),
                "an explicit integer is not auto ({css:?})"
            );
        }
        // `z-index: auto` reads back as 0 but is still reported as auto.
        let sd = body_with_divs(1, "div { z-index: auto; }");
        let child = Some(NodeId::new(1));
        assert_eq!(get_z_index(&sd, child), 0);
        assert!(is_z_index_auto(&sd, child));
    }
    #[test]
    fn z_index_reads_back_the_i16_encoding_boundaries_and_falls_through_above_them() {
        // The compact cache packs z-index into an i16 whose top four values are
        // sentinels (I16_SENTINEL_THRESHOLD = 32764). Values at or above the threshold
        // must be stored as the sentinel and re-read via the cascade, NOT truncated.
        for (css, want) in [
            ("div { z-index: 32763; }", 32_763_i32), // largest directly encodable
            ("div { z-index: -32768; }", -32_768),   // i16 lower bound
            ("div { z-index: 32764; }", 32_764),     // == threshold → sentinel → cascade
            ("div { z-index: 99999; }", 99_999),     // far above → sentinel → cascade
            ("div { z-index: 2147483647; }", i32::MAX),
        ] {
            let sd = body_with_divs(1, css);
            let child = Some(NodeId::new(1));
            assert_eq!(
                get_z_index(&sd, child),
                want,
                "{css:?} must survive the i16 compact encoding"
            );
            assert!(
                !is_z_index_auto(&sd, child),
                "an explicit (if huge) integer is not auto ({css:?})"
            );
        }
    }
    // =====================================================================
    // Border radius
    // =====================================================================
    #[test]
    fn border_radius_is_zero_by_default_for_every_degenerate_element_and_viewport_size() {
        let sd = StyledDom::default();
        let root = NodeId::new(0);
        let st = normal();
        let sizes = [
            (0.0_f32, 0.0_f32),
            (-100.0, -100.0),
            (f32::NAN, f32::NAN),
            (f32::INFINITY, f32::INFINITY),
            (f32::MAX, f32::MAX),
            (f32::MIN_POSITIVE, f32::MIN_POSITIVE),
        ];
        for (w, h) in sizes {
            let element = PhysicalSizeImport {
                width: w,
                height: h,
            };
            let viewport = LogicalSize::new(w, h);
            let r = get_border_radius(&sd, root, &st, element, viewport);
            assert_eq!(r.top_left, 0.0, "element=({w}, {h})");
            assert_eq!(r.top_right, 0.0, "element=({w}, {h})");
            assert_eq!(r.bottom_left, 0.0, "element=({w}, {h})");
            assert_eq!(r.bottom_right, 0.0, "element=({w}, {h})");
        }
    }
    #[test]
    fn border_radius_resolves_authored_pixels_on_both_the_normal_and_the_pseudo_path() {
        let sd = body_with_divs(1, "div { border-radius: 12px; }");
        let child = NodeId::new(1);
        let element = PhysicalSizeImport {
            width: 100.0,
            height: 50.0,
        };
        let viewport = LogicalSize::new(800.0, 600.0);
        for st in [normal(), hovered()] {
            let r = get_border_radius(&sd, child, &st, element, viewport);
            for corner in [r.top_left, r.top_right, r.bottom_left, r.bottom_right] {
                assert!(corner.is_finite(), "corner must stay finite");
                assert_eq!(corner, 12.0);
            }
        }
        let raw = get_style_border_radius(&sd, child, &normal());
        assert!(raw.top_left.number.get().is_finite());
    }
    #[test]
    fn border_radius_percentages_stay_finite_for_zero_and_infinite_element_sizes() {
        let sd = body_with_divs(1, "div { border-radius: 50%; }");
        let child = NodeId::new(1);
        let viewport = LogicalSize::new(0.0, 0.0);
        for (w, h) in [
            (0.0_f32, 0.0_f32),
            (f32::MAX, f32::MAX),
            (-10.0, -10.0),
            (f32::INFINITY, 1.0),
        ] {
            let element = PhysicalSizeImport {
                width: w,
                height: h,
            };
            // Only the pseudo-state path actually resolves the % (the compact cache
            // stores pre-resolved px), so exercise it explicitly.
            let r = get_border_radius(&sd, child, &hovered(), element, viewport);
            for corner in [r.top_left, r.top_right, r.bottom_left, r.bottom_right] {
                assert!(
                    !corner.is_nan(),
                    "a {w}x{h} element produced a NaN corner radius"
                );
            }
        }
    }
    // =====================================================================
    // Smoke coverage for the remaining StyledDom getters
    // =====================================================================
    #[test]
    fn optional_style_getters_are_all_none_on_an_unstyled_node() {
        let sd = body_with_divs(1, "");
        let id = NodeId::new(1);
        for st in [normal(), hovered()] {
            assert!(get_shape_inside(&sd, id, &st).is_none());
            assert!(get_shape_outside(&sd, id, &st).is_none());
            assert!(get_line_clamp(&sd, id, &st).is_none());
            assert!(get_initial_letter(&sd, id, &st).is_none());
            assert!(get_hanging_punctuation(&sd, id, &st).is_none());
            assert!(get_text_combine_upright(&sd, id, &st).is_none());
            assert!(get_hyphenation_language(&sd, id, &st).is_none());
            assert!(get_column_count(&sd, id, &st).is_none());
            assert!(get_filter(&sd, id, &st).is_none());
            assert!(get_backdrop_filter(&sd, id, &st).is_none());
            assert!(get_box_shadow_left(&sd, id, &st).is_none());
            assert!(get_box_shadow_right(&sd, id, &st).is_none());
            assert!(get_box_shadow_top(&sd, id, &st).is_none());
            assert!(get_box_shadow_bottom(&sd, id, &st).is_none());
            assert!(get_text_shadow(&sd, id, &st).is_none());
            assert!(get_transform(&sd, id, &st).is_none());
            assert!(get_counter_reset(&sd, id, &st).is_none());
            assert!(get_counter_increment(&sd, id, &st).is_none());
            assert!(get_clip_path(&sd, id, &st).is_none());
            assert!(get_grid_template_areas_prop(&sd, id, &st).is_none());
        }
    }
    #[test]
    fn numeric_style_getters_use_their_documented_defaults() {
        let sd = body_with_divs(1, "");
        let id = NodeId::new(1);
        for st in [normal(), hovered()] {
            assert_eq!(get_opacity(&sd, id, &st), 1.0, "opacity defaults to 1.0");
            assert_eq!(
                get_exclusion_margin(&sd, id, &st),
                0.0,
                "exclusion-margin defaults to 0.0"
            );
            assert!(get_scrollbar_width_px(&sd, id, &st).is_finite());
            assert!(get_scrollbar_width_px(&sd, id, &st) >= 0.0);
        }
    }
    #[test]
    fn opacity_in_range_agrees_on_the_compact_and_the_cascade_path() {
        for css in [
            "div { opacity: 0; }",
            "div { opacity: 1; }",
            "div { opacity: 0.5; }",
        ] {
            let sd = body_with_divs(1, css);
            let id = NodeId::new(1);
            let fast = get_opacity(&sd, id, &normal()); // compact-cache u8 path
            let slow = get_opacity(&sd, id, &hovered()); // full cascade path
            assert!(fast.is_finite() && slow.is_finite(), "{css:?}");
            assert!(
                (0.0..=1.0).contains(&fast),
                "{css:?} read back out of range on the compact path: {fast}"
            );
            assert!(
                (fast - slow).abs() < 0.01,
                "{css:?}: compact path says {fast}, cascade path says {slow}"
            );
        }
        // A mid-range value must actually take effect (i.e. differ from the 1.0 default).
        let half = get_opacity(&body_with_divs(1, "div { opacity: 0.5; }"), NodeId::new(1), &normal());
        assert!(half < 1.0 && half > 0.0, "opacity: 0.5 read back as {half}");
    }
    #[test]
    fn opacity_never_returns_nan_or_infinity_for_out_of_range_authored_values() {
        // NOTE: CSS Color 3 clamps opacity to [0,1]. The compact-cache encoder does
        // clamp (`normalized().clamp(0.0, 1.0)`), but `get_opacity`'s cascade path
        // returns `inner.normalized()` unclamped — so a non-Normal pseudo-state can
        // report an out-of-range opacity. That divergence is reported separately; the
        // invariant asserted here (always a finite number) must hold on BOTH paths.
        for css in [
            "div { opacity: 5; }",
            "div { opacity: -3; }",
            "div { opacity: 1e30; }",
        ] {
            let sd = body_with_divs(1, css);
            let id = NodeId::new(1);
            for st in [normal(), hovered()] {
                let o = get_opacity(&sd, id, &st);
                assert!(o.is_finite(), "{css:?} produced a non-finite opacity: {o}");
            }
            // The compact path is the one the encoder clamps, so it is always in range.
            let fast = get_opacity(&sd, id, &normal());
            assert!(
                (0.0..=1.0).contains(&fast),
                "{css:?} escaped the compact-cache clamp: {fast}"
            );
        }
    }
    #[test]
    fn enum_property_getters_stay_deterministic_across_pseudo_states() {
        let sd = body_with_divs(1, "");
        let id = NodeId::new(1);
        for st in [normal(), hovered()] {
            // These may be Auto or Exact depending on the UA sheet; the contract under
            // test is only that they answer without panicking and answer consistently.
            let gutter = get_scrollbar_gutter_property(&sd, id, &st);
            assert_eq!(gutter, get_scrollbar_gutter_property(&sd, id, &st));
            let orientation = get_text_orientation_property(&sd, id, &st);
            assert_eq!(orientation, get_text_orientation_property(&sd, id, &st));
            let valign = get_vertical_align_property(&sd, id, &st);
            assert_eq!(valign, get_vertical_align_property(&sd, id, &st));
            let _ = get_background_color(&sd, id, &st);
            let _ = get_background_contents(&sd, id, &st);
            let _ = get_border_info(&sd, id, &st);
            let _ = get_border_spacing(&sd, id, &st);
            let _ = get_height_value(&sd, id, &st);
            let _ = get_line_height_value(&sd, id, &st);
            let _ = get_text_indent_value(&sd, id, &st);
        }
        // vertical-align defaults to the baseline for an unstyled div.
        assert!(matches!(
            get_vertical_align_for_node(&sd, id),
            crate::text3::cache::VerticalAlign::Baseline
        ));
    }
    #[test]
    fn get_inline_border_info_is_none_without_borders_and_survives_a_degenerate_viewport() {
        let sd = body_with_divs(1, "");
        let id = NodeId::new(1);
        let st = normal();
        let info = get_border_info(&sd, id, &st);
        for viewport in [
            PhysicalSize::new(0.0, 0.0),
            PhysicalSize::new(f32::NAN, f32::NAN),
            PhysicalSize::new(f32::INFINITY, f32::INFINITY),
            PhysicalSize::new(-1.0, -1.0),
            PhysicalSize::new(f32::MAX, f32::MAX),
        ] {
            assert!(
                get_inline_border_info(&sd, id, &st, &info, viewport).is_none(),
                "a node with neither border nor padding has no inline border box"
            );
        }
    }
    #[test]
    fn get_inline_border_info_reports_finite_widths_for_a_bordered_node() {
        let sd = body_with_divs(1, "div { border: 3px solid red; padding: 5px; }");
        let id = NodeId::new(1);
        let st = normal();
        let info = get_border_info(&sd, id, &st);
        let inline = get_inline_border_info(&sd, id, &st, &info, PhysicalSize::new(800.0, 600.0))
            .expect("a bordered + padded node must produce an InlineBorderInfo");
        for w in [inline.top, inline.right, inline.bottom, inline.left] {
            assert!(w.is_finite() && w >= 0.0, "border width {w}");
        }
        for p in [
            inline.padding_top,
            inline.padding_right,
            inline.padding_bottom,
            inline.padding_left,
        ] {
            assert!(p.is_finite() && p >= 0.0, "padding {p}");
        }
        assert!(inline.is_first_fragment && inline.is_last_fragment);
        assert!(!inline.is_rtl, "the default direction is ltr");
        // The same node under a NaN viewport: px lengths do not consult the viewport,
        // so the result must stay finite rather than turn into NaN.
        let nan_vp = get_inline_border_info(&sd, id, &st, &info, PhysicalSize::new(f32::NAN, f32::NAN))
            .expect("px borders do not depend on the viewport");
        assert!(nan_vp.top.is_finite() && nan_vp.padding_top.is_finite());
    }
    #[test]
    fn get_style_properties_stays_finite_for_every_degenerate_viewport() {
        let sd = body_with_text("hello");
        for viewport in [
            PhysicalSize::new(0.0, 0.0),
            PhysicalSize::new(-1.0, -1.0),
            PhysicalSize::new(f32::NAN, f32::NAN),
            PhysicalSize::new(f32::INFINITY, f32::INFINITY),
            PhysicalSize::new(f32::MAX, f32::MAX),
        ] {
            for id in [NodeId::new(0), NodeId::new(1)] {
                let props = get_style_properties(&sd, id, None, viewport);
                assert!(
                    props.font_size_px.is_finite(),
                    "viewport {viewport:?} produced a non-finite font size"
                );
                assert!(props.font_size_px > 0.0);
            }
        }
    }
    // =====================================================================
    // user-select / contenteditable predicates
    // =====================================================================
    #[test]
    fn is_text_selectable_is_true_by_default_and_false_for_user_select_none() {
        let sd = body_with_divs(1, "");
        assert!(
            is_text_selectable(&sd, NodeId::new(1), &normal()),
            "text is selectable unless user-select says otherwise"
        );
        let sd = body_with_divs(1, "div { user-select: none; }");
        assert!(!is_text_selectable(&sd, NodeId::new(1), &normal()));
        let sd = body_with_divs(1, "div { user-select: text; }");
        assert!(is_text_selectable(&sd, NodeId::new(1), &normal()));
    }
    #[test]
    fn contenteditable_is_false_everywhere_on_a_plain_dom() {
        let sd = body_with_divs(2, "");
        for idx in 0..sd.node_data.len() {
            let id = NodeId::new(idx);
            assert!(!is_node_contenteditable(&sd, id), "node {idx}");
            assert!(!is_node_contenteditable_inherited(&sd, id), "node {idx}");
            assert_eq!(find_contenteditable_ancestor(&sd, id), None, "node {idx}");
        }
    }
    #[test]
    fn contenteditable_is_inherited_by_descendants_but_not_reported_as_direct() {
        // body(0) > editable div(1) > plain div(2)
        let mut editable = Dom::create_div().with_children(vec![Dom::create_div()].into());
        editable.root.set_contenteditable(true);
        let mut dom = Dom::create_body().with_children(vec![editable].into());
        let sd = StyledDom::create(&mut dom, Css::empty());
        assert_eq!(sd.node_data.len(), 3);
        let (body, editable, child) = (NodeId::new(0), NodeId::new(1), NodeId::new(2));
        assert!(!is_node_contenteditable(&sd, body));
        assert!(is_node_contenteditable(&sd, editable));
        assert!(
            !is_node_contenteditable(&sd, child),
            "the direct check must not walk up the tree"
        );
        assert!(!is_node_contenteditable_inherited(&sd, body));
        assert!(is_node_contenteditable_inherited(&sd, editable));
        assert!(
            is_node_contenteditable_inherited(&sd, child),
            "editability is inherited from the ancestor"
        );
        assert_eq!(find_contenteditable_ancestor(&sd, body), None);
        assert_eq!(find_contenteditable_ancestor(&sd, editable), Some(editable));
        assert_eq!(
            find_contenteditable_ancestor(&sd, child),
            Some(editable),
            "a nested node resolves to its editable container, not to itself"
        );
    }
    // =====================================================================
    // Codepoint / script collection
    // =====================================================================
    #[test]
    fn collect_used_codepoints_strips_ascii_while_the_all_variant_keeps_it() {
        // ASCII + Latin-1 + CJK + an astral-plane emoji (a surrogate pair in UTF-16).
        let sd = body_with_text("aé漢🎉");
        let non_ascii = collect_used_codepoints(&sd);
        assert_eq!(non_ascii.len(), 3, "the ASCII 'a' is dropped");
        assert!(non_ascii.contains(&0x00E9));
        assert!(non_ascii.contains(&0x6F22));
        assert!(non_ascii.contains(&0x0001_F389), "astral plane codepoint");
        assert!(!non_ascii.contains(&u32::from(b'a')));
        let all = collect_used_codepoints_all(&sd);
        assert_eq!(all.len(), 4);
        assert!(all.contains(&'a'));
        assert!(all.contains(&'🎉'));
    }
    #[test]
    fn collect_used_codepoints_dedupes_and_handles_empty_and_ascii_only_text() {
        // Repeats collapse (BTreeSet), and a DOM with no text at all yields nothing.
        let sd = body_with_text("ααα");
        assert_eq!(collect_used_codepoints(&sd).len(), 1);
        let sd = body_with_text("");
        assert!(collect_used_codepoints(&sd).is_empty());
        assert!(collect_used_codepoints_all(&sd).is_empty());
        let sd = body_with_divs(3, "");
        assert!(
            collect_used_codepoints(&sd).is_empty(),
            "element nodes carry no codepoints"
        );
        let sd = body_with_text("plain ascii");
        assert!(collect_used_codepoints(&sd).is_empty());
        assert!(!collect_used_codepoints_all(&sd).is_empty());
    }
    #[test]
    fn scripts_present_in_styled_dom_is_empty_for_ascii_and_bounded_by_the_default_set() {
        let ascii = body_with_text("hello world");
        assert!(
            scripts_present_in_styled_dom(&ascii).is_empty(),
            "an ASCII-only page must not drag in any unicode fallback script"
        );
        let empty = StyledDom::default();
        assert!(scripts_present_in_styled_dom(&empty).is_empty());
        let cjk = body_with_text("漢字");
        let scripts = scripts_present_in_styled_dom(&cjk);
        assert!(!scripts.is_empty(), "CJK text must report at least one script");
        assert!(
            scripts.len() <= DEFAULT_UNICODE_FALLBACK_SCRIPTS.len(),
            "the result is always a subset of the default script set"
        );
        for r in &scripts {
            assert!(r.start <= r.end, "a script range must not be inverted");
        }
    }
    #[test]
    fn collect_font_stacks_from_styled_dom_keeps_its_index_map_consistent() {
        let platform = azul_css::system::Platform::current();
        for sd in [
            StyledDom::default(),
            body_with_text("hello"),
            body_with_divs(3, "div { font-family: Iosevka, monospace; }"),
        ] {
            let collected = collect_font_stacks_from_styled_dom(&sd, &platform);
            assert_eq!(
                collected.hash_to_index.len(),
                collected.font_stacks.len(),
                "every recorded hash must map to exactly one stack"
            );
            for &idx in collected.hash_to_index.values() {
                assert!(
                    idx < collected.font_stacks.len(),
                    "hash_to_index points past the end of font_stacks"
                );
            }
            for stack in &collected.font_stacks {
                assert!(!stack.is_empty(), "an empty font stack is never recorded");
            }
        }
    }
}
#[cfg(test)]
mod memory_font_tier_tests {
    use super::*;
    use crate::text3::cache::{MemoryFace, MemoryFontTier};
6
    fn face(tier: MemoryFontTier) -> MemoryFace {
6
        MemoryFace {
6
            tier,
6
            font_match: rust_fontconfig::FontMatch {
6
                id: FontId::new(),
6
                unicode_ranges: Vec::new(),
6
                fallbacks: Vec::new(),
6
            },
6
            weight: FcWeight::Normal,
6
            italic: false,
6
            oblique: false,
6
            stretch: rust_fontconfig::FcStretch::Normal,
6
            weight_axis: None,
6
        }
6
    }
4
    fn split(
4
        stack: &[&str],
4
        registered: &[(&str, MemoryFontTier)],
4
    ) -> (Vec<String>, Vec<String>, Vec<String>) {
4
        let mut memory_families: HashMap<String, Vec<MemoryFace>> = HashMap::new();
10
        for (family, tier) in registered {
6
            memory_families
6
                .entry(rust_fontconfig::utils::normalize_family_name(family))
6
                .or_default()
6
                .push(face(*tier));
6
        }
6
        let families: Vec<String> = stack.iter().map(|s| (*s).to_string()).collect();
4
        let (primary, disk, fallback) =
4
            split_memory_matches(&families, &memory_families, FcWeight::Normal, false, false);
        (
4
            primary.into_iter().map(|g| g.css_name).collect(),
4
            disk,
4
            fallback.into_iter().map(|g| g.css_name).collect(),
        )
4
    }
    /// A primary face is the family: the disk is never asked about it.
    #[test]
1
    fn a_primary_face_takes_the_family_from_the_disk() {
1
        let (primary, disk, fallback) =
1
            split(&["Helvetica"], &[("Helvetica", MemoryFontTier::Primary)]);
1
        assert_eq!(primary, ["Helvetica"]);
1
        assert!(disk.is_empty());
1
        assert!(fallback.is_empty());
1
    }
    /// A fallback face does NOT take the family - the disk still gets asked, and
    /// the face only waits behind whatever the disk turns up. This is what lets
    /// printpdf offer the 14 standard PDF fonts for `sans-serif` without those
    /// Win-1252 subsets displacing the system's Unicode faces on a desktop.
    #[test]
1
    fn a_fallback_face_leaves_the_family_to_the_disk() {
1
        let (primary, disk, fallback) =
1
            split(&["sans-serif"], &[("sans-serif", MemoryFontTier::Fallback)]);
1
        assert!(primary.is_empty());
1
        assert_eq!(disk, ["sans-serif"], "the disk must still get first refusal");
1
        assert_eq!(fallback, ["sans-serif"]);
1
    }
    /// With nothing installed - wasm - the disk probe comes back empty and the
    /// fallback face is what is left, which is the whole point of the tier.
    #[test]
1
    fn both_tiers_can_appear_in_one_stack() {
1
        let (primary, disk, fallback) = split(
1
            &["Helvetica", "Arial", "sans-serif"],
1
            &[
1
                ("Helvetica", MemoryFontTier::Primary),
1
                ("sans-serif", MemoryFontTier::Fallback),
1
            ],
1
        );
1
        assert_eq!(primary, ["Helvetica"]);
1
        assert_eq!(disk, ["Arial", "sans-serif"]);
1
        assert_eq!(fallback, ["sans-serif"]);
1
    }
    /// Registering both tiers for one family must not make it ambiguous: the
    /// primary face wins and the fallback is not also offered.
    #[test]
1
    fn primary_beats_fallback_for_the_same_family() {
1
        let (primary, disk, fallback) = split(
1
            &["Helvetica"],
1
            &[
1
                ("Helvetica", MemoryFontTier::Fallback),
1
                ("Helvetica", MemoryFontTier::Primary),
1
            ],
1
        );
1
        assert_eq!(primary, ["Helvetica"]);
1
        assert!(disk.is_empty());
1
        assert!(fallback.is_empty());
1
    }
}
#[cfg(test)]
mod style_interning_tests {
    use super::*;
    /// Nodes that resolve to the SAME computed style must share one
    /// allocation.
    ///
    /// The cache used to be keyed on `(node_id, state, viewport)` alone, so
    /// every node got its own `Arc` even when the style was byte-identical
    /// — sharing by producer identity, not by result value. Measured on one
    /// markdown document: 672 distinct `Arc<StyleProperties>` behind 31,086
    /// glyphs, for a document with ~10 distinct text styles. Stylo shipped
    /// the same defect (109k ComputedValues where 2,200 were expected).
    ///
    /// NEGATIVE CONTROL: dropping the `by_value` lookup (always
    /// `Arc::new`) makes the pointer-equality assertion fail — run and
    /// seen.
    #[test]
1
    fn identical_styles_share_one_allocation() {
        use azul_core::dom::{Dom, NodeId};
        use azul_core::styled_dom::StyledDom;
        // Three sibling texts with no styling of their own: identical
        // computed style, three different nodes.
1
        let dom = Dom::create_body()
1
            .with_child(Dom::create_text_do_not_use_without_block_level_wrapper("one"))
1
            .with_child(Dom::create_text_do_not_use_without_block_level_wrapper("two"))
1
            .with_child(Dom::create_text_do_not_use_without_block_level_wrapper("three"));
1
        let sd = StyledDom::create_from_dom(dom);
1
        let viewport = PhysicalSize::new(800.0, 600.0);
1
        let mut cache = StyleCache::default();
1
        let n = sd.node_data.as_container().internal.len();
1
        let mut arcs = Vec::new();
4
        for i in 0..n {
4
            arcs.push(get_style_properties_cached(
4
                &mut cache,
4
                &sd,
4
                NodeId::new(i),
4
                None,
4
                viewport,
4
            ));
4
        }
1
        let distinct: std::collections::BTreeSet<usize> = arcs
1
            .iter()
4
            .map(|a| std::sync::Arc::as_ptr(a) as usize)
1
            .collect();
1
        assert!(
1
            distinct.len() < arcs.len(),
            "{} nodes produced {} DISTINCT StyleProperties allocations — \
             identical computed styles are not being shared, which is the \
             defect this cache exists to prevent",
            arcs.len(),
            distinct.len()
        );
        // Asking for the same node twice must not build anything new.
1
        let again = get_style_properties_cached(
1
            &mut cache,
1
            &sd,
1
            NodeId::new(0),
1
            None,
1
            viewport,
        );
1
        assert!(
1
            std::sync::Arc::ptr_eq(&arcs[0], &again),
            "a repeat query for the same node must hit the by-node memo"
        );
1
    }
    /// A hash collision must NEVER merge two different styles.
    ///
    /// `by_value` buckets on a 64-bit hash. If the bucket were trusted
    /// without an equality check, two distinct styles landing in one bucket
    /// would share an allocation and text would silently render in the
    /// wrong font/colour — a corruption far worse than the memory it saves.
    /// This forces the collision directly by planting a foreign entry under
    /// the hash the next build will compute.
    #[test]
1
    fn a_hash_collision_never_merges_two_different_styles() {
        use azul_core::dom::{Dom, NodeId};
        use azul_core::styled_dom::StyledDom;
1
        let sd = StyledDom::create_from_dom(
1
            Dom::create_body().with_child(Dom::create_text_do_not_use_without_block_level_wrapper("x")),
        );
1
        let viewport = PhysicalSize::new(800.0, 600.0);
1
        let mut cache = StyleCache::default();
1
        let real = get_style_properties_cached(
1
            &mut cache,
1
            &sd,
1
            NodeId::new(0),
1
            None,
1
            viewport,
        );
        // A style that differs from `real`, planted in EVERY bucket so the
        // next lookup is guaranteed to meet a colliding entry.
1
        let mut impostor = (*real).clone();
1
        impostor.font_size_px += 7.0;
1
        assert_ne!(impostor, *real, "the impostor must actually differ");
1
        let impostor = std::sync::Arc::new(impostor);
1
        for bucket in cache.by_value.values_mut() {
1
            bucket.insert(0, std::sync::Arc::clone(&impostor));
1
        }
1
        cache.by_node.clear();
1
        let rebuilt = get_style_properties_cached(
1
            &mut cache,
1
            &sd,
1
            NodeId::new(0),
1
            None,
1
            viewport,
        );
1
        assert_ne!(
1
            *rebuilt, *impostor,
            "a colliding bucket entry must be rejected by the equality \
             check, not returned"
        );
1
        assert_eq!(*rebuilt, *real, "the correct style is still produced");
1
    }
}
#[cfg(test)]
mod alias_prune_tests {
    use super::should_prune_family;
    use std::collections::BTreeSet;
6
    fn set(v: &[&str]) -> BTreeSet<String> {
9
        v.iter().map(|s| s.to_ascii_lowercase()).collect()
6
    }
    /// Only the alias expansion's own inventions get pruned.
    ///
    /// Resolving a family the system does not have costs a real lookup
    /// (~0.52 ms measured); the generic expansion produces ~150 of them per
    /// stack, which was 73.8 ms of a 177 ms cold pagination AND the wall of
    /// `UNRESOLVED font-family` warnings. Pruning them is free. Pruning
    /// anything else would silently change which font renders.
    ///
    /// NEGATIVE CONTROL: dropping the `aliases.contains(..)` term (prune
    /// anything absent) fails the authored-family case; dropping the
    /// `!available.contains(..)` term fails the installed-alias case;
    /// dropping the generic guard fails the generic case. All three run and
    /// seen.
    #[test]
1
    fn only_absent_alias_candidates_are_pruned() {
1
        let aliases = set(&["DejaVu Sans", "ZYSong18030", "Cantarell"]);
1
        let available = set(&["DejaVu Sans", "Liberation Sans"]);
        // Invented by the expansion AND not installed -> the whole point.
1
        assert!(should_prune_family("ZYSong18030", &aliases, &available));
1
        assert!(should_prune_family("Cantarell", &aliases, &available));
        // Case-insensitively, since CSS family names are.
1
        assert!(should_prune_family("zysong18030", &aliases, &available));
        // An alias candidate that IS installed must still be tried, or the
        // system's preferred font stops being reachable.
1
        assert!(!should_prune_family("DejaVu Sans", &aliases, &available));
        // AUTHORED families are never pruned, present or not: pruning a
        // missing one would also swallow its warning, which is the one
        // diagnostic worth keeping.
1
        assert!(!should_prune_family("Liberation Sans", &aliases, &available));
1
        assert!(
1
            !should_prune_family("Comic Sans MS", &aliases, &available),
            "a family the document asked for is not an alias candidate, so it \
             keeps its lookup AND its UNRESOLVED warning"
        );
        // Generics are expanded by the resolver itself.
4
        for g in ["sans-serif", "serif", "monospace", "system-ui"] {
4
            assert!(
4
                !should_prune_family(g, &set(&[g]), &available),
                "{g} must survive even if it appears in an alias list"
            );
        }
1
    }
}