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, StyleFontWeight, StyleFontStyle},
15
            pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
16
            ColorU, PhysicalSize, PixelValue, PropertyContext, ResolutionContext,
17
        },
18
        layout::{
19
            BoxDecorationBreak, BreakInside, LayoutBoxSizing, LayoutClear, LayoutDisplay,
20
            LayoutFlexDirection, LayoutFlexWrap, LayoutFloat, LayoutHeight,
21
            LayoutJustifyContent, LayoutAlignItems, LayoutAlignContent, LayoutOverflow,
22
            LayoutPosition, LayoutWidth, LayoutWritingMode, Orphans, PageBreak, Widows,
23
            StyleScrollbarGutter, StyleOverflowClipMargin,
24
            grid::GridTemplateAreas,
25
        },
26
        property::{CssProperty, CssPropertyType,
27
            LayoutFlexBasisValue, LayoutFlexDirectionValue, LayoutFlexWrapValue,
28
            LayoutFlexGrowValue, LayoutFlexShrinkValue,
29
            LayoutAlignItemsValue, LayoutAlignSelfValue, LayoutAlignContentValue,
30
            LayoutJustifyContentValue, LayoutJustifyItemsValue, LayoutJustifySelfValue,
31
            LayoutGapValue,
32
            LayoutGridTemplateColumnsValue, LayoutGridTemplateRowsValue,
33
            LayoutGridAutoColumnsValue, LayoutGridAutoRowsValue,
34
            LayoutGridAutoFlowValue, LayoutGridColumnValue, LayoutGridRowValue,
35
        },
36
        style::{
37
            border_radius::StyleBorderRadius,
38
            lists::{StyleListStylePosition, StyleListStyleType},
39
            StyleDirection, StyleTextAlign, StyleUserSelect, StyleVerticalAlign,
40
            StyleVisibility, StyleWhiteSpace,
41
            StyleUnicodeBidi, StyleTextBoxTrim, StyleTextBoxEdge,
42
            StyleDominantBaseline, StyleAlignmentBaseline,
43
            StyleInitialLetterAlign, StyleInitialLetterWrap,
44
        },
45
    },
46
};
47

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

            
57
// Font-size resolution helper functions
58

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

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

            
120
    let n = styled_dom.node_data.len();
121
    let mut sizes = alloc::vec![DEFAULT_FONT_SIZE; n];
122
    if n == 0 {
123
        return sizes;
124
    }
125

            
126
    let data_container = styled_dom.node_data.as_container();
127
    let state_container = styled_dom.styled_nodes.as_container();
128
    let hierarchy = styled_dom.node_hierarchy.as_container();
129
    let cache = &styled_dom.css_property_cache.ptr;
130

            
131
    for idx in 0..n {
132
        let dom_id = NodeId::new(idx);
133

            
134
        // Step 1: computed_values short-circuit (matches original).
135
        if let Some(vec) = cache.computed_values.get(idx) {
136
            if let Ok(cv_idx) =
137
                vec.binary_search_by_key(&CssPropertyType::FontSize, |(k, _)| *k)
138
            {
139
                if let CssProperty::FontSize(css_val) = &vec[cv_idx].1.property {
140
                    if let Some(fs) = css_val.get_property() {
141
                        if fs.inner.metric == SizeMetric::Px {
142
                            sizes[idx] = fs.inner.number.get();
143
                            continue;
144
                        }
145
                    }
146
                }
147
            }
148
        }
149

            
150
        // Step 2: full cascade walk.
151
        let parent_font_size = hierarchy
152
            .get(dom_id)
153
            .and_then(|node| node.parent_id())
154
            .map(|p| sizes[p.index()])
155
            .unwrap_or(DEFAULT_FONT_SIZE);
156
        let root_font_size = sizes[0];
157

            
158
        let Some(node_data) = data_container.internal.get(idx) else {
159
            sizes[idx] = DEFAULT_FONT_SIZE;
160
            continue;
161
        };
162
        let Some(styled) = state_container.internal.get(idx) else {
163
            sizes[idx] = DEFAULT_FONT_SIZE;
164
            continue;
165
        };
166
        let node_state = &styled.styled_node_state;
167

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

            
209
        let resolved = cache
210
            .get_font_size(node_data, &dom_id, node_state)
211
            .and_then(|v| v.get_property().cloned())
212
            .map(|v| {
213
                let context = ResolutionContext {
214
                    element_font_size: DEFAULT_FONT_SIZE,
215
                    parent_font_size,
216
                    root_font_size,
217
                    containing_block_size: PhysicalSize::new(0.0, 0.0),
218
                    element_size: None,
219
                    viewport_size: PhysicalSize::new(0.0, 0.0),
220
                };
221
                v.inner
222
                    .resolve_with_context(&context, PropertyContext::FontSize)
223
            });
224

            
225
        // Step 3: fallback to DEFAULT (matches original .unwrap_or).
226
        sizes[idx] = resolved.unwrap_or(DEFAULT_FONT_SIZE);
227
    }
228
    sizes
229
}
230

            
231
/// Un-memoised recursive resolution, used as the fallback for
232
/// non-normal pseudo-states in [`get_element_font_size`] and
233
/// directly by tests that bypass the StyledDom-scoped cache.
234
/// Keeps the original semantics verbatim.
235
586390
fn resolve_font_size_slow(
236
586390
    styled_dom: &StyledDom,
237
586390
    dom_id: NodeId,
238
586390
    node_state: &StyledNodeState,
239
586390
) -> f32 {
240
586390
    let node_data = &styled_dom.node_data.as_container()[dom_id];
241
586390
    let cache = &styled_dom.css_property_cache.ptr;
242

            
243
586390
    if let Some(vec) = cache.computed_values.get(dom_id.index()) {
244
586390
        if let Ok(idx) = vec.binary_search_by_key(
245
586390
            &azul_css::props::property::CssPropertyType::FontSize,
246
            |(k, _)| *k,
247
        ) {
248
23310
            if let azul_css::props::property::CssProperty::FontSize(css_val) = &vec[idx].1.property {
249
23310
                if let Some(fs) = css_val.get_property() {
250
23310
                    if fs.inner.metric == azul_css::props::basic::length::SizeMetric::Px {
251
23310
                        return fs.inner.number.get();
252
                    }
253
                }
254
            }
255
563080
        }
256
    }
257

            
258
563080
    let parent_font_size = styled_dom
259
563080
        .node_hierarchy
260
563080
        .as_container()
261
563080
        .get(dom_id)
262
563080
        .and_then(|node| node.parent_id())
263
563080
        .map(|parent_id| resolve_font_size_slow(styled_dom, parent_id, node_state))
264
563080
        .unwrap_or(DEFAULT_FONT_SIZE);
265

            
266
563080
    let root_font_size = if dom_id == NodeId::new(0) {
267
356090
        DEFAULT_FONT_SIZE
268
    } else {
269
206990
        resolve_font_size_slow(styled_dom, NodeId::new(0), node_state)
270
    };
271

            
272
563080
    cache
273
563080
        .get_font_size(node_data, &dom_id, node_state)
274
563080
        .and_then(|v| v.get_property().cloned())
275
563080
        .map(|v| {
276
            let context = ResolutionContext {
277
                element_font_size: DEFAULT_FONT_SIZE,
278
                parent_font_size,
279
                root_font_size,
280
                containing_block_size: PhysicalSize::new(0.0, 0.0),
281
                element_size: None,
282
                viewport_size: PhysicalSize::new(0.0, 0.0),
283
            };
284
            v.inner
285
                .resolve_with_context(&context, PropertyContext::FontSize)
286
        })
287
563080
        .unwrap_or(DEFAULT_FONT_SIZE)
288
586390
}
289

            
290
/// Helper function to get parent's computed font-size.
291
///
292
/// Retrieves the parent's own `StyledNodeState` so that pseudo-class-specific
293
/// font-size rules (e.g. `div:hover { font-size: 32px }`) are resolved
294
/// against the parent's actual state, not the child's.
295
41160
pub fn get_parent_font_size(
296
41160
    styled_dom: &StyledDom,
297
41160
    dom_id: NodeId,
298
41160
    _node_state: &StyledNodeState, // child's state — intentionally unused
299
41160
) -> f32 {
300
41160
    styled_dom
301
41160
        .node_hierarchy
302
41160
        .as_container()
303
41160
        .get(dom_id)
304
41160
        .and_then(|node| node.parent_id())
305
41160
        .map(|parent_id| {
306
38955
            let parent_state = &styled_dom.styled_nodes.as_container()[parent_id].styled_node_state;
307
38955
            get_element_font_size(styled_dom, parent_id, parent_state)
308
38955
        })
309
41160
        .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE)
310
41160
}
311

            
312
/// Helper function to get root element's font-size.
313
///
314
/// Uses the root element's own `StyledNodeState` so that pseudo-class-specific
315
/// rules are resolved correctly regardless of which node triggered the call.
316
41160
pub fn get_root_font_size(styled_dom: &StyledDom, _node_state: &StyledNodeState) -> f32 {
317
41160
    let root_id = NodeId::new(0);
318
41160
    let root_state = &styled_dom.styled_nodes.as_container()[root_id].styled_node_state;
319
41160
    get_element_font_size(styled_dom, root_id, root_state)
320
41160
}
321

            
322
/// A value that can be Auto, Initial, Inherit, or an explicit value.
323
/// This preserves CSS cascade semantics better than Option<T>.
324
#[derive(Debug, Copy, Clone, PartialEq)]
325
pub enum MultiValue<T> {
326
    /// CSS 'auto' keyword
327
    Auto,
328
    /// CSS 'initial' keyword - use initial value
329
    Initial,
330
    /// CSS 'inherit' keyword - inherit from parent
331
    Inherit,
332
    /// Explicit value (e.g., "10px", "50%")
333
    Exact(T),
334
}
335

            
336
impl<T> MultiValue<T> {
337
    /// Returns true if this is an Auto value
338
59360
    pub fn is_auto(&self) -> bool {
339
59360
        matches!(self, MultiValue::Auto)
340
59360
    }
341

            
342
    /// Returns true if this is an explicit value
343
    pub fn is_exact(&self) -> bool {
344
        matches!(self, MultiValue::Exact(_))
345
    }
346

            
347
    /// Gets the exact value if present
348
60224
    pub fn exact(self) -> Option<T> {
349
60224
        match self {
350
59351
            MultiValue::Exact(v) => Some(v),
351
873
            _ => None,
352
        }
353
60224
    }
354

            
355
    /// Gets the exact value or returns the provided default
356
361015
    pub fn unwrap_or(self, default: T) -> T {
357
361015
        match self {
358
361015
            MultiValue::Exact(v) => v,
359
            _ => default,
360
        }
361
361015
    }
362

            
363
    /// Gets the exact value or returns T::default()
364
863069
    pub fn unwrap_or_default(self) -> T
365
863069
    where
366
863069
        T: Default,
367
    {
368
863069
        match self {
369
774504
            MultiValue::Exact(v) => v,
370
88565
            _ => T::default(),
371
        }
372
863069
    }
373

            
374
    /// Maps the inner value if Exact, otherwise returns self unchanged
375
    pub fn map<U, F>(self, f: F) -> MultiValue<U>
376
    where
377
        F: FnOnce(T) -> U,
378
    {
379
        match self {
380
            MultiValue::Exact(v) => MultiValue::Exact(f(v)),
381
            MultiValue::Auto => MultiValue::Auto,
382
            MultiValue::Initial => MultiValue::Initial,
383
            MultiValue::Inherit => MultiValue::Inherit,
384
        }
385
    }
386
}
387

            
388
// Implement helper methods for LayoutOverflow specifically
389
impl MultiValue<LayoutOverflow> {
390
    /// Returns true if this overflow value causes content to be clipped.
391
    /// This includes Hidden, Clip, Auto, and Scroll (all values except Visible).
392
54040
    pub fn is_clipped(&self) -> bool {
393
53410
        matches!(
394
54040
            self,
395
            MultiValue::Exact(
396
                LayoutOverflow::Hidden
397
                    | LayoutOverflow::Clip
398
                    | LayoutOverflow::Auto
399
                    | LayoutOverflow::Scroll
400
            )
401
        )
402
54040
    }
403

            
404
75320
    pub fn is_scroll(&self) -> bool {
405
75320
        matches!(
406
75320
            self,
407
            MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
408
        )
409
75320
    }
410

            
411
210
    pub fn is_auto_overflow(&self) -> bool {
412
210
        matches!(self, MultiValue::Exact(LayoutOverflow::Auto))
413
210
    }
414

            
415
    pub fn is_hidden(&self) -> bool {
416
        matches!(self, MultiValue::Exact(LayoutOverflow::Hidden))
417
    }
418

            
419
    pub fn is_hidden_or_clip(&self) -> bool {
420
        matches!(
421
            self,
422
            MultiValue::Exact(LayoutOverflow::Hidden | LayoutOverflow::Clip)
423
        )
424
    }
425

            
426
    pub fn is_scroll_explicit(&self) -> bool {
427
        matches!(self, MultiValue::Exact(LayoutOverflow::Scroll))
428
    }
429

            
430
210
    pub fn is_clip(&self) -> bool {
431
210
        matches!(self, MultiValue::Exact(LayoutOverflow::Clip))
432
210
    }
433

            
434
18585
    pub fn is_visible_or_clip(&self) -> bool {
435
35
        matches!(
436
18585
            self,
437
            MultiValue::Exact(LayoutOverflow::Visible | LayoutOverflow::Clip)
438
        )
439
18585
    }
440

            
441
    // +spec:overflow:833078 - visible/clip compute to auto/hidden if other axis is scrollable
442
    /// Resolves the computed value per CSS Overflow 3 § 3.1:
443
    /// visible/clip values compute to auto/hidden (respectively)
444
    /// if the other axis is neither visible nor clip.
445
53620
    pub fn resolve_computed(&self, other_axis: &MultiValue<LayoutOverflow>) -> MultiValue<LayoutOverflow> {
446
53620
        match (self, other_axis) {
447
53620
            (MultiValue::Exact(val), MultiValue::Exact(other)) => {
448
53620
                MultiValue::Exact(val.resolve_computed(*other))
449
            }
450
            _ => *self,
451
        }
452
53620
    }
453
}
454

            
455
// Implement helper methods for LayoutPosition
456
impl MultiValue<LayoutPosition> {
457
9275
    pub fn is_absolute_or_fixed(&self) -> bool {
458
9240
        matches!(
459
9275
            self,
460
            MultiValue::Exact(LayoutPosition::Absolute | LayoutPosition::Fixed)
461
        )
462
9275
    }
463
}
464

            
465
// Implement helper methods for LayoutFloat
466
impl MultiValue<LayoutFloat> {
467
9240
    pub fn is_none(&self) -> bool {
468
875
        matches!(
469
9240
            self,
470
            MultiValue::Auto
471
                | MultiValue::Initial
472
                | MultiValue::Inherit
473
                | MultiValue::Exact(LayoutFloat::None)
474
        )
475
9240
    }
476
}
477

            
478
impl<T: Default> Default for MultiValue<T> {
479
    fn default() -> Self {
480
        MultiValue::Auto
481
    }
482
}
483

            
484
/// Helper macro to reduce boilerplate for simple CSS property getters
485
/// Returns the inner PixelValue wrapped in MultiValue
486
macro_rules! get_css_property_pixel {
487
    // Variant WITH compact cache fast path for i16-encoded resolved px properties
488
    ($fn_name:ident, $cache_method:ident, $ua_property:expr, compact_i16 = $compact_method:ident) => {
489
446040
        pub fn $fn_name(
490
446040
            styled_dom: &StyledDom,
491
446040
            node_id: NodeId,
492
446040
            node_state: &StyledNodeState,
493
446040
        ) -> MultiValue<PixelValue> {
494
            // FAST PATH: compact cache for normal state (O(1) array lookup)
495
446040
            if node_state.is_normal() {
496
446040
                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
497
446040
                    let raw = cc.$compact_method(node_id.index());
498
446040
                    if raw == azul_css::compact_cache::I16_AUTO {
499
2940
                        return MultiValue::Auto;
500
443100
                    }
501
443100
                    if raw == azul_css::compact_cache::I16_INITIAL {
502
                        return MultiValue::Initial;
503
443100
                    }
504
443100
                    if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
505
                        // Valid value: decode i16 ×10 → px
506
442925
                        return MultiValue::Exact(PixelValue::px(raw as f32 / 10.0));
507
175
                    }
508
                    // I16_SENTINEL or I16_INHERIT → fall through to slow path
509
                }
510
            }
511

            
512
175
            let node_data = &styled_dom.node_data.as_container()[node_id];
513

            
514
175
            let author_css = styled_dom
515
175
                .css_property_cache
516
175
                .ptr
517
175
                .$cache_method(node_data, &node_id, node_state);
518

            
519
175
            if let Some(ref val) = author_css {
520
175
                if val.is_auto() {
521
                    return MultiValue::Auto;
522
175
                }
523
175
                if let Some(exact) = val.get_property().copied() {
524
175
                    return MultiValue::Exact(exact.inner);
525
                }
526
            }
527

            
528
            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
529

            
530
            if let Some(ua_prop) = ua_css {
531
                if let Some(inner) = ua_prop.get_pixel_inner() {
532
                    return MultiValue::Exact(inner);
533
                }
534
            }
535

            
536
            MultiValue::Initial
537
446040
        }
538
    };
539
    // Variant WITHOUT compact cache (original behavior)
540
    ($fn_name:ident, $cache_method:ident, $ua_property:expr) => {
541
        pub fn $fn_name(
542
            styled_dom: &StyledDom,
543
            node_id: NodeId,
544
            node_state: &StyledNodeState,
545
        ) -> MultiValue<PixelValue> {
546
            let node_data = &styled_dom.node_data.as_container()[node_id];
547

            
548
            // 1. Check author CSS first (includes inline styles - highest priority)
549
            let author_css = styled_dom
550
                .css_property_cache
551
                .ptr
552
                .$cache_method(node_data, &node_id, node_state);
553

            
554
            // NOTE: Check for Auto FIRST — CssPropertyValue::Auto is a valid value
555
            // that should NOT fall through to UA CSS. Previously, get_property()
556
            // returned None for Auto, causing inline "margin: auto" to be ignored.
557
            if let Some(ref val) = author_css {
558
                if val.is_auto() {
559
                    return MultiValue::Auto;
560
                }
561
                if let Some(exact) = val.get_property().copied() {
562
                    return MultiValue::Exact(exact.inner);
563
                }
564
                // For Initial, Inherit, None, Revert, Unset - fall through to UA CSS
565
            }
566

            
567
            // 2. Check User Agent CSS (only if author CSS didn't set a value)
568
            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
569

            
570
            if let Some(ua_prop) = ua_css {
571
                if let Some(inner) = ua_prop.get_pixel_inner() {
572
                    return MultiValue::Exact(inner);
573
                }
574
            }
575

            
576
            // 3. Fallback to Initial (not set)
577
            // IMPORTANT: Use Initial, not Auto! In CSS, the initial value for 
578
            // margin is 0, not auto. Using Auto here caused margins to be treated
579
            // as "margin: auto" which blocks align-self: stretch in flexbox.
580
            MultiValue::Initial
581
        }
582
    };
583
}
584

            
585
/// Helper trait to extract PixelValue from any CssProperty variant
586
trait CssPropertyPixelInner {
587
    fn get_pixel_inner(&self) -> Option<PixelValue>;
588
}
589

            
590
impl CssPropertyPixelInner for azul_css::props::property::CssProperty {
591
    fn get_pixel_inner(&self) -> Option<PixelValue> {
592
        match self {
593
            CssProperty::Left(CssPropertyValue::Exact(v)) => Some(v.inner),
594
            CssProperty::Right(CssPropertyValue::Exact(v)) => Some(v.inner),
595
            CssProperty::Top(CssPropertyValue::Exact(v)) => Some(v.inner),
596
            CssProperty::Bottom(CssPropertyValue::Exact(v)) => Some(v.inner),
597
            CssProperty::MarginLeft(CssPropertyValue::Exact(v)) => Some(v.inner),
598
            CssProperty::MarginRight(CssPropertyValue::Exact(v)) => Some(v.inner),
599
            CssProperty::MarginTop(CssPropertyValue::Exact(v)) => Some(v.inner),
600
            CssProperty::MarginBottom(CssPropertyValue::Exact(v)) => Some(v.inner),
601
            CssProperty::PaddingLeft(CssPropertyValue::Exact(v)) => Some(v.inner),
602
            CssProperty::PaddingRight(CssPropertyValue::Exact(v)) => Some(v.inner),
603
            CssProperty::PaddingTop(CssPropertyValue::Exact(v)) => Some(v.inner),
604
            CssProperty::PaddingBottom(CssPropertyValue::Exact(v)) => Some(v.inner),
605
            _ => None,
606
        }
607
    }
608
}
609

            
610
/// Generic macro for CSS properties with UA CSS fallback - returns MultiValue<T>
611
macro_rules! get_css_property {
612
    // Variant WITH compact cache fast path (for enum properties in Tier 1)
613
    ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr, compact = $compact_method:ident) => {
614
1828575
        pub fn $fn_name(
615
1828575
            styled_dom: &StyledDom,
616
1828575
            node_id: NodeId,
617
1828575
            node_state: &StyledNodeState,
618
1828575
        ) -> MultiValue<$return_type> {
619
            // FAST PATH: compact cache for normal state (O(1) array + bitshift)
620
            // NOTE (M12.7): skipping this fast path does NOT fix get_display_type's
621
            // divergence — the slow path / the `match get_display_type(...)` on the
622
            // LayoutDisplay enum (a niche-discriminant) mis-lifts too. So this isn't the
623
            // cache (unlike the font-size fix); it's the deeper niche/enum decode. Kept.
624
1828575
            if node_state.is_normal() {
625
1828575
                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
626
1828575
                    return MultiValue::Exact(cc.$compact_method(node_id.index()));
627
                }
628
            }
629

            
630
            // SLOW PATH: full cascade resolution
631
            let node_data = &styled_dom.node_data.as_container()[node_id];
632

            
633
            // 1. Check author CSS first
634
            let author_css = styled_dom
635
                .css_property_cache
636
                .ptr
637
                .$cache_method(node_data, &node_id, node_state);
638

            
639
            if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
640
                return MultiValue::Exact(val);
641
            }
642

            
643
            // 2. Check User Agent CSS
644
            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
645

            
646
            if let Some(ua_prop) = ua_css {
647
                if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
648
                    return MultiValue::Exact(val);
649
                }
650
            }
651

            
652
            // 3. Fallback to Auto (not set)
653
            MultiValue::Auto
654
1828575
        }
655
    };
656
    // Variant WITH compact cache for u32-encoded dimension enums (LayoutWidth/LayoutHeight)
657
    // These types have Auto, Px(PixelValue), MinContent, MaxContent, Calc variants
658
    ($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) => {
659
143360
        pub fn $fn_name(
660
143360
            styled_dom: &StyledDom,
661
143360
            node_id: NodeId,
662
143360
            node_state: &StyledNodeState,
663
143360
        ) -> MultiValue<$return_type> {
664
            // FAST PATH: compact cache for normal state
665
143360
            if node_state.is_normal() {
666
143360
                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
667
143360
                    let raw = cc.$compact_raw_method(node_id.index());
668
143360
                    match raw {
669
113120
                        azul_css::compact_cache::U32_AUTO => return MultiValue::Auto,
670
                        azul_css::compact_cache::U32_INITIAL => return MultiValue::Initial,
671
                        azul_css::compact_cache::U32_NONE => return MultiValue::Auto,
672
                        azul_css::compact_cache::U32_MIN_CONTENT => return MultiValue::Exact($min_content_variant),
673
                        azul_css::compact_cache::U32_MAX_CONTENT => return MultiValue::Exact($max_content_variant),
674
                        azul_css::compact_cache::U32_SENTINEL | azul_css::compact_cache::U32_INHERIT => {
675
                            // fall through to slow path
676
                        }
677
                        _ => {
678
                            // Valid encoded pixel value
679
30240
                            if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
680
30240
                                return MultiValue::Exact($px_variant(pv));
681
                            }
682
                            // decode failed → slow path
683
                        }
684
                    }
685
                }
686
            }
687

            
688
            // SLOW PATH: full cascade resolution
689
            let node_data = &styled_dom.node_data.as_container()[node_id];
690

            
691
            let author_css = styled_dom
692
                .css_property_cache
693
                .ptr
694
                .$cache_method(node_data, &node_id, node_state);
695

            
696
            if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
697
                return MultiValue::Exact(val);
698
            }
699

            
700
            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
701

            
702
            if let Some(ua_prop) = ua_css {
703
                if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
704
                    return MultiValue::Exact(val);
705
                }
706
            }
707

            
708
            MultiValue::Auto
709
143360
        }
710
    };
711
    // Variant WITH compact cache for u32-encoded dimension structs (LayoutMinWidth etc.)
712
    // These types are struct { inner: PixelValue }
713
    ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr, compact_u32_struct = $compact_raw_method:ident) => {
714
260400
        pub fn $fn_name(
715
260400
            styled_dom: &StyledDom,
716
260400
            node_id: NodeId,
717
260400
            node_state: &StyledNodeState,
718
260400
        ) -> MultiValue<$return_type> {
719
            // FAST PATH: compact cache for normal state
720
260400
            if node_state.is_normal() {
721
260400
                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
722
260400
                    let raw = cc.$compact_raw_method(node_id.index());
723
260400
                    match raw {
724
259490
                        azul_css::compact_cache::U32_AUTO | azul_css::compact_cache::U32_NONE => return MultiValue::Auto,
725
                        azul_css::compact_cache::U32_INITIAL => return MultiValue::Initial,
726
                        azul_css::compact_cache::U32_SENTINEL | azul_css::compact_cache::U32_INHERIT => {
727
                            // fall through to slow path
728
                        }
729
                        _ => {
730
910
                            if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
731
910
                                return MultiValue::Exact(
732
910
                                    <$return_type as azul_css::props::PixelValueTaker>::from_pixel_value(pv)
733
910
                                );
734
                            }
735
                        }
736
                    }
737
                }
738
            }
739

            
740
            // SLOW PATH
741
            let node_data = &styled_dom.node_data.as_container()[node_id];
742

            
743
            let author_css = styled_dom
744
                .css_property_cache
745
                .ptr
746
                .$cache_method(node_data, &node_id, node_state);
747

            
748
            if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
749
                return MultiValue::Exact(val);
750
            }
751

            
752
            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
753

            
754
            if let Some(ua_prop) = ua_css {
755
                if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
756
                    return MultiValue::Exact(val);
757
                }
758
            }
759

            
760
            MultiValue::Auto
761
260400
        }
762
    };
763
    // Variant WITHOUT compact cache (original behavior)
764
    ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr) => {
765
        pub fn $fn_name(
766
            styled_dom: &StyledDom,
767
            node_id: NodeId,
768
            node_state: &StyledNodeState,
769
        ) -> MultiValue<$return_type> {
770
            let node_data = &styled_dom.node_data.as_container()[node_id];
771

            
772
            // 1. Check author CSS first
773
            let author_css = styled_dom
774
                .css_property_cache
775
                .ptr
776
                .$cache_method(node_data, &node_id, node_state);
777

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

            
782
            // 2. Check User Agent CSS
783
            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
784

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

            
791
            // 3. Fallback to Auto (not set)
792
            MultiValue::Auto
793
        }
794
    };
795
}
796

            
797
/// Helper trait to extract typed values from UA CSS properties
798
trait ExtractPropertyValue<T> {
799
    fn extract(&self) -> Option<T>;
800
}
801

            
802
fn extract_property_value<T>(prop: &azul_css::props::property::CssProperty) -> Option<T>
803
where
804
    azul_css::props::property::CssProperty: ExtractPropertyValue<T>,
805
{
806
    prop.extract()
807
}
808

            
809
// Implement extraction for all layout types
810

            
811
impl ExtractPropertyValue<LayoutWidth> for azul_css::props::property::CssProperty {
812
    fn extract(&self) -> Option<LayoutWidth> {
813
        match self {
814
            Self::Width(CssPropertyValue::Exact(v)) => Some(v.clone()),
815
            _ => None,
816
        }
817
    }
818
}
819

            
820
impl ExtractPropertyValue<LayoutHeight> for azul_css::props::property::CssProperty {
821
    fn extract(&self) -> Option<LayoutHeight> {
822
        match self {
823
            Self::Height(CssPropertyValue::Exact(v)) => Some(v.clone()),
824
            _ => None,
825
        }
826
    }
827
}
828

            
829
impl ExtractPropertyValue<LayoutMinWidth> for azul_css::props::property::CssProperty {
830
    fn extract(&self) -> Option<LayoutMinWidth> {
831
        match self {
832
            Self::MinWidth(CssPropertyValue::Exact(v)) => Some(*v),
833
            _ => None,
834
        }
835
    }
836
}
837

            
838
impl ExtractPropertyValue<LayoutMinHeight> for azul_css::props::property::CssProperty {
839
    fn extract(&self) -> Option<LayoutMinHeight> {
840
        match self {
841
            Self::MinHeight(CssPropertyValue::Exact(v)) => Some(*v),
842
            _ => None,
843
        }
844
    }
845
}
846

            
847
impl ExtractPropertyValue<LayoutMaxWidth> for azul_css::props::property::CssProperty {
848
    fn extract(&self) -> Option<LayoutMaxWidth> {
849
        match self {
850
            Self::MaxWidth(CssPropertyValue::Exact(v)) => Some(*v),
851
            _ => None,
852
        }
853
    }
854
}
855

            
856
impl ExtractPropertyValue<LayoutMaxHeight> for azul_css::props::property::CssProperty {
857
    fn extract(&self) -> Option<LayoutMaxHeight> {
858
        match self {
859
            Self::MaxHeight(CssPropertyValue::Exact(v)) => Some(*v),
860
            _ => None,
861
        }
862
    }
863
}
864

            
865
impl ExtractPropertyValue<LayoutDisplay> for azul_css::props::property::CssProperty {
866
    fn extract(&self) -> Option<LayoutDisplay> {
867
        match self {
868
            Self::Display(CssPropertyValue::Exact(v)) => Some(*v),
869
            _ => None,
870
        }
871
    }
872
}
873

            
874
impl ExtractPropertyValue<LayoutWritingMode> for azul_css::props::property::CssProperty {
875
    fn extract(&self) -> Option<LayoutWritingMode> {
876
        match self {
877
            Self::WritingMode(CssPropertyValue::Exact(v)) => Some(*v),
878
            _ => None,
879
        }
880
    }
881
}
882

            
883
impl ExtractPropertyValue<LayoutFlexWrap> for azul_css::props::property::CssProperty {
884
    fn extract(&self) -> Option<LayoutFlexWrap> {
885
        match self {
886
            Self::FlexWrap(CssPropertyValue::Exact(v)) => Some(*v),
887
            _ => None,
888
        }
889
    }
890
}
891

            
892
impl ExtractPropertyValue<LayoutJustifyContent> for azul_css::props::property::CssProperty {
893
    fn extract(&self) -> Option<LayoutJustifyContent> {
894
        match self {
895
            Self::JustifyContent(CssPropertyValue::Exact(v)) => Some(*v),
896
            _ => None,
897
        }
898
    }
899
}
900

            
901
impl ExtractPropertyValue<StyleTextAlign> for azul_css::props::property::CssProperty {
902
    fn extract(&self) -> Option<StyleTextAlign> {
903
        match self {
904
            Self::TextAlign(CssPropertyValue::Exact(v)) => Some(*v),
905
            _ => None,
906
        }
907
    }
908
}
909

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

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

            
928
impl ExtractPropertyValue<LayoutOverflow> for azul_css::props::property::CssProperty {
929
    fn extract(&self) -> Option<LayoutOverflow> {
930
        match self {
931
            Self::OverflowX(CssPropertyValue::Exact(v)) => Some(*v),
932
            Self::OverflowY(CssPropertyValue::Exact(v)) => Some(*v),
933
            Self::OverflowBlock(CssPropertyValue::Exact(v)) => Some(*v),
934
            Self::OverflowInline(CssPropertyValue::Exact(v)) => Some(*v),
935
            _ => None,
936
        }
937
    }
938
}
939

            
940
impl ExtractPropertyValue<LayoutPosition> for azul_css::props::property::CssProperty {
941
    fn extract(&self) -> Option<LayoutPosition> {
942
        match self {
943
            Self::Position(CssPropertyValue::Exact(v)) => Some(*v),
944
            _ => None,
945
        }
946
    }
947
}
948

            
949
impl ExtractPropertyValue<LayoutBoxSizing> for azul_css::props::property::CssProperty {
950
    fn extract(&self) -> Option<LayoutBoxSizing> {
951
        match self {
952
            Self::BoxSizing(CssPropertyValue::Exact(v)) => Some(*v),
953
            _ => None,
954
        }
955
    }
956
}
957

            
958
impl ExtractPropertyValue<PixelValue> for azul_css::props::property::CssProperty {
959
    fn extract(&self) -> Option<PixelValue> {
960
        self.get_pixel_inner()
961
    }
962
}
963

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

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

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

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

            
impl ExtractPropertyValue<StyleFontStyle> for azul_css::props::property::CssProperty {
    fn extract(&self) -> Option<StyleFontStyle> {
        match self {
            Self::FontStyle(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleVisibility> for azul_css::props::property::CssProperty {
    fn extract(&self) -> Option<StyleVisibility> {
        match self {
            Self::Visibility(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleWhiteSpace> for azul_css::props::property::CssProperty {
    fn extract(&self) -> Option<StyleWhiteSpace> {
        match self {
            Self::WhiteSpace(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleDirection> for azul_css::props::property::CssProperty {
    fn extract(&self) -> Option<StyleDirection> {
        match self {
            Self::Direction(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleUnicodeBidi> for azul_css::props::property::CssProperty {
    fn extract(&self) -> Option<StyleUnicodeBidi> {
        match self {
            Self::UnicodeBidi(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleTextBoxTrim> for azul_css::props::property::CssProperty {
    fn extract(&self) -> Option<StyleTextBoxTrim> {
        match self {
            Self::TextBoxTrim(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleTextBoxEdge> for azul_css::props::property::CssProperty {
    fn extract(&self) -> Option<StyleTextBoxEdge> {
        match self {
            Self::TextBoxEdge(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleDominantBaseline> for azul_css::props::property::CssProperty {
    fn extract(&self) -> Option<StyleDominantBaseline> {
        match self {
            Self::DominantBaseline(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleAlignmentBaseline> for azul_css::props::property::CssProperty {
    fn extract(&self) -> Option<StyleAlignmentBaseline> {
        match self {
            Self::AlignmentBaseline(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleInitialLetterAlign> for azul_css::props::property::CssProperty {
    fn extract(&self) -> Option<StyleInitialLetterAlign> {
        match self {
            Self::InitialLetterAlign(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleInitialLetterWrap> for azul_css::props::property::CssProperty {
    fn extract(&self) -> Option<StyleInitialLetterWrap> {
        match self {
            Self::InitialLetterWrap(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleScrollbarGutter> for azul_css::props::property::CssProperty {
    fn extract(&self) -> Option<StyleScrollbarGutter> {
        match self {
            Self::ScrollbarGutter(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleOverflowClipMargin> for azul_css::props::property::CssProperty {
    fn extract(&self) -> Option<StyleOverflowClipMargin> {
        match self {
            Self::OverflowClipMargin(CssPropertyValue::Exact(v)) => Some(*v),
            _ => None,
        }
    }
}
impl ExtractPropertyValue<StyleVerticalAlign> for azul_css::props::property::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,
    azul_css::props::property::CssPropertyType::WritingMode,
    compact = get_writing_mode
);
get_css_property!(
    get_css_width,
    get_width,
    LayoutWidth,
    azul_css::props::property::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,
    azul_css::props::property::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,
    azul_css::props::property::CssPropertyType::FlexWrap,
    compact = get_flex_wrap
);
get_css_property!(
    get_justify_content,
    get_justify_content,
    LayoutJustifyContent,
    azul_css::props::property::CssPropertyType::JustifyContent,
    compact = get_justify_content
);
get_css_property!(
    get_text_align,
    get_text_align,
    StyleTextAlign,
    azul_css::props::property::CssPropertyType::TextAlign,
    compact = get_text_align
);
get_css_property!(
    get_float,
    get_float,
    LayoutFloat,
    azul_css::props::property::CssPropertyType::Float,
    compact = get_float
);
get_css_property!(
    get_clear,
    get_clear,
    LayoutClear,
    azul_css::props::property::CssPropertyType::Clear,
    compact = get_clear
);
get_css_property!(
    get_overflow_x,
    get_overflow_x,
    LayoutOverflow,
    azul_css::props::property::CssPropertyType::OverflowX,
    compact = get_overflow_x
);
get_css_property!(
    get_overflow_y,
    get_overflow_y,
    LayoutOverflow,
    azul_css::props::property::CssPropertyType::OverflowY,
    compact = get_overflow_y
);
// +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,
    azul_css::props::property::CssPropertyType::OverflowBlock
);
get_css_property!(
    get_overflow_inline,
    get_overflow_inline,
    LayoutOverflow,
    azul_css::props::property::CssPropertyType::OverflowInline
);
get_css_property!(
    get_position,
    get_position,
    LayoutPosition,
    azul_css::props::property::CssPropertyType::Position,
    compact = get_position
);
get_css_property!(
    get_css_box_sizing,
    get_box_sizing,
    LayoutBoxSizing,
    azul_css::props::property::CssPropertyType::BoxSizing,
    compact = get_box_sizing
);
get_css_property!(
    get_flex_direction,
    get_flex_direction,
    LayoutFlexDirection,
    azul_css::props::property::CssPropertyType::FlexDirection,
    compact = get_flex_direction
);
get_css_property!(
    get_align_items,
    get_align_items,
    LayoutAlignItems,
    azul_css::props::property::CssPropertyType::AlignItems,
    compact = get_align_items
);
get_css_property!(
    get_align_content,
    get_align_content,
    LayoutAlignContent,
    azul_css::props::property::CssPropertyType::AlignContent,
    compact = get_align_content
);
get_css_property!(
    get_font_weight_property,
    get_font_weight,
    StyleFontWeight,
    azul_css::props::property::CssPropertyType::FontWeight,
    compact = get_font_weight
);
get_css_property!(
    get_font_style_property,
    get_font_style,
    StyleFontStyle,
    azul_css::props::property::CssPropertyType::FontStyle,
    compact = get_font_style
);
get_css_property!(
    get_visibility,
    get_visibility,
    StyleVisibility,
    azul_css::props::property::CssPropertyType::Visibility,
    compact = get_visibility
);
get_css_property!(
    get_white_space_property,
    get_white_space,
    StyleWhiteSpace,
    azul_css::props::property::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,
    azul_css::props::property::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,
    azul_css::props::property::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,
    azul_css::props::property::CssPropertyType::TextBoxTrim
);
get_css_property!(
    get_text_box_edge_property,
    get_text_box_edge,
    StyleTextBoxEdge,
    azul_css::props::property::CssPropertyType::TextBoxEdge
);
get_css_property!(
    get_dominant_baseline_property,
    get_dominant_baseline,
    StyleDominantBaseline,
    azul_css::props::property::CssPropertyType::DominantBaseline
);
get_css_property!(
    get_alignment_baseline_property,
    get_alignment_baseline,
    StyleAlignmentBaseline,
    azul_css::props::property::CssPropertyType::AlignmentBaseline
);
get_css_property!(
    get_initial_letter_align_property,
    get_initial_letter_align,
    StyleInitialLetterAlign,
    azul_css::props::property::CssPropertyType::InitialLetterAlign
);
get_css_property!(
    get_initial_letter_wrap_property,
    get_initial_letter_wrap,
    StyleInitialLetterWrap,
    azul_css::props::property::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.
47635
pub fn get_scrollbar_gutter_property(
47635
    styled_dom: &StyledDom,
47635
    node_id: NodeId,
47635
    node_state: &StyledNodeState,
47635
) -> MultiValue<StyleScrollbarGutter> {
    // FAST PATH: 2-bit enum in hot_flags
47635
    if node_state.is_normal() {
47635
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
47635
            let bits = cc.get_scrollbar_gutter_bits(node_id.index());
47635
            let val = match bits {
47635
                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,
            };
47635
            return MultiValue::Exact(val);
        }
    }
    // SLOW PATH: cascade resolution for pseudo-states or missing cache
    let node_data = &styled_dom.node_data.as_container()[node_id];
    let author_css = styled_dom
        .css_property_cache
        .ptr
        .get_scrollbar_gutter(node_data, &node_id, node_state);
    if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
        return MultiValue::Exact(val);
    }
    MultiValue::Auto
47635
}
get_css_property!(
    get_overflow_clip_margin_property,
    get_overflow_clip_margin,
    StyleOverflowClipMargin,
    azul_css::props::property::CssPropertyType::OverflowClipMargin
);
get_css_property!(
    get_object_fit_property,
    get_object_fit,
    StyleObjectFit,
    azul_css::props::property::CssPropertyType::ObjectFit
);
// +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).
60865
pub fn get_text_orientation_property(
60865
    styled_dom: &StyledDom,
60865
    node_id: NodeId,
60865
    node_state: &StyledNodeState,
60865
) -> MultiValue<StyleTextOrientation> {
60865
    if node_state.is_normal() {
60865
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
60865
            if !cc.has_text_orientation(node_id.index()) {
60865
                return MultiValue::Auto;
            }
        }
    }
    let node_data = &styled_dom.node_data.as_container()[node_id];
    if let Some(val) = styled_dom
        .css_property_cache
        .ptr
        .get_text_orientation(node_data, &node_id, node_state)
        .and_then(|v| v.get_property().cloned())
    {
        return MultiValue::Exact(val);
    }
    let ua = azul_core::ua_css::get_ua_property(
        &node_data.node_type,
        azul_css::props::property::CssPropertyType::TextOrientation,
    );
    if let Some(ua_prop) = ua {
        if let Some(val) = extract_property_value::<StyleTextOrientation>(ua_prop) {
            return MultiValue::Exact(val);
        }
    }
    MultiValue::Auto
60865
}
get_css_property!(
    get_object_position_property,
    get_object_position,
    StyleObjectPosition,
    azul_css::props::property::CssPropertyType::ObjectPosition
);
get_css_property!(
    get_aspect_ratio_property,
    get_aspect_ratio,
    StyleAspectRatio,
    azul_css::props::property::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.
7140
pub fn get_vertical_align_property(
7140
    styled_dom: &StyledDom,
7140
    node_id: NodeId,
7140
    node_state: &StyledNodeState,
7140
) -> MultiValue<StyleVerticalAlign> {
7140
    let node_data = &styled_dom.node_data.as_container()[node_id];
7140
    let author_css = styled_dom
7140
        .css_property_cache
7140
        .ptr
7140
        .get_vertical_align(node_data, &node_id, node_state);
7140
    if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
1890
        return MultiValue::Exact(val);
5250
    }
5250
    let ua_css = azul_core::ua_css::get_ua_property(
5250
        &node_data.node_type,
5250
        azul_css::props::property::CssPropertyType::VerticalAlign,
    );
5250
    if let Some(ua_prop) = ua_css {
        if let Some(val) = extract_property_value::<StyleVerticalAlign>(ua_prop) {
            return MultiValue::Exact(val);
        }
5250
    }
5250
    MultiValue::Auto
7140
}
// Complex Property Getters
/// Get border radius for all four corners (raw CSS property values)
15470
pub fn get_style_border_radius(
15470
    styled_dom: &StyledDom,
15470
    node_id: NodeId,
15470
    node_state: &StyledNodeState,
15470
) -> azul_css::props::style::border_radius::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.
15470
    if node_state.is_normal() {
15470
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
15470
            let idx = node_id.index();
61880
            let decode = |raw: i16| -> PixelValue {
61880
                if raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
61880
                    PixelValue::px(0.0)
                } else {
                    PixelValue::px(raw as f32 / 10.0)
                }
61880
            };
15470
            return StyleBorderRadius {
15470
                top_left: decode(cc.get_border_top_left_radius_raw(idx)),
15470
                top_right: decode(cc.get_border_top_right_radius_raw(idx)),
15470
                bottom_right: decode(cc.get_border_bottom_right_radius_raw(idx)),
15470
                bottom_left: decode(cc.get_border_bottom_left_radius_raw(idx)),
15470
            };
        }
    }
    let node_data = &styled_dom.node_data.as_container()[node_id];
    let top_left = styled_dom
        .css_property_cache
        .ptr
        .get_border_top_left_radius(node_data, &node_id, node_state)
        .and_then(|br| br.get_property_or_default())
        .map(|v| v.inner)
        .unwrap_or_default();
    let top_right = styled_dom
        .css_property_cache
        .ptr
        .get_border_top_right_radius(node_data, &node_id, node_state)
        .and_then(|br| br.get_property_or_default())
        .map(|v| v.inner)
        .unwrap_or_default();
    let bottom_right = styled_dom
        .css_property_cache
        .ptr
        .get_border_bottom_right_radius(node_data, &node_id, node_state)
        .and_then(|br| br.get_property_or_default())
        .map(|v| v.inner)
        .unwrap_or_default();
    let bottom_left = styled_dom
        .css_property_cache
        .ptr
        .get_border_bottom_left_radius(node_data, &node_id, node_state)
        .and_then(|br| br.get_property_or_default())
        .map(|v| v.inner)
        .unwrap_or_default();
    StyleBorderRadius {
        top_left,
        top_right,
        bottom_right,
        bottom_left,
    }
15470
}
/// 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.
70350
pub fn get_border_radius(
70350
    styled_dom: &StyledDom,
70350
    node_id: NodeId,
70350
    node_state: &StyledNodeState,
70350
    element_size: PhysicalSizeImport,
70350
    viewport_size: LogicalSize,
70350
) -> 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.
70350
    if node_state.is_normal() {
70350
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
70350
            let idx = node_id.index();
70350
            let tl = cc.get_border_top_left_radius_raw(idx);
70350
            let tr = cc.get_border_top_right_radius_raw(idx);
70350
            let br = cc.get_border_bottom_right_radius_raw(idx);
70350
            let bl = cc.get_border_bottom_left_radius_raw(idx);
            // sentinel = "unset" = 0 px (no corner radius)
70350
            let thresh = azul_css::compact_cache::I16_SENTINEL_THRESHOLD;
281400
            let decode = |raw: i16| -> f32 {
281400
                if raw >= thresh { 0.0 } else { raw as f32 / 10.0 }
281400
            };
70350
            return BorderRadius {
70350
                top_left: decode(tl),
70350
                top_right: decode(tr),
70350
                bottom_right: decode(br),
70350
                bottom_left: decode(bl),
70350
            };
        }
    }
    let node_data = &styled_dom.node_data.as_container()[node_id];
    // Get font sizes for em/rem resolution
    let element_font_size = get_element_font_size(styled_dom, node_id, node_state);
    let parent_font_size = styled_dom
        .node_hierarchy
        .as_container()
        .get(node_id)
        .and_then(|node| node.parent_id())
        .map(|p| get_element_font_size(styled_dom, p, node_state))
        .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE);
    let root_font_size = get_root_font_size(styled_dom, node_state);
    // Create resolution context
    let context = ResolutionContext {
        element_font_size,
        parent_font_size,
        root_font_size,
        containing_block_size: PhysicalSize::new(0.0, 0.0), // Not used for border-radius
        element_size: Some(PhysicalSize::new(element_size.width, element_size.height)),
        viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
    };
    let top_left = styled_dom
        .css_property_cache
        .ptr
        .get_border_top_left_radius(node_data, &node_id, node_state)
        .and_then(|br| br.get_property().cloned())
        .unwrap_or_default();
    let top_right = styled_dom
        .css_property_cache
        .ptr
        .get_border_top_right_radius(node_data, &node_id, node_state)
        .and_then(|br| br.get_property().cloned())
        .unwrap_or_default();
    let bottom_right = styled_dom
        .css_property_cache
        .ptr
        .get_border_bottom_right_radius(node_data, &node_id, node_state)
        .and_then(|br| br.get_property().cloned())
        .unwrap_or_default();
    let bottom_left = styled_dom
        .css_property_cache
        .ptr
        .get_border_bottom_left_radius(node_data, &node_id, node_state)
        .and_then(|br| br.get_property().cloned())
        .unwrap_or_default();
    BorderRadius {
        top_left: top_left
            .inner
            .resolve_with_context(&context, PropertyContext::BorderRadius),
        top_right: top_right
            .inner
            .resolve_with_context(&context, PropertyContext::BorderRadius),
        bottom_right: bottom_right
            .inner
            .resolve_with_context(&context, PropertyContext::BorderRadius),
        bottom_left: bottom_left
            .inner
            .resolve_with_context(&context, PropertyContext::BorderRadius),
    }
70350
}
// +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
5180
pub fn get_z_index(styled_dom: &StyledDom, node_id: Option<NodeId>) -> i32 {
    use azul_css::props::layout::position::LayoutZIndex;
5180
    let node_id = match node_id {
5180
        Some(id) => id,
        None => return 0,
    };
5180
    let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
    // FAST PATH: compact cache for normal state
5180
    if node_state.is_normal() {
5180
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5180
            let raw = cc.get_z_index(node_id.index());
5180
            if raw == azul_css::compact_cache::I16_AUTO {
5180
                return 0;
            }
            if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
                return raw as i32;
            }
            // I16_SENTINEL → fall through to slow path
        }
    }
    // SLOW PATH
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom
        .css_property_cache
        .ptr
        .get_z_index(node_data, &node_id, &node_state)
        .and_then(|v| v.get_property())
        .map(|z| match z {
            LayoutZIndex::Auto => 0,
            LayoutZIndex::Integer(i) => *i,
        })
        .unwrap_or(0)
5180
}
// +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.
43470
pub fn is_z_index_auto(styled_dom: &StyledDom, node_id: Option<NodeId>) -> bool {
    use azul_css::props::layout::position::LayoutZIndex;
43470
    let node_id = match node_id {
43470
        Some(id) => id,
        None => return true,
    };
43470
    let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
    // FAST PATH: compact cache for normal state
43470
    if node_state.is_normal() {
43470
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
43470
            let raw = cc.get_z_index(node_id.index());
43470
            if raw == azul_css::compact_cache::I16_AUTO {
43470
                return true;
            }
            if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
                return false; // explicit integer
            }
            // I16_SENTINEL → fall through to slow path
        }
    }
    // SLOW PATH
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom
        .css_property_cache
        .ptr
        .get_z_index(node_data, &node_id, &node_state)
        .and_then(|v| v.get_property())
        .map(|z| matches!(z, LayoutZIndex::Auto))
        .unwrap_or(true) // no value = auto
43470
}
// 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.
43400
pub fn get_background_color(
43400
    styled_dom: &StyledDom,
43400
    node_id: NodeId,
43400
    node_state: &StyledNodeState,
43400
) -> ColorU {
43400
    let node_data = &styled_dom.node_data.as_container()[node_id];
43400
    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.
45290
    let get_node_bg = |nid: NodeId, ndata: &azul_core::dom::NodeData, state: &StyledNodeState| {
45290
        if state.is_normal() {
45290
            if let Some(ref cc) = cache.compact_cache {
45290
                if !cc.has_background(nid.index()) {
43960
                    return None;
1330
                }
            }
        }
1330
        cache
1330
            .get_background_content(ndata, &nid, state)
1330
            .and_then(|bg| bg.get_property())
1330
            .and_then(|bg_vec| bg_vec.get(0).cloned())
1330
            .and_then(|first_bg| match &first_bg {
1295
                azul_css::props::style::StyleBackgroundContent::Color(color) => Some(color.clone()),
                azul_css::props::style::StyleBackgroundContent::Image(_) => None, // Has image, not transparent
35
                _ => None,
1330
            })
45290
    };
43400
    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)
43400
    if !matches!(node_data.node_type, NodeType::Html) || own_bg.is_some() {
        // Not Html or has its own background - return own background or transparent
41510
        return own_bg.unwrap_or(ColorU {
41510
            r: 0,
41510
            g: 0,
41510
            b: 0,
41510
            a: 0,
41510
        });
1890
    }
    // Html node with transparent background - check if we should propagate from <body>
1890
    let first_child = styled_dom
1890
        .node_hierarchy
1890
        .as_container()
1890
        .get(node_id)
1890
        .and_then(|node| node.first_child_id(node_id));
1890
    let Some(first_child) = first_child else {
        return ColorU {
            r: 0,
            g: 0,
            b: 0,
            a: 0,
        };
    };
1890
    let first_child_data = &styled_dom.node_data.as_container()[first_child];
    // Check if first child is <body>
1890
    if !matches!(first_child_data.node_type, NodeType::Body) {
        return ColorU {
            r: 0,
            g: 0,
            b: 0,
            a: 0,
        };
1890
    }
    // Propagate <body>'s background to <html> (canvas)
1890
    let first_child_state = &styled_dom.styled_nodes.as_container()[first_child].styled_node_state;
1890
    get_node_bg(first_child, first_child_data, first_child_state).unwrap_or(ColorU {
1890
        r: 0,
1890
        g: 0,
1890
        b: 0,
1890
        a: 0,
1890
    })
43400
}
/// 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.
48440
pub fn get_background_contents(
48440
    styled_dom: &StyledDom,
48440
    node_id: NodeId,
48440
    node_state: &StyledNodeState,
48440
) -> Vec<azul_css::props::style::StyleBackgroundContent> {
    use azul_core::dom::NodeType;
    use azul_css::props::style::StyleBackgroundContent;
48440
    let node_data = &styled_dom.node_data.as_container()[node_id];
48440
    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.
48440
    let get_node_backgrounds =
        |nid: NodeId, ndata: &azul_core::dom::NodeData, state: &StyledNodeState|
50330
        -> Vec<StyleBackgroundContent> {
50330
            if state.is_normal() {
50330
                if let Some(ref cc) = cache.compact_cache {
50330
                    if !cc.has_background(nid.index()) {
45360
                        return Vec::new();
4970
                    }
                }
            }
4970
            cache
4970
                .get_background_content(ndata, &nid, state)
4970
                .and_then(|bg| bg.get_property())
4970
                .map(|bg_vec| bg_vec.iter().cloned().collect())
4970
                .unwrap_or_default()
50330
        };
48440
    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
48440
    if !matches!(node_data.node_type, NodeType::Html) || !own_backgrounds.is_empty() {
46550
        return own_backgrounds;
1890
    }
    // Html node with no backgrounds - check if we should propagate from <body>
1890
    let first_child = styled_dom
1890
        .node_hierarchy
1890
        .as_container()
1890
        .get(node_id)
1890
        .and_then(|node| node.first_child_id(node_id));
1890
    let Some(first_child) = first_child else {
        return own_backgrounds;
    };
1890
    let first_child_data = &styled_dom.node_data.as_container()[first_child];
    // Check if first child is <body>
1890
    if !matches!(first_child_data.node_type, NodeType::Body) {
        return own_backgrounds;
1890
    }
    // Propagate <body>'s backgrounds to <html> (canvas)
1890
    let first_child_state = &styled_dom.styled_nodes.as_container()[first_child].styled_node_state;
1890
    get_node_backgrounds(first_child, first_child_data, first_child_state)
48440
}
/// Information about border rendering
pub struct BorderInfo {
    pub widths: crate::solver3::display_list::StyleBorderWidths,
    pub colors: crate::solver3::display_list::StyleBorderColors,
    pub styles: crate::solver3::display_list::StyleBorderStyles,
}
48440
pub fn get_border_info(
48440
    styled_dom: &StyledDom,
48440
    node_id: NodeId,
48440
    node_state: &StyledNodeState,
48440
) -> 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::{
        LayoutBorderTopWidth, LayoutBorderRightWidth,
        LayoutBorderBottomWidth, LayoutBorderLeftWidth,
    };
    use azul_css::props::style::border::{
        BorderStyle, StyleBorderTopColor, StyleBorderRightColor,
        StyleBorderBottomColor, StyleBorderLeftColor,
        StyleBorderTopStyle, StyleBorderRightStyle,
        StyleBorderBottomStyle, StyleBorderLeftStyle,
    };
    // FAST PATH: compact cache for normal state
48440
    if node_state.is_normal() {
48440
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
48440
            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.
193760
            let make_width_px = |raw: i16| -> Option<PixelValue> {
193760
                if raw == azul_css::compact_cache::I16_AUTO
193760
                    || raw == azul_css::compact_cache::I16_INITIAL
193760
                    || raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD
                {
                    None
                } else {
193760
                    Some(PixelValue::px(raw as f32 / 10.0))
                }
193760
            };
48440
            let widths = StyleBorderWidths {
48440
                top: make_width_px(cc.get_border_top_width_raw(idx))
48440
                    .map(|px| CssPropertyValue::Exact(LayoutBorderTopWidth { inner: px })),
48440
                right: make_width_px(cc.get_border_right_width_raw(idx))
48440
                    .map(|px| CssPropertyValue::Exact(LayoutBorderRightWidth { inner: px })),
48440
                bottom: make_width_px(cc.get_border_bottom_width_raw(idx))
48440
                    .map(|px| CssPropertyValue::Exact(LayoutBorderBottomWidth { inner: px })),
48440
                left: make_width_px(cc.get_border_left_width_raw(idx))
48440
                    .map(|px| CssPropertyValue::Exact(LayoutBorderLeftWidth { inner: px })),
            };
            // Border colors from compact cache
193760
            let make_color = |raw: u32| -> Option<ColorU> {
193760
                if raw == 0 { None } else {
3535
                    Some(ColorU {
3535
                        r: ((raw >> 24) & 0xFF) as u8,
3535
                        g: ((raw >> 16) & 0xFF) as u8,
3535
                        b: ((raw >> 8) & 0xFF) as u8,
3535
                        a: (raw & 0xFF) as u8,
3535
                    })
                }
193760
            };
48440
            let colors = StyleBorderColors {
48440
                top: make_color(cc.get_border_top_color_raw(idx))
48440
                    .map(|c| CssPropertyValue::Exact(StyleBorderTopColor { inner: c })),
48440
                right: make_color(cc.get_border_right_color_raw(idx))
48440
                    .map(|c| CssPropertyValue::Exact(StyleBorderRightColor { inner: c })),
48440
                bottom: make_color(cc.get_border_bottom_color_raw(idx))
48440
                    .map(|c| CssPropertyValue::Exact(StyleBorderBottomColor { inner: c })),
48440
                left: make_color(cc.get_border_left_color_raw(idx))
48440
                    .map(|c| CssPropertyValue::Exact(StyleBorderLeftColor { inner: c })),
            };
            // Border styles from compact cache
48440
            let styles = StyleBorderStyles {
48440
                top: Some(CssPropertyValue::Exact(StyleBorderTopStyle {
48440
                    inner: cc.get_border_top_style(idx),
48440
                })),
48440
                right: Some(CssPropertyValue::Exact(StyleBorderRightStyle {
48440
                    inner: cc.get_border_right_style(idx),
48440
                })),
48440
                bottom: Some(CssPropertyValue::Exact(StyleBorderBottomStyle {
48440
                    inner: cc.get_border_bottom_style(idx),
48440
                })),
48440
                left: Some(CssPropertyValue::Exact(StyleBorderLeftStyle {
48440
                    inner: cc.get_border_left_style(idx),
48440
                })),
48440
            };
48440
            return BorderInfo { widths, colors, styles };
        }
    }
    // SLOW PATH: full cascade
    let node_data = &styled_dom.node_data.as_container()[node_id];
    // Get all border widths
    let widths = StyleBorderWidths {
        top: styled_dom
            .css_property_cache
            .ptr
            .get_border_top_width(node_data, &node_id, node_state)
            .cloned(),
        right: styled_dom
            .css_property_cache
            .ptr
            .get_border_right_width(node_data, &node_id, node_state)
            .cloned(),
        bottom: styled_dom
            .css_property_cache
            .ptr
            .get_border_bottom_width(node_data, &node_id, node_state)
            .cloned(),
        left: styled_dom
            .css_property_cache
            .ptr
            .get_border_left_width(node_data, &node_id, node_state)
            .cloned(),
    };
    // Get all border colors
    let colors = StyleBorderColors {
        top: styled_dom
            .css_property_cache
            .ptr
            .get_border_top_color(node_data, &node_id, node_state)
            .cloned(),
        right: styled_dom
            .css_property_cache
            .ptr
            .get_border_right_color(node_data, &node_id, node_state)
            .cloned(),
        bottom: styled_dom
            .css_property_cache
            .ptr
            .get_border_bottom_color(node_data, &node_id, node_state)
            .cloned(),
        left: styled_dom
            .css_property_cache
            .ptr
            .get_border_left_color(node_data, &node_id, node_state)
            .cloned(),
    };
    // Get all border styles
    let styles = StyleBorderStyles {
        top: styled_dom
            .css_property_cache
            .ptr
            .get_border_top_style(node_data, &node_id, node_state)
            .cloned(),
        right: styled_dom
            .css_property_cache
            .ptr
            .get_border_right_style(node_data, &node_id, node_state)
            .cloned(),
        bottom: styled_dom
            .css_property_cache
            .ptr
            .get_border_bottom_style(node_data, &node_id, node_state)
            .cloned(),
        left: styled_dom
            .css_property_cache
            .ptr
            .get_border_left_style(node_data, &node_id, node_state)
            .cloned(),
    };
    BorderInfo {
        widths,
        colors,
        styles,
    }
48440
}
/// 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.
32970
pub fn get_inline_border_info(
32970
    styled_dom: &StyledDom,
32970
    node_id: NodeId,
32970
    node_state: &StyledNodeState,
32970
    border_info: &BorderInfo,
32970
) -> Option<crate::text3::cache::InlineBorderInfo> {
    use crate::text3::cache::InlineBorderInfo;
    // Helper to extract pixel value from border width
32970
    fn get_border_width_px(
32970
        width: &Option<
32970
            azul_css::css::CssPropertyValue<azul_css::props::style::border::LayoutBorderTopWidth>,
32970
        >,
32970
    ) -> f32 {
32970
        width
32970
            .as_ref()
32970
            .and_then(|v| v.get_property())
32970
            .map(|w| w.inner.number.get())
32970
            .unwrap_or(0.0)
32970
    }
32970
    fn get_border_width_px_right(
32970
        width: &Option<
32970
            azul_css::css::CssPropertyValue<azul_css::props::style::border::LayoutBorderRightWidth>,
32970
        >,
32970
    ) -> f32 {
32970
        width
32970
            .as_ref()
32970
            .and_then(|v| v.get_property())
32970
            .map(|w| w.inner.number.get())
32970
            .unwrap_or(0.0)
32970
    }
32970
    fn get_border_width_px_bottom(
32970
        width: &Option<
32970
            azul_css::css::CssPropertyValue<
32970
                azul_css::props::style::border::LayoutBorderBottomWidth,
32970
            >,
32970
        >,
32970
    ) -> f32 {
32970
        width
32970
            .as_ref()
32970
            .and_then(|v| v.get_property())
32970
            .map(|w| w.inner.number.get())
32970
            .unwrap_or(0.0)
32970
    }
32970
    fn get_border_width_px_left(
32970
        width: &Option<
32970
            azul_css::css::CssPropertyValue<azul_css::props::style::border::LayoutBorderLeftWidth>,
32970
        >,
32970
    ) -> f32 {
32970
        width
32970
            .as_ref()
32970
            .and_then(|v| v.get_property())
32970
            .map(|w| w.inner.number.get())
32970
            .unwrap_or(0.0)
32970
    }
    // Helper to extract color from border color
    fn get_border_color_top(
        color: &Option<
            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderTopColor>,
        >,
    ) -> ColorU {
        color
            .as_ref()
            .and_then(|v| v.get_property())
            .map(|c| c.inner)
            .unwrap_or(ColorU::BLACK)
    }
    fn get_border_color_right(
        color: &Option<
            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderRightColor>,
        >,
    ) -> ColorU {
        color
            .as_ref()
            .and_then(|v| v.get_property())
            .map(|c| c.inner)
            .unwrap_or(ColorU::BLACK)
    }
    fn get_border_color_bottom(
        color: &Option<
            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderBottomColor>,
        >,
    ) -> ColorU {
        color
            .as_ref()
            .and_then(|v| v.get_property())
            .map(|c| c.inner)
            .unwrap_or(ColorU::BLACK)
    }
    fn get_border_color_left(
        color: &Option<
            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderLeftColor>,
        >,
    ) -> ColorU {
        color
            .as_ref()
            .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)
    fn get_border_radius_px(
        styled_dom: &StyledDom,
        node_id: NodeId,
        node_state: &StyledNodeState,
    ) -> Option<f32> {
        let node_data = &styled_dom.node_data.as_container()[node_id];
        let top_left = styled_dom
            .css_property_cache
            .ptr
            .get_border_top_left_radius(node_data, &node_id, node_state)
            .and_then(|br| br.get_property().cloned())
            .map(|v| v.inner.number.get());
        let top_right = styled_dom
            .css_property_cache
            .ptr
            .get_border_top_right_radius(node_data, &node_id, node_state)
            .and_then(|br| br.get_property().cloned())
            .map(|v| v.inner.number.get());
        let bottom_left = styled_dom
            .css_property_cache
            .ptr
            .get_border_bottom_left_radius(node_data, &node_id, node_state)
            .and_then(|br| br.get_property().cloned())
            .map(|v| v.inner.number.get());
        let bottom_right = styled_dom
            .css_property_cache
            .ptr
            .get_border_bottom_right_radius(node_data, &node_id, node_state)
            .and_then(|br| br.get_property().cloned())
            .map(|v| v.inner.number.get());
        // If any radius is defined, use the maximum (for inline, uniform radius is most common)
        let radii: Vec<f32> = [top_left, top_right, bottom_left, bottom_right]
            .into_iter()
            .filter_map(|r| r)
            .collect();
        if radii.is_empty() {
            None
        } else {
            Some(radii.into_iter().fold(0.0f32, |a, b| a.max(b)))
        }
    }
32970
    let top = get_border_width_px(&border_info.widths.top);
32970
    let right = get_border_width_px_right(&border_info.widths.right);
32970
    let bottom = get_border_width_px_bottom(&border_info.widths.bottom);
32970
    let left = get_border_width_px_left(&border_info.widths.left);
    // Fetch padding values for inline elements
131880
    fn resolve_padding(mv: MultiValue<PixelValue>) -> f32 {
131880
        match mv {
131880
            MultiValue::Exact(pv) => {
131880
                super::calc::resolve_pixel_value(&pv, 0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
            }
            _ => 0.0,
        }
131880
    }
32970
    let p_top = resolve_padding(get_css_padding_top(styled_dom, node_id, node_state));
32970
    let p_right = resolve_padding(get_css_padding_right(styled_dom, node_id, node_state));
32970
    let p_bottom = resolve_padding(get_css_padding_bottom(styled_dom, node_id, node_state));
32970
    let p_left = resolve_padding(get_css_padding_left(styled_dom, node_id, node_state));
    // Only return Some if there's actually a border or padding
32970
    let has_border = top > 0.0 || right > 0.0 || bottom > 0.0 || left > 0.0;
32970
    let has_padding = p_top > 0.0 || p_right > 0.0 || p_bottom > 0.0 || p_left > 0.0;
32970
    if !has_border && !has_padding {
32970
        return None;
    }
    // CSS 2.2 §8.6: detect direction for visual-order border/padding rendering in bidi
    let is_rtl = matches!(
        get_direction_property(styled_dom, node_id, node_state),
        MultiValue::Exact(StyleDirection::Rtl)
    );
    Some(InlineBorderInfo {
        top,
        right,
        bottom,
        left,
        top_color: get_border_color_top(&border_info.colors.top),
        right_color: get_border_color_right(&border_info.colors.right),
        bottom_color: get_border_color_bottom(&border_info.colors.bottom),
        left_color: get_border_color_left(&border_info.colors.left),
        radius: get_border_radius_px(styled_dom, node_id, node_state),
        padding_top: p_top,
        padding_right: p_right,
        padding_bottom: p_bottom,
        padding_left: p_left,
        is_first_fragment: true,
        is_last_fragment: true,
        is_rtl,
    })
32970
}
// 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)
    pub text_color: Option<ColorU>,
    /// Border radius for selection rectangles
    pub radius: f32,
}
/// Get selection style for a node
pub fn get_selection_style(
    styled_dom: &StyledDom, 
    node_id: Option<NodeId>,
    system_style: Option<&std::sync::Arc<azul_css::system::SystemStyle>>,
) -> SelectionStyle {
    let Some(node_id) = node_id else {
        return SelectionStyle::default();
    };
    let node_data = &styled_dom.node_data.as_container()[node_id];
    let node_state = &StyledNodeState::default();
    // Try to get selection background from CSS, otherwise use system color, otherwise hard-coded default
    let default_bg = system_style
        .and_then(|ss| ss.colors.selection_background.as_option().copied())
        .unwrap_or(ColorU {
            r: 51,
            g: 153,
            b: 255, // Standard blue selection color
            a: 128, // Semi-transparent
        });
    let bg_color = styled_dom
        .css_property_cache
        .ptr
        .get_selection_background_color(node_data, &node_id, node_state)
        .and_then(|c| c.get_property().cloned())
        .map(|c| c.inner)
        .unwrap_or(default_bg);
    // Try to get selection text color from CSS, otherwise use system color
    let default_text = system_style
        .and_then(|ss| ss.colors.selection_text.as_option().copied());
    let text_color = styled_dom
        .css_property_cache
        .ptr
        .get_selection_color(node_data, &node_id, node_state)
        .and_then(|c| c.get_property().cloned())
        .map(|c| c.inner)
        .or(default_text);
    let radius = styled_dom
        .css_property_cache
        .ptr
        .get_selection_radius(node_data, &node_id, node_state)
        .and_then(|r| r.get_property().cloned())
        .map(|r| r.inner.to_pixels_internal(0.0, 16.0, 16.0)) // percent=0, em=16px default font size
        .unwrap_or(0.0);
    SelectionStyle {
        bg_color,
        text_color,
        radius,
    }
}
/// 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 in milliseconds (0 = no blink)
    pub animation_duration: u32,
}
impl Default for CaretStyle {
    fn default() -> Self {
        Self {
            color: ColorU::BLACK,
            width: 2.0,
            animation_duration: 500,
        }
    }
}
/// Get caret style for a node
1050
pub fn get_caret_style(styled_dom: &StyledDom, node_id: Option<NodeId>) -> CaretStyle {
1050
    let Some(node_id) = node_id else {
        return CaretStyle::default();
    };
1050
    let node_data = &styled_dom.node_data.as_container()[node_id];
1050
    let node_state = &StyledNodeState::default();
1050
    let color = styled_dom
1050
        .css_property_cache
1050
        .ptr
1050
        .get_caret_color(node_data, &node_id, node_state)
1050
        .and_then(|c| c.get_property().cloned())
1050
        .map(|c| c.inner)
1050
        .unwrap_or(ColorU::BLACK);
1050
    let width = styled_dom
1050
        .css_property_cache
1050
        .ptr
1050
        .get_caret_width(node_data, &node_id, node_state)
1050
        .and_then(|w| w.get_property().cloned())
1050
        .map(|w| w.inner.to_pixels_internal(0.0, 16.0, 16.0)) // 16.0 as default em size
1050
        .unwrap_or(2.0); // 2px width by default
1050
    let animation_duration = styled_dom
1050
        .css_property_cache
1050
        .ptr
1050
        .get_caret_animation_duration(node_data, &node_id, node_state)
1050
        .and_then(|d| d.get_property().cloned())
1050
        .map(|d| d.inner.inner) // Duration.inner is the u32 milliseconds value
1050
        .unwrap_or(500); // 500ms blink by default
1050
    CaretStyle {
1050
        color,
1050
        width,
1050
        animation_duration,
1050
    }
1050
}
// 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()` + `merge_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.
pub fn get_scrollbar_info_from_layout(node: &LayoutNode) -> ScrollbarRequirements {
    node.scrollbar_info
        .clone()
        .unwrap_or_default()
}
/// 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).
pub fn get_layout_scrollbar_width_px<T: crate::font_traits::ParsedFontTrait>(
    ctx: &crate::solver3::LayoutContext<'_, T>,
    dom_id: NodeId,
    styled_node_state: &StyledNodeState,
) -> 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
    let style = get_scrollbar_style(
        ctx.styled_dom,
        dom_id,
        styled_node_state,
        ctx.system_style.as_deref(),
    );
    style.reserve_width_px
}
get_css_property!(
    get_display_property_internal,
    get_display,
    LayoutDisplay,
    azul_css::props::property::CssPropertyType::Display,
    compact = get_display
);
525630
pub fn get_display_property(
525630
    styled_dom: &StyledDom,
525630
    dom_id: Option<NodeId>,
525630
) -> MultiValue<LayoutDisplay> {
525630
    let Some(id) = dom_id else {
35
        return MultiValue::Exact(LayoutDisplay::Inline);
    };
525595
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
525595
    get_display_property_internal(styled_dom, id, node_state)
525630
}
/// 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.
2975
pub fn blockify_display(raw_display: LayoutDisplay) -> LayoutDisplay {
2975
    match raw_display {
        // Inline-level display types become their block-level equivalents
        LayoutDisplay::Inline => LayoutDisplay::Block,
        // Per CSS Display 3 §2.7: inline-block blockifies to block
        // (for legacy reasons, loses its flow-root nature)
        LayoutDisplay::InlineBlock => LayoutDisplay::Block,
        LayoutDisplay::InlineFlex => LayoutDisplay::Flex,
        LayoutDisplay::InlineTable => LayoutDisplay::Table,
        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
        | LayoutDisplay::TableCaption => LayoutDisplay::Block,
        // Already block-level types are unchanged
2975
        other => other,
    }
2975
}
/// // +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
9450
pub fn get_computed_display(
9450
    raw_display: LayoutDisplay,
9450
    is_absolute_or_fixed: bool,
9450
    is_floated: bool,
9450
    is_root: bool,
9450
    is_flex_grid_child: bool,
9450
) -> LayoutDisplay {
9450
    if raw_display == LayoutDisplay::None {
        return LayoutDisplay::None;
9450
    }
    // +spec:positioning:69468c - absolute/fixed blockifies the box
9450
    if is_absolute_or_fixed || is_floated || is_root || is_flex_grid_child {
2975
        blockify_display(raw_display)
    } else {
6475
        raw_display
    }
9450
}
// +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
385
pub fn get_vertical_align_for_node(
385
    styled_dom: &StyledDom,
385
    dom_id: NodeId,
385
) -> crate::text3::cache::VerticalAlign {
385
    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
385
    let va = match get_vertical_align_property(styled_dom, dom_id, node_state) {
        MultiValue::Exact(v) => v,
385
        _ => StyleVerticalAlign::default(),
    };
385
    match va {
385
        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,
        StyleVerticalAlign::Sub => crate::text3::cache::VerticalAlign::Sub,
        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);
            let line_height = get_line_height_value(styled_dom, dom_id, node_state)
                .map(|lh| lh.inner.normalized() * font_size)
                .unwrap_or(font_size * 1.2);
            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);
            let px = super::calc::resolve_pixel_value(&l, 0.0, font_size, font_size);
            crate::text3::cache::VerticalAlign::Offset(px)
        }
    }
385
}
32970
pub fn get_style_properties(
32970
    styled_dom: &StyledDom,
32970
    dom_id: NodeId,
32970
    system_style: Option<&std::sync::Arc<azul_css::system::SystemStyle>>,
32970
    viewport_size: azul_css::props::basic::PhysicalSize,
32970
) -> StyleProperties {
    use azul_css::props::basic::{PhysicalSize, PropertyContext, ResolutionContext};
32970
    let node_data = &styled_dom.node_data.as_container()[dom_id];
32970
    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
32970
    let cache = &styled_dom.css_property_cache.ptr;
    use azul_css::props::basic::font::{StyleFontFamily, StyleFontFamilyVec};
    // 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.)
32970
    let font_families = if node_state.is_normal() {
32970
        cache.compact_cache.as_ref()
32970
            .and_then(|cc| {
32970
                let fh = cc.tier2b_text[dom_id.index()].font_family_hash;
32970
                if fh == 0 { return None; }
4725
                cc.font_hash_to_families.get(&fh).cloned()
32970
            })
32970
            .unwrap_or_else(|| {
28245
                StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
28245
            })
    } 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.
32970
    let parent_font_size = get_parent_font_size(styled_dom, dom_id, node_state);
32970
    let root_font_size = get_root_font_size(styled_dom, node_state);
    // Create resolution context for font-size (em refers to parent)
32970
    let font_size_context = ResolutionContext {
32970
        element_font_size: azul_css::props::basic::pixel::DEFAULT_FONT_SIZE, /* Not used for font-size property */
32970
        parent_font_size,
32970
        root_font_size,
32970
        containing_block_size: PhysicalSize::new(0.0, 0.0),
32970
        element_size: None,
32970
        viewport_size,
32970
    };
    // 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)
32970
    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).
32970
        let mut fast_font_size: Option<f32> = None;
32970
        let mut compact_said_inherit = false;
32970
        if node_state.is_normal() {
32970
            if let Some(ref cc) = cache.compact_cache {
32970
                let raw = cc.get_font_size_raw(dom_id.index());
32970
                if raw == azul_css::compact_cache::U32_SENTINEL
32970
                    || raw == azul_css::compact_cache::U32_INHERIT
32970
                    || raw == azul_css::compact_cache::U32_INITIAL
28105
                {
28105
                    compact_said_inherit = true;
28105
                } else if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
4865
                    fast_font_size = Some(pv.resolve_with_context(
4865
                        &font_size_context,
4865
                        PropertyContext::FontSize,
4865
                    ));
4865
                }
            }
        }
32970
        if let Some(fs) = fast_font_size {
4865
            fs
28105
        } else if compact_said_inherit {
28105
            parent_font_size
        } else {
            cache
                .get_font_size(node_data, &dom_id, node_state)
                .and_then(|v| v.get_property().cloned())
                .map(|v| {
                    v.inner
                        .resolve_with_context(&font_size_context, PropertyContext::FontSize)
                })
                .unwrap_or(parent_font_size)
        }
    };
32970
    let color_from_cache = {
        // FAST PATH: compact cache for text color
32970
        let mut fast_color = None;
32970
        if node_state.is_normal() {
32970
            if let Some(ref cc) = cache.compact_cache {
32970
                let raw = cc.get_text_color_raw(dom_id.index());
32970
                if raw != 0 {
105
                    // Decode 0xRRGGBBAA → ColorU
105
                    fast_color = Some(ColorU {
105
                        r: (raw >> 24) as u8,
105
                        g: (raw >> 16) as u8,
105
                        b: (raw >> 8) as u8,
105
                        a: raw as u8,
105
                    });
32865
                }
            }
        }
32970
        fast_color.or_else(|| {
32865
            cache
32865
                .get_text_color(node_data, &dom_id, node_state)
32865
                .and_then(|v| v.get_property().cloned())
32865
                .map(|v| v.inner)
32865
        })
    };
    // 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.
32970
    let color = color_from_cache.unwrap_or(ColorU::BLACK);
    // +spec:font-metrics:e480da - line-height: normal/number/length/percentage resolution
32970
    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.
32970
        let mut fast_lh = None;
32970
        let mut sentinel_normal = false;
32970
        if node_state.is_normal() {
32970
            if let Some(ref cc) = cache.compact_cache {
32970
                if let Some(normalized) = cc.get_line_height(dom_id.index()) {
                    fast_lh = Some(crate::text3::cache::LineHeight::Px(normalized / 100.0 * font_size));
32970
                } else {
32970
                    // Sentinel in compact cache = "normal" (CSS default).
32970
                    sentinel_normal = true;
32970
                }
            }
        }
32970
        if sentinel_normal {
32970
            crate::text3::cache::LineHeight::Normal
        } else {
            fast_lh.unwrap_or_else(|| {
                cache
                    .get_line_height(node_data, &dom_id, node_state)
                    .and_then(|v| v.get_property().cloned())
                    .map(|v| crate::text3::cache::LineHeight::Px(v.inner.normalized() * font_size))
                    .unwrap_or(crate::text3::cache::LineHeight::Normal)
            })
        }
    };
    // 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.
    use azul_css::props::layout::LayoutDisplay;
32970
    let display = match get_display_property(styled_dom, Some(dom_id)) {
32970
        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
32970
    let (background_color, background_content, border) =
32970
        if matches!(display, LayoutDisplay::Inline | LayoutDisplay::InlineBlock) {
32970
            let bg = get_background_color(styled_dom, dom_id, node_state);
32970
            let bg_color = if bg.a > 0 { Some(bg) } else { None };
            // Get full background contents (including gradients)
32970
            let bg_contents = get_background_contents(styled_dom, dom_id, node_state);
            // Get border info for inline elements
32970
            let border_info = get_border_info(styled_dom, dom_id, node_state);
32970
            let inline_border =
32970
                get_inline_border_info(styled_dom, dom_id, node_state, &border_info);
32970
            (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
            (None, Vec::new(), None)
        };
    // Query font-weight from CSS cache
32970
    let font_weight = match get_font_weight_property(styled_dom, dom_id, node_state) {
32970
        MultiValue::Exact(v) => v,
        _ => StyleFontWeight::Normal,
    };
    // Query font-style from CSS cache
32970
    let font_style = match get_font_style_property(styled_dom, dom_id, node_state) {
32970
        MultiValue::Exact(v) => v,
        _ => StyleFontStyle::Normal,
    };
    // Convert StyleFontWeight/StyleFontStyle to fontconfig types
32970
    let fc_weight = super::fc::convert_font_weight(font_weight);
32970
    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
32970
    let font_stack = {
        // Look for a Ref in the font families
32970
        let font_ref = (0..font_families.len())
32970
            .find_map(|i| {
32970
                match font_families.get(i).unwrap() {
                    azul_css::props::basic::font::StyleFontFamily::Ref(r) => Some(r.clone()),
32970
                    _ => None,
                }
32970
            });
        // Get platform for resolving system font types
32970
        let platform = system_style.map(|ss| &ss.platform);
32970
        if let Some(font_ref) = font_ref {
            // Use FontStack::Ref for embedded fonts
            FontStack::Ref(font_ref)
        } else {
            // Build regular font stack from all font families
32970
            let mut stack = Vec::with_capacity(font_families.len() + 3);
32970
            for i in 0..font_families.len() {
32970
                let family = font_families.get(i).unwrap();
                // Handle SystemFontType specially - resolve to actual OS font names
                // (e.g., "system:ui" → ["System Font", "Helvetica Neue", "Lucida Grande"] on macOS)
32970
                if let azul_css::props::basic::font::StyleFontFamily::SystemType(system_type) = family {
                    if let Some(platform) = platform {
                        let font_names = system_type.get_fallback_chain(platform);
                        let system_weight = if system_type.is_bold() {
                            rust_fontconfig::FcWeight::Bold
                        } else {
                            fc_weight
                        };
                        let system_style_val = if system_type.is_italic() {
                            crate::text3::cache::FontStyle::Italic
                        } else {
                            fc_style
                        };
                        for font_name in font_names {
                            stack.push(crate::text3::cache::FontSelector {
                                family: font_name.to_string(),
                                weight: system_weight,
                                style: system_style_val,
                                unicode_ranges: Vec::new(),
                            });
                        }
                    } else {
                        // No platform info - fall back to generic sans-serif
                        stack.push(crate::text3::cache::FontSelector {
                            family: "sans-serif".to_string(),
                            weight: fc_weight,
                            style: fc_style,
                            unicode_ranges: Vec::new(),
                        });
                    }
32970
                } else {
32970
                    stack.push(crate::text3::cache::FontSelector {
32970
                        family: family.as_string(),
32970
                        weight: fc_weight,
32970
                        style: fc_style,
32970
                        unicode_ranges: Vec::new(),
32970
                    });
32970
                }
            }
            // Add generic fallbacks (serif/sans-serif will be resolved based on Unicode ranges later)
32970
            let generic_fallbacks = ["sans-serif", "serif", "monospace"];
131880
            for fallback in &generic_fallbacks {
98910
                if !stack
98910
                    .iter()
131880
                    .any(|f| f.family.to_lowercase() == fallback.to_lowercase())
65940
                {
65940
                    stack.push(crate::text3::cache::FontSelector {
65940
                        family: fallback.to_string(),
65940
                        weight: rust_fontconfig::FcWeight::Normal,
65940
                        style: crate::text3::cache::FontStyle::Normal,
65940
                        unicode_ranges: Vec::new(),
65940
                    });
65940
                }
            }
32970
            FontStack::Stack(stack)
        }
    };
    // Get letter-spacing from CSS
32970
    let letter_spacing = {
        // FAST PATH: compact cache for letter-spacing (i16 resolved px × 10)
32970
        let mut fast_ls = None;
32970
        if node_state.is_normal() {
32970
            if let Some(ref cc) = cache.compact_cache {
32970
                if let Some(px_val) = cc.get_letter_spacing(dom_id.index()) {
32970
                    fast_ls = Some(crate::text3::cache::Spacing::Px(px_val.round() as i32));
32970
                }
            }
        }
32970
        fast_ls.unwrap_or_else(|| {
            cache
                .get_letter_spacing(node_data, &dom_id, node_state)
                .and_then(|v| v.get_property().cloned())
                .map(|v| {
                    let px_value = v.inner.resolve_with_context(&font_size_context, PropertyContext::FontSize);
                    crate::text3::cache::Spacing::Px(px_value.round() as i32)
                })
                .unwrap_or_default()
        })
    };
    // Get word-spacing from CSS
32970
    let word_spacing = {
        // FAST PATH: compact cache for word-spacing (i16 resolved px × 10)
32970
        let mut fast_ws = None;
32970
        if node_state.is_normal() {
32970
            if let Some(ref cc) = cache.compact_cache {
32970
                if let Some(px_val) = cc.get_word_spacing(dom_id.index()) {
32970
                    fast_ws = Some(crate::text3::cache::Spacing::Px(px_val.round() as i32));
32970
                }
            }
        }
32970
        fast_ws.unwrap_or_else(|| {
            cache
                .get_word_spacing(node_data, &dom_id, node_state)
                .and_then(|v| v.get_property().cloned())
                .map(|v| {
                    let px_value = v.inner.resolve_with_context(&font_size_context, PropertyContext::FontSize);
                    crate::text3::cache::Spacing::Px(px_value.round() as i32)
                })
                .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.
32970
    let text_decoration = {
32970
        let mut skip_walk = false;
32970
        if node_state.is_normal() {
32970
            if let Some(ref cc) = cache.compact_cache {
32970
                if !cc.has_text_decoration(dom_id.index()) {
24325
                    skip_walk = true;
24325
                }
            }
        }
32970
        if skip_walk {
24325
            crate::text3::cache::TextDecoration::default()
        } else {
8645
            cache
8645
                .get_text_decoration(node_data, &dom_id, node_state)
8645
                .and_then(|v| v.get_property().cloned())
8645
                .map(|v| crate::text3::cache::TextDecoration::from_css(v))
8645
                .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.
32970
    let tab_size = {
32970
        let mut fast_tab = None;
32970
        if node_state.is_normal() {
32970
            if let Some(ref cc) = cache.compact_cache {
32970
                let raw = cc.get_tab_size_raw(dom_id.index());
32970
                if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
                    fast_tab = Some(raw as f32 / 10.0);
32970
                } else {
32970
                    // Sentinel / Inherit / Initial → spec default is 8.
32970
                    fast_tab = Some(8.0);
32970
                }
            }
        }
32970
        fast_tab.unwrap_or_else(|| {
            cache
                .get_tab_size(node_data, &dom_id, node_state)
                .and_then(|v| v.get_property().cloned())
                .map(|v| v.inner.number.get())
                .unwrap_or(8.0)
        })
    };
32970
    let properties = StyleProperties {
32970
        font_stack,
32970
        font_size_px: font_size,
32970
        color,
32970
        background_color,
32970
        background_content,
32970
        border,
32970
        line_height,
32970
        letter_spacing,
32970
        word_spacing,
32970
        text_decoration,
32970
        tab_size,
32970
        // These still use defaults - could be extended in future:
32970
        // font_features, font_variations, text_transform, writing_mode, 
32970
        // text_orientation, text_combine_upright, font_variant_*
32970
        ..Default::default()
32970
    };
32970
    properties
32970
}
pub fn get_list_style_type(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> StyleListStyleType {
    let Some(id) = dom_id else {
        return StyleListStyleType::default();
    };
    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_list_style_type(node_data, &id, node_state)
        .and_then(|v| v.get_property().copied())
        .unwrap_or_default()
}
pub fn get_list_style_position(
    styled_dom: &StyledDom,
    dom_id: Option<NodeId>,
) -> StyleListStylePosition {
    let Some(id) = dom_id else {
        return StyleListStylePosition::default();
    };
    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_list_style_position(node_data, &id, node_state)
        .and_then(|v| v.get_property().copied())
        .unwrap_or_default()
}
// 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,
    azul_css::props::property::CssPropertyType::Left,
    compact_i16 = get_left
);
get_css_property_pixel!(
    get_css_right,
    get_right,
    azul_css::props::property::CssPropertyType::Right,
    compact_i16 = get_right
);
get_css_property_pixel!(
    get_css_top,
    get_top,
    azul_css::props::property::CssPropertyType::Top,
    compact_i16 = get_top
);
get_css_property_pixel!(
    get_css_bottom,
    get_bottom,
    azul_css::props::property::CssPropertyType::Bottom,
    compact_i16 = get_bottom
);
/// Get margin properties - returns MultiValue<PixelValue>
get_css_property_pixel!(
    get_css_margin_left,
    get_margin_left,
    azul_css::props::property::CssPropertyType::MarginLeft,
    compact_i16 = get_margin_left_raw
);
get_css_property_pixel!(
    get_css_margin_right,
    get_margin_right,
    azul_css::props::property::CssPropertyType::MarginRight,
    compact_i16 = get_margin_right_raw
);
get_css_property_pixel!(
    get_css_margin_top,
    get_margin_top,
    azul_css::props::property::CssPropertyType::MarginTop,
    compact_i16 = get_margin_top_raw
);
get_css_property_pixel!(
    get_css_margin_bottom,
    get_margin_bottom,
    azul_css::props::property::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,
    azul_css::props::property::CssPropertyType::PaddingLeft,
    compact_i16 = get_padding_left_raw
);
get_css_property_pixel!(
    get_css_padding_right,
    get_padding_right,
    azul_css::props::property::CssPropertyType::PaddingRight,
    compact_i16 = get_padding_right_raw
);
get_css_property_pixel!(
    get_css_padding_top,
    get_padding_top,
    azul_css::props::property::CssPropertyType::PaddingTop,
    compact_i16 = get_padding_top_raw
);
get_css_property_pixel!(
    get_css_padding_bottom,
    get_padding_bottom,
    azul_css::props::property::CssPropertyType::PaddingBottom,
    compact_i16 = get_padding_bottom_raw
);
/// Get min/max size properties
get_css_property!(
    get_css_min_width,
    get_min_width,
    LayoutMinWidth,
    azul_css::props::property::CssPropertyType::MinWidth,
    compact_u32_struct = get_min_width_raw
);
get_css_property!(
    get_css_min_height,
    get_min_height,
    LayoutMinHeight,
    azul_css::props::property::CssPropertyType::MinHeight,
    compact_u32_struct = get_min_height_raw
);
get_css_property!(
    get_css_max_width,
    get_max_width,
    LayoutMaxWidth,
    azul_css::props::property::CssPropertyType::MaxWidth,
    compact_u32_struct = get_max_width_raw
);
get_css_property!(
    get_css_max_height,
    get_max_height,
    LayoutMaxHeight,
    azul_css::props::property::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,
    azul_css::props::property::CssPropertyType::BorderLeftWidth,
    compact_i16 = get_border_left_width_raw
);
get_css_property_pixel!(
    get_css_border_right_width,
    get_border_right_width,
    azul_css::props::property::CssPropertyType::BorderRightWidth,
    compact_i16 = get_border_right_width_raw
);
get_css_property_pixel!(
    get_css_border_top_width,
    get_border_top_width,
    azul_css::props::property::CssPropertyType::BorderTopWidth,
    compact_i16 = get_border_top_width_raw
);
get_css_property_pixel!(
    get_css_border_bottom_width,
    get_border_bottom_width,
    azul_css::props::property::CssPropertyType::BorderBottomWidth,
    compact_i16 = get_border_bottom_width_raw
);
// Fragmentation (page breaking) properties
/// Get break-before property for paged media
26810
pub fn get_break_before(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> PageBreak {
26810
    let Some(id) = dom_id else {
        return PageBreak::Auto;
    };
26810
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
    // Negative fast path: break-* is almost never declared.
26810
    if node_state.is_normal() {
26810
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
26810
            if !cc.has_break(id.index()) {
26810
                return PageBreak::Auto;
            }
        }
    }
    let node_data = &styled_dom.node_data.as_container()[id];
    styled_dom
        .css_property_cache
        .ptr
        .get_break_before(node_data, &id, node_state)
        .and_then(|v| v.get_property().cloned())
        .unwrap_or(PageBreak::Auto)
26810
}
/// Get break-after property for paged media
26810
pub fn get_break_after(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> PageBreak {
26810
    let Some(id) = dom_id else {
        return PageBreak::Auto;
    };
26810
    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
26810
    if node_state.is_normal() {
26810
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
26810
            if !cc.has_break(id.index()) {
26810
                return PageBreak::Auto;
            }
        }
    }
    let node_data = &styled_dom.node_data.as_container()[id];
    styled_dom
        .css_property_cache
        .ptr
        .get_break_after(node_data, &id, node_state)
        .and_then(|v| v.get_property().cloned())
        .unwrap_or(PageBreak::Auto)
26810
}
/// Check if a PageBreak value forces a page break (always, page, left, right, etc.)
53620
pub fn is_forced_page_break(page_break: PageBreak) -> bool {
53620
    matches!(
53620
        page_break,
        PageBreak::Always
            | PageBreak::Page
            | PageBreak::Left
            | PageBreak::Right
            | PageBreak::Recto
            | PageBreak::Verso
            | PageBreak::All
    )
53620
}
/// Get break-inside property for paged media
pub fn get_break_inside(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> BreakInside {
    let Some(id) = dom_id else {
        return BreakInside::Auto;
    };
    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_break_inside(node_data, &id, node_state)
        .and_then(|v| v.get_property().cloned())
        .unwrap_or(BreakInside::Auto)
}
/// Get orphans property (minimum lines at bottom of page)
pub fn get_orphans(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> u32 {
    let Some(id) = dom_id else {
        return 2; // Default value
    };
    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_orphans(node_data, &id, node_state)
        .and_then(|v| v.get_property().cloned())
        .map(|o| o.inner)
        .unwrap_or(2)
}
/// Get widows property (minimum lines at top of page)
pub fn get_widows(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> u32 {
    let Some(id) = dom_id else {
        return 2; // Default value
    };
    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_widows(node_data, &id, node_state)
        .and_then(|v| v.get_property().cloned())
        .map(|w| w.inner)
        .unwrap_or(2)
}
/// Get box-decoration-break property
pub fn get_box_decoration_break(
    styled_dom: &StyledDom,
    dom_id: Option<NodeId>,
) -> BoxDecorationBreak {
    let Some(id) = dom_id else {
        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().cloned())
        .unwrap_or(BoxDecorationBreak::Slice)
}
// Helper functions for break properties
/// Check if a PageBreak value is avoid
pub fn is_avoid_page_break(page_break: &PageBreak) -> bool {
    matches!(page_break, PageBreak::Avoid | PageBreak::AvoidPage)
}
/// Check if a BreakInside value prevents breaks
pub fn is_avoid_break_inside(break_inside: &BreakInside) -> bool {
    matches!(
        break_inside,
        BreakInside::Avoid | BreakInside::AvoidPage | BreakInside::AvoidColumn
    )
}
// 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};
/// 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)]
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>,
}
impl ResolvedFontChains {
    /// Get a font chain by its key
    pub fn get(&self, key: &FontChainKeyOrRef) -> Option<&FontFallbackChain> {
        self.chains.get(key)
    }
    /// Get a font chain by FontChainKey (for system fonts)
    pub fn get_by_chain_key(&self, key: &FontChainKey) -> Option<&FontFallbackChain> {
        self.chains.get(&FontChainKeyOrRef::Chain(key.clone()))
    }
    /// Get a font chain for a font stack (via fontconfig)
    pub fn get_for_font_stack(&self, font_stack: &[FontSelector]) -> Option<&FontFallbackChain> {
        let key = FontChainKeyOrRef::Chain(FontChainKey::from_selectors(font_stack));
        self.chains.get(&key)
    }
    /// Get a font chain for a FontRef pointer
    pub fn get_for_font_ref(&self, ptr: usize) -> Option<&FontFallbackChain> {
        self.chains.get(&FontChainKeyOrRef::Ref(ptr))
    }
    /// Consume self and return the inner HashMap with FontChainKeyOrRef keys
    ///
    /// This is useful when you need access to both Chain and Ref variants.
    pub fn into_inner(self) -> HashMap<FontChainKeyOrRef, FontFallbackChain> {
        self.chains
    }
    /// 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.
4690
    pub fn into_fontconfig_chains(self) -> HashMap<FontChainKey, FontFallbackChain> {
4690
        self.chains
4690
            .into_iter()
4690
            .filter_map(|(key, chain)| {
3535
                match key {
3535
                    FontChainKeyOrRef::Chain(chain_key) => Some((chain_key, chain)),
                    FontChainKeyOrRef::Ref(_) => None,
                }
3535
            })
4690
            .collect()
4690
    }
    /// Get the number of resolved chains
2275
    pub fn len(&self) -> usize {
2275
        self.chains.len()
2275
    }
    /// Check if there are no resolved chains
    pub fn is_empty(&self) -> bool {
        self.chains.is_empty()
    }
    /// Get the number of direct FontRefs
    pub fn font_refs_len(&self) -> usize {
        self.chains.keys().filter(|k| k.is_ref()).count()
    }
}
/// 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
4690
pub fn collect_font_stacks_from_styled_dom(
4690
    styled_dom: &StyledDom,
4690
    platform: &azul_css::system::Platform,
4690
) -> CollectedFontStacks {
    use azul_css::compact_cache::{FONT_WEIGHT_SHIFT, FONT_WEIGHT_MASK, FONT_STYLE_SHIFT, FONT_STYLE_MASK};
4690
    let mut font_stacks = Vec::new();
4690
    let mut hash_to_index: HashMap<u64, usize> = HashMap::new();
4690
    let mut font_refs: HashMap<usize, azul_css::props::basic::font::FontRef> = HashMap::new();
4690
    let node_data = styled_dom.node_data.as_container();
4690
    let cache = &styled_dom.css_property_cache.ptr;
4690
    let compact = match cache.compact_cache.as_ref() {
4690
        Some(c) => c,
        None => return CollectedFontStacks { font_stacks, hash_to_index, font_refs },
    };
    // 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
4690
    let mut unique_font_keys: HashMap<(u64, u8, u8), usize> = HashMap::new();
4690
    let node_count = node_data.internal.len();
29785
    for i in 0..node_count {
        // Only text nodes need fonts
29785
        if !matches!(node_data.internal[i].node_type, NodeType::Text(_)) {
20510
            continue;
9275
        }
9275
        let fh = compact.tier2b_text[i].font_family_hash;
9275
        let t1 = compact.tier1_enums[i];
9275
        let weight_bits = ((t1 >> FONT_WEIGHT_SHIFT) & FONT_WEIGHT_MASK) as u8;
9275
        let style_bits = ((t1 >> FONT_STYLE_SHIFT) & FONT_STYLE_MASK) as u8;
9275
        let key = (fh, weight_bits, style_bits);
9275
        unique_font_keys.entry(key).or_insert(i);
    }
    // Phase 2: For each unique tuple, do ONE expensive CSS lookup on the
    // representative node to get the actual font-family names.
4690
    let styled_nodes = styled_dom.styled_nodes.as_container();
8225
    for (&(fh, _wb, _sb), &repr_idx) in &unique_font_keys {
3535
        let dom_id = match NodeId::from_usize(repr_idx) {
3535
            Some(id) => id,
            None => continue,
        };
3535
        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).
3535
        let font_families = compact.font_hash_to_families
3535
            .get(&fh)
3535
            .cloned()
3535
            .unwrap_or_else(|| {
2870
                StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
2870
            });
        // Check for embedded FontRef
3535
        if let Some(first_family) = font_families.get(0) {
3535
            if let StyleFontFamily::Ref(font_ref) = first_family {
                let ptr = font_ref.parsed as usize;
                font_refs.entry(ptr).or_insert_with(|| font_ref.clone());
                continue;
3535
            }
        }
3535
        let font_weight = match get_font_weight_property(styled_dom, dom_id, node_state) {
3535
            MultiValue::Exact(v) => v,
            _ => StyleFontWeight::Normal,
        };
3535
        let font_style = match get_font_style_property(styled_dom, dom_id, node_state) {
3535
            MultiValue::Exact(v) => v,
            _ => StyleFontStyle::Normal,
        };
3535
        let fc_weight = super::fc::convert_font_weight(font_weight);
3535
        let fc_style = super::fc::convert_font_style(font_style);
3535
        let mut font_stack = Vec::with_capacity(font_families.len() + 3);
3535
        for i in 0..font_families.len() {
3535
            let family = font_families.get(i).unwrap();
3535
            if matches!(family, StyleFontFamily::Ref(_)) {
                continue;
3535
            }
3535
            if let StyleFontFamily::SystemType(system_type) = family {
                let font_names = system_type.get_fallback_chain(platform);
                let system_weight = if system_type.is_bold() { FcWeight::Bold } else { fc_weight };
                let system_style = if system_type.is_italic() { FontStyle::Italic } else { fc_style };
                for font_name in font_names {
                    font_stack.push(FontSelector {
                        family: font_name.to_string(),
                        weight: system_weight,
                        style: system_style,
                        unicode_ranges: Vec::new(),
                    });
                }
3535
            } else {
3535
                font_stack.push(FontSelector {
3535
                    family: family.as_string(),
3535
                    weight: fc_weight,
3535
                    style: fc_style,
3535
                    unicode_ranges: Vec::new(),
3535
                });
3535
            }
        }
        // Add generic fallbacks
14140
        for fallback in &["sans-serif", "serif", "monospace"] {
14140
            if !font_stack.iter().any(|f| f.family.eq_ignore_ascii_case(fallback)) {
7070
                font_stack.push(FontSelector {
7070
                    family: fallback.to_string(),
7070
                    weight: FcWeight::Normal,
7070
                    style: FontStyle::Normal,
7070
                    unicode_ranges: Vec::new(),
7070
                });
7070
            }
        }
3535
        if font_stack.is_empty() {
            continue;
3535
        }
3535
        let key = FontChainKey::from_selectors(&font_stack);
3535
        let hash = {
            use std::hash::{Hash, Hasher};
3535
            let mut hasher = std::collections::hash_map::DefaultHasher::new();
3535
            key.hash(&mut hasher);
3535
            hasher.finish()
        };
3535
        if !hash_to_index.contains_key(&hash) {
3535
            let idx = font_stacks.len();
3535
            font_stacks.push(font_stack);
3535
            hash_to_index.insert(hash, idx);
3535
        }
    }
4690
    CollectedFontStacks {
4690
        font_stacks,
4690
        hash_to_index,
4690
        font_refs,
4690
    }
4690
}
/// 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.
4690
pub fn collect_used_codepoints(
4690
    styled_dom: &StyledDom,
4690
) -> std::collections::BTreeSet<u32> {
4690
    let mut out = std::collections::BTreeSet::new();
4690
    let node_data = styled_dom.node_data.as_container();
29785
    for node in node_data.internal.iter() {
29785
        let azul_core::dom::NodeType::Text(s) = &node.node_type else {
20510
            continue;
        };
97055
        for c in s.as_str().chars() {
97055
            let cp = c as u32;
97055
            if cp >= 0x80 {
                out.insert(cp);
97055
            }
        }
    }
4690
    out
4690
}
/// 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.
pub fn collect_used_codepoints_all(
    styled_dom: &StyledDom,
) -> std::collections::BTreeSet<char> {
    let mut out = std::collections::BTreeSet::new();
    let node_data = styled_dom.node_data.as_container();
    for node in node_data.internal.iter() {
        let azul_core::dom::NodeType::Text(s) = &node.node_type else {
            continue;
        };
        for c in s.as_str().chars() {
            out.insert(c);
        }
    }
    out
}
/// 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`.
3535
pub fn prune_chain_to_used_chars(
3535
    chain: &mut rust_fontconfig::FontFallbackChain,
3535
    used_chars: &std::collections::BTreeSet<u32>,
3535
) {
    fn fm_covers(fm: &rust_fontconfig::FontMatch, cp: u32) -> bool {
        fm.unicode_ranges
            .iter()
            .any(|r| cp >= r.start && cp <= r.end)
    }
70700
    for group in &mut chain.css_fallbacks {
67165
        if group.fonts.is_empty() {
42420
            continue;
24745
        }
        // Track which non-ASCII chars still need coverage as we walk
        // matches in order. We always keep at least the first match.
24745
        let mut needed: Vec<u32> = used_chars.iter().copied().collect();
24745
        needed.retain(|&cp| !fm_covers(&group.fonts[0], cp));
24745
        let mut keep = 1;
24745
        for fm in group.fonts.iter().skip(1) {
17675
            if needed.is_empty() {
17675
                break;
            }
            keep += 1;
            needed.retain(|&cp| !fm_covers(fm, cp));
        }
24745
        group.fonts.truncate(keep);
    }
3535
    chain.unicode_fallbacks.retain(|fm| {
        used_chars.iter().any(|&cp| fm_covers(fm, cp))
    });
3535
}
/// 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.
4690
pub fn scripts_present_in_styled_dom(styled_dom: &StyledDom) -> Vec<UnicodeRange> {
4690
    let scripts = DEFAULT_UNICODE_FALLBACK_SCRIPTS;
4690
    let mut seen = vec![false; scripts.len()];
4690
    let mut hits = 0usize;
4690
    let node_data = styled_dom.node_data.as_container();
29785
    'outer: for node in node_data.internal.iter() {
29785
        let text: &str = match &node.node_type {
9275
            azul_core::dom::NodeType::Text(s) => s.as_str(),
20510
            _ => continue,
        };
97055
        for c in text.chars() {
97055
            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.
97055
            if cp < 0x0400 {
97055
                continue;
            }
            for (idx, r) in scripts.iter().enumerate() {
                if !seen[idx] && cp >= r.start && cp <= r.end {
                    seen[idx] = true;
                    hits += 1;
                    if hits == scripts.len() {
                        break 'outer;
                    }
                    break;
                }
            }
        }
    }
4690
    scripts
4690
        .iter()
4690
        .enumerate()
32830
        .filter_map(|(i, r)| if seen[i] { Some(*r) } else { None })
4690
        .collect()
4690
}
/// 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`].
pub fn resolve_font_chains(
    collected: &CollectedFontStacks,
    fc_cache: &FcFontCache,
    scripts_hint: Option<&[UnicodeRange]>,
) -> ResolvedFontChains {
    resolve_font_chains_with_registry(collected, fc_cache, None, scripts_hint)
}
/// 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.
4690
pub fn resolve_font_chains_with_registry(
4690
    collected: &CollectedFontStacks,
4690
    fc_cache: &FcFontCache,
4690
    registry: Option<&rust_fontconfig::registry::FcFontRegistry>,
4690
    scripts_hint: Option<&[UnicodeRange]>,
4690
) -> ResolvedFontChains {
4690
    let mut chains = HashMap::new();
    // Resolve system/file font stacks via fontconfig
8225
    for font_stack in &collected.font_stacks {
3535
        if font_stack.is_empty() {
            continue;
3535
        }
        // Build font families list
3535
        let font_families: Vec<String> = font_stack
3535
            .iter()
10605
            .map(|s| s.family.clone())
10605
            .filter(|f| !f.is_empty())
3535
            .collect();
3535
        let font_families = if font_families.is_empty() {
            vec!["sans-serif".to_string()]
        } else {
3535
            font_families
        };
3535
        let weight = font_stack[0].weight;
3535
        let is_italic = font_stack[0].style == FontStyle::Italic;
3535
        let is_oblique = font_stack[0].style == FontStyle::Oblique;
3535
        let cache_key = FontChainKeyOrRef::Chain(FontChainKey {
3535
            font_families: font_families.clone(),
3535
            weight,
3535
            italic: is_italic,
3535
            oblique: is_oblique,
3535
        });
        // Skip if already resolved
3535
        if chains.contains_key(&cache_key) {
            continue;
3535
        }
        // 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.
3535
        let italic = if is_italic {
            PatternMatch::True
        } else {
3535
            PatternMatch::False
        };
3535
        let oblique = if is_oblique {
            PatternMatch::True
        } else {
3535
            PatternMatch::False
        };
        // Registry-aware resolve: scout-on-demand path when available.
        // See `resolve_font_chains_with_registry` doc for rationale.
3535
        let chain = if let Some(reg) = registry {
            reg.request_and_resolve_with_scripts(
                &font_families, weight, italic, oblique, scripts_hint,
            )
        } else {
3535
            let mut trace = Vec::new();
3535
            fc_cache.resolve_font_chain_with_scripts(
3535
                &font_families, weight, italic, oblique, scripts_hint, &mut trace,
            )
        };
3535
        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.
4690
    ResolvedFontChains { chains }
4690
}
/// 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.
2820
pub fn collect_and_resolve_font_chains_with_registration<T: crate::font_traits::ParsedFontTrait>(
2820
    styled_dom: &StyledDom,
2820
    fc_cache: &FcFontCache,
2820
    font_manager: &crate::text3::cache::FontManager<T>,
2820
    platform: &azul_css::system::Platform,
2820
) -> ResolvedFontChains {
2820
    let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
    // Register embedded FontRefs (from the same scan, no second pass)
2820
    for (_ptr, font_ref) in &collected.font_refs {
        font_manager.register_embedded_font(font_ref);
    }
    // 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)
2820
    if let Some(registry) = font_manager.registry.as_deref() {
        let used_chars = collect_used_codepoints_all(styled_dom);
        if !used_chars.is_empty() {
            return resolve_font_chains_fast(
                &collected,
                registry,
                &used_chars,
            );
        }
2820
    }
    // Legacy path: pattern-map resolver. Only reached when the
    // caller passes an `FcFontCache` without a live registry
    // (ad-hoc tests, the PDF writer, etc.).
2820
    let scripts = scripts_present_in_styled_dom(styled_dom);
2820
    let mut resolved = resolve_font_chains_with_registry(
2820
        &collected,
2820
        fc_cache,
2820
        font_manager.registry.as_deref(),
2820
        Some(&scripts),
    );
2820
    let used_chars = collect_used_codepoints(styled_dom);
2820
    for chain in resolved.chains.values_mut() {
1835
        prune_chain_to_used_chars(chain, &used_chars);
1835
    }
2820
    resolved
2820
}
/// 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.
pub fn resolve_font_chains_fast(
    collected: &CollectedFontStacks,
    registry: &rust_fontconfig::registry::FcFontRegistry,
    codepoints: &std::collections::BTreeSet<char>,
) -> ResolvedFontChains {
    use rust_fontconfig::PatternMatch;
    static DBG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
    let dbg = *DBG.get_or_init(|| std::env::var_os("AZ_FAST_RESOLVE_DEBUG").is_some());
    let mut chains: HashMap<FontChainKeyOrRef, rust_fontconfig::FontFallbackChain> =
        HashMap::new();
    for font_stack in &collected.font_stacks {
        if font_stack.is_empty() {
            continue;
        }
        let font_families: Vec<String> = font_stack
            .iter()
            .map(|s| s.family.clone())
            .filter(|f| !f.is_empty())
            .collect();
        let font_families = if font_families.is_empty() {
            vec!["sans-serif".to_string()]
        } else {
            font_families
        };
        let weight = font_stack[0].weight;
        let is_italic = font_stack[0].style == FontStyle::Italic;
        let is_oblique = font_stack[0].style == FontStyle::Oblique;
        let cache_key = FontChainKeyOrRef::Chain(FontChainKey {
            font_families: font_families.clone(),
            weight,
            italic: is_italic,
            oblique: is_oblique,
        });
        if chains.contains_key(&cache_key) {
            continue;
        }
        let italic_match = if is_italic {
            PatternMatch::True
        } else {
            PatternMatch::False
        };
        let request = vec![(font_families.clone(), codepoints.clone())];
        let mut chains_out = registry.request_fonts_fast(&request, weight, italic_match);
        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(|c| c.css_fallbacks.len()).unwrap_or(0),
                total_fonts,
            );
        }
        if let Some(chain) = chains_out.pop() {
            chains.insert(cache_key, chain);
        }
    }
    ResolvedFontChains { chains }
}
/// Legacy wrapper: collect + resolve without registration. Kept for
/// backward compatibility; defaults to the full 7-script unicode
/// fallback set.
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: crate::font_traits::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 (_ptr, font_ref) in &collected.font_refs {
        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.
4690
pub fn collect_font_ids_from_chains(chains: &ResolvedFontChains) -> HashSet<FontId> {
4690
    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).
4690
    if chains.chains.is_empty() {
1190
        return font_ids;
3500
    }
3535
    for chain in chains.chains.values() {
        // Collect from CSS fallbacks
70700
        for group in &chain.css_fallbacks {
91910
            for font in &group.fonts {
24745
                font_ids.insert(font.id);
24745
            }
        }
        // Collect from Unicode fallbacks
3535
        for font in &chain.unicode_fallbacks {
            font_ids.insert(font.id);
        }
    }
3500
    font_ids
4690
}
/// 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
4690
pub fn compute_fonts_to_load(
4690
    required_fonts: &HashSet<FontId>,
4690
    already_loaded: &HashSet<FontId>,
4690
) -> 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.
4690
    if required_fonts.is_empty() {
1190
        return HashSet::new();
3500
    }
3500
    required_fonts.difference(already_loaded).cloned().collect()
4690
}
/// 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
1800
pub fn load_fonts_from_disk<T, F>(
1800
    font_ids: &HashSet<FontId>,
1800
    fc_cache: &FcFontCache,
1800
    load_fn: F,
1800
) -> FontLoadResult<T>
1800
where
1800
    // Bytes come in as `Arc<FontBytes>` so the loader can retain
1800
    // them cheaply (one `Arc::clone` per retained copy). On disk the
1800
    // backing is an mmap, so untouched glyf/CFF pages don't count
1800
    // toward RSS — the layout shaper only faults in pages it reads.
1800
    F: Fn(std::sync::Arc<rust_fontconfig::FontBytes>, usize) -> Result<T, crate::text3::cache::LayoutError>,
{
1800
    let mut loaded = HashMap::new();
1800
    let mut failed = Vec::new();
12740
    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.
10940
        let font_bytes = match fc_cache.get_font_bytes(font_id) {
10940
            Some(bytes) => bytes,
            None => {
                failed.push((
                    *font_id,
                    format!("Could not get font bytes for {:?}", font_id),
                ));
                continue;
            }
        };
        // Get font index (for font collections like .ttc files)
10940
        let font_index = fc_cache
10940
            .get_font_by_id(font_id)
10940
            .and_then(|source| match source {
10940
                rust_fontconfig::OwnedFontSource::Disk(path) => Some(path.font_index),
                rust_fontconfig::OwnedFontSource::Memory(font) => Some(font.font_index),
10940
            })
10940
            .unwrap_or(0) as usize;
        // Load the font using the provided function
10940
        match load_fn(font_bytes, font_index) {
10940
            Ok(font) => {
10940
                loaded.insert(*font_id, font);
10940
            }
            Err(e) => {
                failed.push((
                    *font_id,
                    format!("Failed to parse font {:?}: {:?}", font_id, e),
                ));
            }
        }
    }
1800
    FontLoadResult { loaded, failed }
1800
}
/// 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)
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(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 {
210
    fn default() -> Self {
        // Evaluate UA CSS rules with a default context (no OS info).
        // Picks the unconditional fallback: classic light, auto width.
210
        let ctx = azul_css::dynamic_selector::DynamicSelectorContext::default();
210
        let ua = azul_core::ua_css::evaluate_ua_scrollbar_css(&ctx);
210
        Self::from_ua_resolved(&ua)
210
    }
}
impl ComputedScrollbarStyle {
    /// Build from resolved UA scrollbar CSS properties.
    ///
    /// Each property is read individually from the resolved UA CSS.
27020
    fn from_ua_resolved(ua: &azul_core::ua_css::ResolvedUaScrollbar) -> Self {
27020
        let width_mode = ua.width;
27020
        let visibility = ua.visibility;
27020
        let fade_delay_ms = ua.fade_delay.ms;
27020
        let fade_duration_ms = ua.fade_duration.ms;
27020
        let visual_width_px = match width_mode {
            LayoutScrollbarWidth::Thin => 8.0,
27020
            LayoutScrollbarWidth::Auto => 12.0,
            LayoutScrollbarWidth::None => 0.0,
        };
        // Overlay scrollbars don't reserve layout space.
27020
        let reserve_width_px = if visibility == ScrollbarVisibilityMode::WhenScrolling {
            0.0
        } else {
27020
            visual_width_px
        };
27020
        let clip = visibility == ScrollbarVisibilityMode::WhenScrolling;
        // Overlay scrollbars hide buttons and corner by default.
27020
        let is_overlay = visibility == ScrollbarVisibilityMode::WhenScrolling;
27020
        let show_scroll_buttons = !is_overlay;
27020
        let scroll_button_size_px = if is_overlay { 0.0 } else { visual_width_px };
27020
        let show_corner_rect = !is_overlay;
27020
        let (thumb_color, track_color) = match ua.color {
27020
            StyleScrollbarColor::Custom(c) => (c.thumb, c.track),
            _ => (ColorU::TRANSPARENT, ColorU::TRANSPARENT),
        };
        // Compute hover / active variants:
        // Hover: lighten thumb by ~20%, widen by +4px
        // Active: darken thumb by ~10%, widen by +4px
27020
        let thumb_hover = ColorU {
27020
            r: thumb_color.r.saturating_add(30),
27020
            g: thumb_color.g.saturating_add(30),
27020
            b: thumb_color.b.saturating_add(30),
27020
            a: thumb_color.a.saturating_add(40).min(255),
27020
        };
27020
        let thumb_active = ColorU {
27020
            r: thumb_color.r.saturating_sub(15),
27020
            g: thumb_color.g.saturating_sub(15),
27020
            b: thumb_color.b.saturating_sub(15),
27020
            a: 255,  // fully opaque when pressed
27020
        };
27020
        let track_hover = ColorU {
27020
            r: track_color.r,
27020
            g: track_color.g,
27020
            b: track_color.b,
27020
            a: track_color.a.saturating_add(40).min(255),
27020
        };
27020
        let hover_width = visual_width_px + 4.0;
27020
        let active_width = visual_width_px + 4.0;
27020
        Self {
27020
            width_mode,
27020
            visual_width_px,
27020
            reserve_width_px,
27020
            thumb_color,
27020
            track_color,
27020
            button_color: ColorU::TRANSPARENT,
27020
            corner_color: ColorU::TRANSPARENT,
27020
            clip_to_container_border: clip,
27020
            fade_delay_ms,
27020
            fade_duration_ms,
27020
            visibility,
27020
            show_scroll_buttons,
27020
            scroll_button_size_px,
27020
            show_corner_rect,
27020
            thumb_color_hover: Some(thumb_hover),
27020
            thumb_color_active: Some(thumb_active),
27020
            track_color_hover: Some(track_hover),
27020
            visual_width_px_hover: Some(hover_width),
27020
            visual_width_px_active: Some(active_width),
27020
        }
27020
    }
}
/// 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).
26810
pub fn get_scrollbar_style(
26810
    styled_dom: &StyledDom,
26810
    node_id: NodeId,
26810
    node_state: &StyledNodeState,
26810
    system_style: Option<&azul_css::system::SystemStyle>,
26810
) -> ComputedScrollbarStyle {
26810
    let node_data = &styled_dom.node_data.as_container()[node_id];
    // Step 1: Evaluate UA scrollbar CSS using the DynamicSelector system.
26810
    let ctx = match system_style {
        Some(sys) => {
            azul_css::dynamic_selector::DynamicSelectorContext::from_system_style(sys)
        }
26810
        None => azul_css::dynamic_selector::DynamicSelectorContext::default(),
    };
26810
    let ua = azul_core::ua_css::evaluate_ua_scrollbar_css(&ctx);
26810
    let result = ComputedScrollbarStyle::from_ua_resolved(&ua);
    // FAST PATH: 99% of nodes have no scrollbar CSS. Bail before walking 8 × cascade.
26810
    if node_state.is_normal() {
26810
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
26810
            if !cc.has_scrollbar_css(node_id.index()) {
26810
                return result;
            }
        }
    }
    let mut result = result;
    // Step 2: Check individual scrollbar part backgrounds
    if let Some(track) = styled_dom
        .css_property_cache
        .ptr
        .get_scrollbar_track(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
    {
        result.track_color = extract_color_from_background(track);
    }
    if let Some(thumb) = styled_dom
        .css_property_cache
        .ptr
        .get_scrollbar_thumb(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
    {
        result.thumb_color = extract_color_from_background(thumb);
    }
    if let Some(button) = styled_dom
        .css_property_cache
        .ptr
        .get_scrollbar_button(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
    {
        result.button_color = extract_color_from_background(button);
    }
    if let Some(corner) = styled_dom
        .css_property_cache
        .ptr
        .get_scrollbar_corner(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
    {
        result.corner_color = extract_color_from_background(corner);
    }
    // Step 3: Check for scrollbar-width (overrides width only, not overlay)
    if let Some(scrollbar_width) = styled_dom
        .css_property_cache
        .ptr
        .get_scrollbar_width(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
    {
        result.width_mode = *scrollbar_width;
        let w = match scrollbar_width {
            LayoutScrollbarWidth::Auto => 12.0,
            LayoutScrollbarWidth::Thin => 8.0,
            LayoutScrollbarWidth::None => 0.0,
        };
        result.visual_width_px = w;
        if result.visibility != ScrollbarVisibilityMode::WhenScrolling {
            result.reserve_width_px = w;
        }
    }
    // Step 4: Check for scrollbar-color (overrides thumb/track colors)
    if let Some(scrollbar_color) = styled_dom
        .css_property_cache
        .ptr
        .get_scrollbar_color(node_data, &node_id, node_state)
        .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;
            }
        }
    }
    // Step 5: Check for -azul-scrollbar-visibility
    if let Some(vis) = styled_dom
        .css_property_cache
        .ptr
        .get_scrollbar_visibility(node_data, &node_id, node_state)
        .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;
        }
    }
    // Step 6: Check for -azul-scrollbar-fade-delay
    if let Some(delay) = styled_dom
        .css_property_cache
        .ptr
        .get_scrollbar_fade_delay(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
    {
        result.fade_delay_ms = delay.ms;
    }
    // Step 7: Check for -azul-scrollbar-fade-duration
    if let Some(dur) = styled_dom
        .css_property_cache
        .ptr
        .get_scrollbar_fade_duration(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
    {
        result.fade_duration_ms = dur.ms;
    }
    result
26810
}
/// 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).
38455
pub fn get_scrollbar_style_cached<T: crate::font_traits::ParsedFontTrait>(
38455
    ctx: &crate::solver3::LayoutContext<'_, T>,
38455
    node_id: NodeId,
38455
    node_state: &StyledNodeState,
38455
) -> ComputedScrollbarStyle {
38455
    if let Some(s) = ctx.scrollbar_style_cache.borrow().get(&node_id) {
20825
        return s.clone();
17630
    }
17630
    let style = get_scrollbar_style(
17630
        ctx.styled_dom,
17630
        node_id,
17630
        node_state,
17630
        ctx.system_style.as_deref(),
    );
17630
    ctx.scrollbar_style_cache.borrow_mut().insert(node_id, style.clone());
17630
    style
38455
}
/// Helper to extract a solid color from a StyleBackgroundContent
fn extract_color_from_background(
    bg: &azul_css::props::style::background::StyleBackgroundContent,
) -> ColorU {
    use azul_css::props::style::background::StyleBackgroundContent;
    match bg {
        StyleBackgroundContent::Color(c) => *c,
        _ => ColorU::TRANSPARENT,
    }
}
/// Check if a node should clip its scrollbar to the container's border-radius
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)
pub fn get_scrollbar_width_px(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> f32 {
    let style = get_scrollbar_style(styled_dom, node_id, node_state, None);
    style.visual_width_px
}
/// 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.
10045
pub fn is_text_selectable(
10045
    styled_dom: &StyledDom,
10045
    node_id: NodeId,
10045
    node_state: &StyledNodeState,
10045
) -> bool {
10045
    let node_data = &styled_dom.node_data.as_container()[node_id];
10045
    styled_dom
10045
        .css_property_cache
10045
        .ptr
10045
        .get_user_select(node_data, &node_id, node_state)
10045
        .and_then(|v| v.get_property())
10045
        .map(|us| *us != StyleUserSelect::None)
10045
        .unwrap_or(true) // Default: text is selectable
10045
}
/// 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.
pub fn is_node_contenteditable(styled_dom: &StyledDom, node_id: NodeId) -> bool {
    use azul_core::dom::AttributeType;
    let node_data = &styled_dom.node_data.as_container()[node_id];
    // First check the direct contenteditable field (primary method)
    if node_data.is_contenteditable() {
        return true;
    }
    // Also check the attribute for backwards compatibility
    // Only return true if the attribute value is explicitly true
    node_data.attributes().as_ref().iter().any(|attr| {
        matches!(attr, AttributeType::ContentEditable(true))
    })
}
// =============================================================================
// Additional ExtractPropertyValue impls (not in compact cache tier 1/2)
// =============================================================================
use azul_css::props::layout::text::LayoutTextJustify;
use azul_css::props::layout::table::{LayoutTableLayout, StyleBorderCollapse, StyleCaptionSide, StyleEmptyCells};
use azul_css::props::style::text::StyleHyphens;
use azul_css::props::style::text::StyleWordBreak;
use azul_css::props::style::text::StyleOverflowWrap;
use azul_css::props::style::text::StyleLineBreak;
use azul_css::props::style::text::StyleTextAlignLast;
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::style::effects::StyleAspectRatio;
use azul_css::props::style::effects::StyleTextOrientation;
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<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.clone()),
            _ => 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.
pub fn get_height_value(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<LayoutHeight> {
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_height(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
}
/// Get shape-inside property. Returns Option<ShapeInside> (cloned).
pub fn get_shape_inside(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<azul_css::props::layout::shape::ShapeInside> {
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_shape_inside(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
}
/// Get shape-outside property. Returns Option<ShapeOutside> (cloned).
pub fn get_shape_outside(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<azul_css::props::layout::shape::ShapeOutside> {
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_shape_outside(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
}
/// Get line-height as the full StyleLineHeight value for caller resolution.
pub fn get_line_height_value(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<azul_css::props::style::text::StyleLineHeight> {
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_line_height(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
}
/// Get text-indent as the full StyleTextIndent value for caller resolution.
pub fn get_text_indent_value(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<azul_css::props::style::text::StyleTextIndent> {
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_text_indent(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
}
/// Get column-count property. Returns Option<ColumnCount>.
pub fn get_column_count(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<azul_css::props::layout::column::ColumnCount> {
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_column_count(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
}
/// Get initial-letter property. Returns Option<StyleInitialLetter>.
pub fn get_initial_letter(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<azul_css::props::style::text::StyleInitialLetter> {
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_initial_letter(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
}
/// Get line-clamp property. Returns Option<StyleLineClamp>.
pub fn get_line_clamp(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<azul_css::props::style::text::StyleLineClamp> {
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_line_clamp(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
}
/// Get hanging-punctuation property. Returns Option<StyleHangingPunctuation>.
pub fn get_hanging_punctuation(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<azul_css::props::style::text::StyleHangingPunctuation> {
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_hanging_punctuation(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
}
/// Get text-combine-upright property. Returns Option<StyleTextCombineUpright>.
pub fn get_text_combine_upright(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<azul_css::props::style::text::StyleTextCombineUpright> {
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_text_combine_upright(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
}
/// Get exclusion-margin value. Returns f32 (default 0.0).
pub fn get_exclusion_margin(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> f32 {
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_exclusion_margin(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .map(|v| v.inner.get() as f32)
        .unwrap_or(0.0)
}
/// Get hyphenation-language property. Returns Option<StyleHyphenationLanguage>.
pub fn get_hyphenation_language(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<azul_css::props::style::azul_exclusion::StyleHyphenationLanguage> {
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_hyphenation_language(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
}
/// Get border-spacing property.
pub fn get_border_spacing(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> azul_css::props::layout::table::LayoutBorderSpacing {
    use azul_css::props::basic::pixel::PixelValue;
    // FAST PATH: compact cache for normal state
    if node_state.is_normal() {
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
            let h_raw = cc.get_border_spacing_h_raw(node_id.index());
            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
            if h_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
                && v_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
            {
                return azul_css::props::layout::table::LayoutBorderSpacing {
                    horizontal: PixelValue::px(h_raw as f32 / 10.0),
                    vertical: PixelValue::px(v_raw as f32 / 10.0),
                };
            }
        }
    }
    // SLOW PATH
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_border_spacing(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
        .unwrap_or_default()
}
/// 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.
48580
pub fn get_opacity(
48580
    styled_dom: &StyledDom,
48580
    node_id: NodeId,
48580
    node_state: &StyledNodeState,
48580
) -> f32 {
    // FAST PATH: compact cache for normal state
48580
    if node_state.is_normal() {
48580
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
48580
            let raw = cc.get_opacity_raw(node_id.index());
48580
            if raw == azul_css::compact_cache::OPACITY_SENTINEL {
48440
                return 1.0;
140
            }
140
            return (raw as f32) / 254.0;
        }
    }
    // SLOW PATH: fall back to cascade walk (state != normal, or no compact cache)
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_opacity(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .map(|v| v.inner.normalized())
        .unwrap_or(1.0)
48580
}
/// Get filter property. Returns Option with cloned filter list.
pub fn get_filter(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<azul_css::props::style::filter::StyleFilterVec> {
    if node_state.is_normal() {
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
            if !cc.has_filter(node_id.index()) { return None; }
        }
    }
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_filter(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
}
/// Get backdrop-filter property. Returns Option with cloned filter list.
pub fn get_backdrop_filter(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<azul_css::props::style::filter::StyleFilterVec> {
    if node_state.is_normal() {
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
            if !cc.has_backdrop_filter(node_id.index()) { return None; }
        }
    }
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_backdrop_filter(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
}
/// 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]
61600
fn box_shadow_fast_bail(
61600
    styled_dom: &StyledDom,
61600
    node_id: NodeId,
61600
    node_state: &StyledNodeState,
61600
) -> bool {
61600
    if !node_state.is_normal() { return false; }
61600
    if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
61600
        return !cc.has_box_shadow(node_id.index());
    }
    false
61600
}
/// Get box-shadow for left side. Returns Option<StyleBoxShadow> (cloned).
15400
pub fn get_box_shadow_left(
15400
    styled_dom: &StyledDom,
15400
    node_id: NodeId,
15400
    node_state: &StyledNodeState,
15400
) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
15400
    if box_shadow_fast_bail(styled_dom, node_id, node_state) { return None; }
35
    let node_data = &styled_dom.node_data.as_container()[node_id];
35
    styled_dom.css_property_cache.ptr
35
        .get_box_shadow_left(node_data, &node_id, node_state)
35
        .and_then(|v| v.get_property())
35
        .map(|v| (**v).clone())
15400
}
/// Get box-shadow for right side. Returns Option<StyleBoxShadow> (cloned).
15400
pub fn get_box_shadow_right(
15400
    styled_dom: &StyledDom,
15400
    node_id: NodeId,
15400
    node_state: &StyledNodeState,
15400
) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
15400
    if box_shadow_fast_bail(styled_dom, node_id, node_state) { return None; }
35
    let node_data = &styled_dom.node_data.as_container()[node_id];
35
    styled_dom.css_property_cache.ptr
35
        .get_box_shadow_right(node_data, &node_id, node_state)
35
        .and_then(|v| v.get_property())
35
        .map(|v| (**v).clone())
15400
}
/// Get box-shadow for top side. Returns Option<StyleBoxShadow> (cloned).
15400
pub fn get_box_shadow_top(
15400
    styled_dom: &StyledDom,
15400
    node_id: NodeId,
15400
    node_state: &StyledNodeState,
15400
) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
15400
    if box_shadow_fast_bail(styled_dom, node_id, node_state) { return None; }
35
    let node_data = &styled_dom.node_data.as_container()[node_id];
35
    styled_dom.css_property_cache.ptr
35
        .get_box_shadow_top(node_data, &node_id, node_state)
35
        .and_then(|v| v.get_property())
35
        .map(|v| (**v).clone())
15400
}
/// Get box-shadow for bottom side. Returns Option<StyleBoxShadow> (cloned).
15400
pub fn get_box_shadow_bottom(
15400
    styled_dom: &StyledDom,
15400
    node_id: NodeId,
15400
    node_state: &StyledNodeState,
15400
) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
15400
    if box_shadow_fast_bail(styled_dom, node_id, node_state) { return None; }
35
    let node_data = &styled_dom.node_data.as_container()[node_id];
35
    styled_dom.css_property_cache.ptr
35
        .get_box_shadow_bottom(node_data, &node_id, node_state)
35
        .and_then(|v| v.get_property())
35
        .map(|v| (**v).clone())
15400
}
/// Get text-shadow property. Returns Option<StyleBoxShadow> (cloned).
pub fn get_text_shadow(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
    if node_state.is_normal() {
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
            if !cc.has_text_shadow(node_id.index()) { return None; }
        }
    }
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_text_shadow(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .map(|v| (**v).clone())
}
/// 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.
43330
pub fn get_transform(
43330
    styled_dom: &StyledDom,
43330
    node_id: NodeId,
43330
    node_state: &StyledNodeState,
43330
) -> Option<azul_css::props::style::transform::StyleTransformVec> {
    // FAST PATH: bit check in compact cache
43330
    if node_state.is_normal() {
43330
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
43330
            if !cc.has_transform(node_id.index()) {
43330
                return None;
            }
            // has_transform set → fall through to cascade walk for the value
        }
    }
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_transform(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
43330
}
/// Get counter-reset property. Returns Option<CounterReset> (cloned).
pub fn get_counter_reset(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<azul_css::props::style::content::CounterReset> {
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_counter_reset(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
}
/// Get counter-increment property. Returns Option<CounterIncrement> (cloned).
pub fn get_counter_increment(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<azul_css::props::style::content::CounterIncrement> {
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_counter_increment(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
}
/// 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>
/// ```
1540
pub fn is_node_contenteditable_inherited(styled_dom: &StyledDom, node_id: NodeId) -> bool {
    use azul_core::dom::AttributeType;
1540
    let node_data_container = styled_dom.node_data.as_container();
1540
    let hierarchy = styled_dom.node_hierarchy.as_container();
1540
    let mut current_node_id = Some(node_id);
2590
    while let Some(nid) = current_node_id {
2100
        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
2100
        if node_data.is_contenteditable() {
1050
            return true;
1050
        }
        // Then check for explicit contenteditable attribute on this node
        // This handles HTML-style contenteditable="true" or contenteditable="false"
1050
        for attr in node_data.attributes().as_ref().iter() {
35
            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;
35
            }
        }
        // No explicit setting on this node, check parent for inheritance
1050
        current_node_id = hierarchy.get(nid).and_then(|h| h.parent_id());
    }
    // Reached root without finding contenteditable - not editable
490
    false
1540
}
/// 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
pub fn find_contenteditable_ancestor(styled_dom: &StyledDom, node_id: NodeId) -> Option<NodeId> {
    use azul_core::dom::AttributeType;
    let node_data_container = styled_dom.node_data.as_container();
    let hierarchy = styled_dom.node_hierarchy.as_container();
    let mut current_node_id = Some(node_id);
    while let Some(nid) = current_node_id {
        let node_data = &node_data_container[nid];
        // First check the direct contenteditable field (set via set_contenteditable())
        if node_data.is_contenteditable() {
            return Some(nid);
        }
        // Then check for contenteditable attribute on this node
        for attr in node_data.attributes().as_ref().iter() {
            if let AttributeType::ContentEditable(is_editable) = attr {
                if *is_editable {
                    return Some(nid);
                } else {
                    // Explicitly not editable - stop search
                    return None;
                }
            }
        }
        // Check parent
        current_node_id = hierarchy.get(nid).and_then(|h| h.parent_id());
    }
    None
}
// --- 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) => {
315
        pub fn $fn_name(
315
            styled_dom: &StyledDom,
315
            node_id: NodeId,
315
            node_state: &StyledNodeState,
315
        ) -> Option<$ret_type> {
315
            let node_data = &styled_dom.node_data.as_container()[node_id];
315
            styled_dom
315
                .css_property_cache
315
                .ptr
315
                .$cache_method(node_data, &node_id, node_state)
315
                .cloned()
315
        }
    };
}
// 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).
pub fn get_grid_template_areas_prop(
    styled_dom: &StyledDom,
    node_id: NodeId,
    node_state: &StyledNodeState,
) -> Option<GridTemplateAreas> {
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom
        .css_property_cache
        .ptr
        .get_property(node_data, &node_id, node_state, &CssPropertyType::GridTemplateAreas)
        .and_then(|p| {
            if let CssProperty::GridTemplateAreas(v) = p {
                v.get_property().cloned()
            } else {
                None
            }
        })
}
/// 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).
26915
pub fn get_clip_path(
26915
    styled_dom: &StyledDom,
26915
    node_id: NodeId,
26915
    node_state: &StyledNodeState,
26915
) -> Option<azul_css::props::layout::shape::ClipPath> {
    // Negative fast path: most nodes have `clip-path: none`.
26915
    if node_state.is_normal() {
26915
        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
26915
            if !cc.has_clip_path(node_id.index()) {
26915
                return None;
            }
        }
    }
    let node_data = &styled_dom.node_data.as_container()[node_id];
    styled_dom.css_property_cache.ptr
        .get_clip_path(node_data, &node_id, node_state)
        .and_then(|v| v.get_property())
        .cloned()
26915
}