Skip to main content

azul_layout/solver3/
getters.rs

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
4use azul_core::{
5    dom::{NodeId, NodeType},
6    geom::LogicalSize,
7    id::NodeId as CoreNodeId,
8    styled_dom::{StyledDom, StyledNodeState},
9};
10use 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
48use 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.
77pub fn get_element_font_size(
78    styled_dom: &StyledDom,
79    dom_id: NodeId,
80    node_state: &StyledNodeState,
81) -> 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    let _ = compute_all_font_sizes_px; // referenced so other callers / native keep it
94    resolve_font_size_slow(styled_dom, dom_id, node_state)
95}
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).
114fn 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.
235fn resolve_font_size_slow(
236    styled_dom: &StyledDom,
237    dom_id: NodeId,
238    node_state: &StyledNodeState,
239) -> f32 {
240    let node_data = &styled_dom.node_data.as_container()[dom_id];
241    let cache = &styled_dom.css_property_cache.ptr;
242
243    if let Some(vec) = cache.computed_values.get(dom_id.index()) {
244        if let Ok(idx) = vec.binary_search_by_key(
245            &azul_css::props::property::CssPropertyType::FontSize,
246            |(k, _)| *k,
247        ) {
248            if let azul_css::props::property::CssProperty::FontSize(css_val) = &vec[idx].1.property {
249                if let Some(fs) = css_val.get_property() {
250                    if fs.inner.metric == azul_css::props::basic::length::SizeMetric::Px {
251                        return fs.inner.number.get();
252                    }
253                }
254            }
255        }
256    }
257
258    let parent_font_size = styled_dom
259        .node_hierarchy
260        .as_container()
261        .get(dom_id)
262        .and_then(|node| node.parent_id())
263        .map(|parent_id| resolve_font_size_slow(styled_dom, parent_id, node_state))
264        .unwrap_or(DEFAULT_FONT_SIZE);
265
266    let root_font_size = if dom_id == NodeId::new(0) {
267        DEFAULT_FONT_SIZE
268    } else {
269        resolve_font_size_slow(styled_dom, NodeId::new(0), node_state)
270    };
271
272    cache
273        .get_font_size(node_data, &dom_id, node_state)
274        .and_then(|v| v.get_property().cloned())
275        .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        .unwrap_or(DEFAULT_FONT_SIZE)
288}
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.
295pub fn get_parent_font_size(
296    styled_dom: &StyledDom,
297    dom_id: NodeId,
298    _node_state: &StyledNodeState, // child's state — intentionally unused
299) -> f32 {
300    styled_dom
301        .node_hierarchy
302        .as_container()
303        .get(dom_id)
304        .and_then(|node| node.parent_id())
305        .map(|parent_id| {
306            let parent_state = &styled_dom.styled_nodes.as_container()[parent_id].styled_node_state;
307            get_element_font_size(styled_dom, parent_id, parent_state)
308        })
309        .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE)
310}
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.
316pub fn get_root_font_size(styled_dom: &StyledDom, _node_state: &StyledNodeState) -> f32 {
317    let root_id = NodeId::new(0);
318    let root_state = &styled_dom.styled_nodes.as_container()[root_id].styled_node_state;
319    get_element_font_size(styled_dom, root_id, root_state)
320}
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)]
325pub 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
336impl<T> MultiValue<T> {
337    /// Returns true if this is an Auto value
338    pub fn is_auto(&self) -> bool {
339        matches!(self, MultiValue::Auto)
340    }
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    pub fn exact(self) -> Option<T> {
349        match self {
350            MultiValue::Exact(v) => Some(v),
351            _ => None,
352        }
353    }
354
355    /// Gets the exact value or returns the provided default
356    pub fn unwrap_or(self, default: T) -> T {
357        match self {
358            MultiValue::Exact(v) => v,
359            _ => default,
360        }
361    }
362
363    /// Gets the exact value or returns T::default()
364    pub fn unwrap_or_default(self) -> T
365    where
366        T: Default,
367    {
368        match self {
369            MultiValue::Exact(v) => v,
370            _ => T::default(),
371        }
372    }
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
389impl 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    pub fn is_clipped(&self) -> bool {
393        matches!(
394            self,
395            MultiValue::Exact(
396                LayoutOverflow::Hidden
397                    | LayoutOverflow::Clip
398                    | LayoutOverflow::Auto
399                    | LayoutOverflow::Scroll
400            )
401        )
402    }
403
404    pub fn is_scroll(&self) -> bool {
405        matches!(
406            self,
407            MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
408        )
409    }
410
411    pub fn is_auto_overflow(&self) -> bool {
412        matches!(self, MultiValue::Exact(LayoutOverflow::Auto))
413    }
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    pub fn is_clip(&self) -> bool {
431        matches!(self, MultiValue::Exact(LayoutOverflow::Clip))
432    }
433
434    pub fn is_visible_or_clip(&self) -> bool {
435        matches!(
436            self,
437            MultiValue::Exact(LayoutOverflow::Visible | LayoutOverflow::Clip)
438        )
439    }
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    pub fn resolve_computed(&self, other_axis: &MultiValue<LayoutOverflow>) -> MultiValue<LayoutOverflow> {
446        match (self, other_axis) {
447            (MultiValue::Exact(val), MultiValue::Exact(other)) => {
448                MultiValue::Exact(val.resolve_computed(*other))
449            }
450            _ => *self,
451        }
452    }
453}
454
455// Implement helper methods for LayoutPosition
456impl MultiValue<LayoutPosition> {
457    pub fn is_absolute_or_fixed(&self) -> bool {
458        matches!(
459            self,
460            MultiValue::Exact(LayoutPosition::Absolute | LayoutPosition::Fixed)
461        )
462    }
463}
464
465// Implement helper methods for LayoutFloat
466impl MultiValue<LayoutFloat> {
467    pub fn is_none(&self) -> bool {
468        matches!(
469            self,
470            MultiValue::Auto
471                | MultiValue::Initial
472                | MultiValue::Inherit
473                | MultiValue::Exact(LayoutFloat::None)
474        )
475    }
476}
477
478impl<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
486macro_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        pub fn $fn_name(
490            styled_dom: &StyledDom,
491            node_id: NodeId,
492            node_state: &StyledNodeState,
493        ) -> MultiValue<PixelValue> {
494            // FAST PATH: compact cache for normal state (O(1) array lookup)
495            if node_state.is_normal() {
496                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
497                    let raw = cc.$compact_method(node_id.index());
498                    if raw == azul_css::compact_cache::I16_AUTO {
499                        return MultiValue::Auto;
500                    }
501                    if raw == azul_css::compact_cache::I16_INITIAL {
502                        return MultiValue::Initial;
503                    }
504                    if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
505                        // Valid value: decode i16 ×10 → px
506                        return MultiValue::Exact(PixelValue::px(raw as f32 / 10.0));
507                    }
508                    // I16_SENTINEL or I16_INHERIT → fall through to slow path
509                }
510            }
511
512            let node_data = &styled_dom.node_data.as_container()[node_id];
513
514            let author_css = styled_dom
515                .css_property_cache
516                .ptr
517                .$cache_method(node_data, &node_id, node_state);
518
519            if let Some(ref val) = author_css {
520                if val.is_auto() {
521                    return MultiValue::Auto;
522                }
523                if let Some(exact) = val.get_property().copied() {
524                    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        }
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
586trait CssPropertyPixelInner {
587    fn get_pixel_inner(&self) -> Option<PixelValue>;
588}
589
590impl 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>
611macro_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        pub fn $fn_name(
615            styled_dom: &StyledDom,
616            node_id: NodeId,
617            node_state: &StyledNodeState,
618        ) -> 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            if node_state.is_normal() {
625                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
626                    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        }
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        pub fn $fn_name(
660            styled_dom: &StyledDom,
661            node_id: NodeId,
662            node_state: &StyledNodeState,
663        ) -> MultiValue<$return_type> {
664            // FAST PATH: compact cache for normal state
665            if node_state.is_normal() {
666                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
667                    let raw = cc.$compact_raw_method(node_id.index());
668                    match raw {
669                        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                            if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
680                                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        }
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        pub fn $fn_name(
715            styled_dom: &StyledDom,
716            node_id: NodeId,
717            node_state: &StyledNodeState,
718        ) -> MultiValue<$return_type> {
719            // FAST PATH: compact cache for normal state
720            if node_state.is_normal() {
721                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
722                    let raw = cc.$compact_raw_method(node_id.index());
723                    match raw {
724                        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                            if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
731                                return MultiValue::Exact(
732                                    <$return_type as azul_css::props::PixelValueTaker>::from_pixel_value(pv)
733                                );
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        }
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
798trait ExtractPropertyValue<T> {
799    fn extract(&self) -> Option<T>;
800}
801
802fn extract_property_value<T>(prop: &azul_css::props::property::CssProperty) -> Option<T>
803where
804    azul_css::props::property::CssProperty: ExtractPropertyValue<T>,
805{
806    prop.extract()
807}
808
809// Implement extraction for all layout types
810
811impl 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
820impl 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
829impl 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
838impl 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
847impl 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
856impl 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
865impl 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
874impl 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
883impl 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
892impl 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
901impl 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
910impl 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
919impl 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
928impl 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
940impl 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
949impl 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
958impl ExtractPropertyValue<PixelValue> for azul_css::props::property::CssProperty {
959    fn extract(&self) -> Option<PixelValue> {
960        self.get_pixel_inner()
961    }
962}
963
964impl 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
973impl 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
982impl 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
991impl 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
1000impl ExtractPropertyValue<StyleFontStyle> for azul_css::props::property::CssProperty {
1001    fn extract(&self) -> Option<StyleFontStyle> {
1002        match self {
1003            Self::FontStyle(CssPropertyValue::Exact(v)) => Some(*v),
1004            _ => None,
1005        }
1006    }
1007}
1008
1009impl ExtractPropertyValue<StyleVisibility> for azul_css::props::property::CssProperty {
1010    fn extract(&self) -> Option<StyleVisibility> {
1011        match self {
1012            Self::Visibility(CssPropertyValue::Exact(v)) => Some(*v),
1013            _ => None,
1014        }
1015    }
1016}
1017
1018impl ExtractPropertyValue<StyleWhiteSpace> for azul_css::props::property::CssProperty {
1019    fn extract(&self) -> Option<StyleWhiteSpace> {
1020        match self {
1021            Self::WhiteSpace(CssPropertyValue::Exact(v)) => Some(*v),
1022            _ => None,
1023        }
1024    }
1025}
1026
1027impl ExtractPropertyValue<StyleDirection> for azul_css::props::property::CssProperty {
1028    fn extract(&self) -> Option<StyleDirection> {
1029        match self {
1030            Self::Direction(CssPropertyValue::Exact(v)) => Some(*v),
1031            _ => None,
1032        }
1033    }
1034}
1035
1036impl ExtractPropertyValue<StyleUnicodeBidi> for azul_css::props::property::CssProperty {
1037    fn extract(&self) -> Option<StyleUnicodeBidi> {
1038        match self {
1039            Self::UnicodeBidi(CssPropertyValue::Exact(v)) => Some(*v),
1040            _ => None,
1041        }
1042    }
1043}
1044
1045impl ExtractPropertyValue<StyleTextBoxTrim> for azul_css::props::property::CssProperty {
1046    fn extract(&self) -> Option<StyleTextBoxTrim> {
1047        match self {
1048            Self::TextBoxTrim(CssPropertyValue::Exact(v)) => Some(*v),
1049            _ => None,
1050        }
1051    }
1052}
1053
1054impl ExtractPropertyValue<StyleTextBoxEdge> for azul_css::props::property::CssProperty {
1055    fn extract(&self) -> Option<StyleTextBoxEdge> {
1056        match self {
1057            Self::TextBoxEdge(CssPropertyValue::Exact(v)) => Some(*v),
1058            _ => None,
1059        }
1060    }
1061}
1062
1063impl ExtractPropertyValue<StyleDominantBaseline> for azul_css::props::property::CssProperty {
1064    fn extract(&self) -> Option<StyleDominantBaseline> {
1065        match self {
1066            Self::DominantBaseline(CssPropertyValue::Exact(v)) => Some(*v),
1067            _ => None,
1068        }
1069    }
1070}
1071
1072impl ExtractPropertyValue<StyleAlignmentBaseline> for azul_css::props::property::CssProperty {
1073    fn extract(&self) -> Option<StyleAlignmentBaseline> {
1074        match self {
1075            Self::AlignmentBaseline(CssPropertyValue::Exact(v)) => Some(*v),
1076            _ => None,
1077        }
1078    }
1079}
1080
1081impl ExtractPropertyValue<StyleInitialLetterAlign> for azul_css::props::property::CssProperty {
1082    fn extract(&self) -> Option<StyleInitialLetterAlign> {
1083        match self {
1084            Self::InitialLetterAlign(CssPropertyValue::Exact(v)) => Some(*v),
1085            _ => None,
1086        }
1087    }
1088}
1089
1090impl ExtractPropertyValue<StyleInitialLetterWrap> for azul_css::props::property::CssProperty {
1091    fn extract(&self) -> Option<StyleInitialLetterWrap> {
1092        match self {
1093            Self::InitialLetterWrap(CssPropertyValue::Exact(v)) => Some(*v),
1094            _ => None,
1095        }
1096    }
1097}
1098
1099impl ExtractPropertyValue<StyleScrollbarGutter> for azul_css::props::property::CssProperty {
1100    fn extract(&self) -> Option<StyleScrollbarGutter> {
1101        match self {
1102            Self::ScrollbarGutter(CssPropertyValue::Exact(v)) => Some(*v),
1103            _ => None,
1104        }
1105    }
1106}
1107
1108impl ExtractPropertyValue<StyleOverflowClipMargin> for azul_css::props::property::CssProperty {
1109    fn extract(&self) -> Option<StyleOverflowClipMargin> {
1110        match self {
1111            Self::OverflowClipMargin(CssPropertyValue::Exact(v)) => Some(*v),
1112            _ => None,
1113        }
1114    }
1115}
1116
1117impl ExtractPropertyValue<StyleVerticalAlign> for azul_css::props::property::CssProperty {
1118    fn extract(&self) -> Option<StyleVerticalAlign> {
1119        match self {
1120            Self::VerticalAlign(CssPropertyValue::Exact(v)) => Some(*v),
1121            _ => None,
1122        }
1123    }
1124}
1125
1126get_css_property!(
1127    get_writing_mode,
1128    get_writing_mode,
1129    LayoutWritingMode,
1130    azul_css::props::property::CssPropertyType::WritingMode,
1131    compact = get_writing_mode
1132);
1133
1134get_css_property!(
1135    get_css_width,
1136    get_width,
1137    LayoutWidth,
1138    azul_css::props::property::CssPropertyType::Width,
1139    compact_u32_dim = get_width_raw, LayoutWidth::Px, LayoutWidth::Auto, LayoutWidth::MinContent, LayoutWidth::MaxContent
1140);
1141
1142get_css_property!(
1143    get_css_height,
1144    get_height,
1145    LayoutHeight,
1146    azul_css::props::property::CssPropertyType::Height,
1147    compact_u32_dim = get_height_raw, LayoutHeight::Px, LayoutHeight::Auto, LayoutHeight::MinContent, LayoutHeight::MaxContent
1148);
1149
1150get_css_property!(
1151    get_wrap,
1152    get_flex_wrap,
1153    LayoutFlexWrap,
1154    azul_css::props::property::CssPropertyType::FlexWrap,
1155    compact = get_flex_wrap
1156);
1157
1158get_css_property!(
1159    get_justify_content,
1160    get_justify_content,
1161    LayoutJustifyContent,
1162    azul_css::props::property::CssPropertyType::JustifyContent,
1163    compact = get_justify_content
1164);
1165
1166get_css_property!(
1167    get_text_align,
1168    get_text_align,
1169    StyleTextAlign,
1170    azul_css::props::property::CssPropertyType::TextAlign,
1171    compact = get_text_align
1172);
1173
1174get_css_property!(
1175    get_float,
1176    get_float,
1177    LayoutFloat,
1178    azul_css::props::property::CssPropertyType::Float,
1179    compact = get_float
1180);
1181
1182get_css_property!(
1183    get_clear,
1184    get_clear,
1185    LayoutClear,
1186    azul_css::props::property::CssPropertyType::Clear,
1187    compact = get_clear
1188);
1189
1190get_css_property!(
1191    get_overflow_x,
1192    get_overflow_x,
1193    LayoutOverflow,
1194    azul_css::props::property::CssPropertyType::OverflowX,
1195    compact = get_overflow_x
1196);
1197
1198get_css_property!(
1199    get_overflow_y,
1200    get_overflow_y,
1201    LayoutOverflow,
1202    azul_css::props::property::CssPropertyType::OverflowY,
1203    compact = get_overflow_y
1204);
1205
1206// +spec:overflow:17654b - overflow-block and overflow-inline logical properties resolve to physical overflow based on writing mode
1207get_css_property!(
1208    get_overflow_block,
1209    get_overflow_block,
1210    LayoutOverflow,
1211    azul_css::props::property::CssPropertyType::OverflowBlock
1212);
1213
1214get_css_property!(
1215    get_overflow_inline,
1216    get_overflow_inline,
1217    LayoutOverflow,
1218    azul_css::props::property::CssPropertyType::OverflowInline
1219);
1220
1221get_css_property!(
1222    get_position,
1223    get_position,
1224    LayoutPosition,
1225    azul_css::props::property::CssPropertyType::Position,
1226    compact = get_position
1227);
1228
1229get_css_property!(
1230    get_css_box_sizing,
1231    get_box_sizing,
1232    LayoutBoxSizing,
1233    azul_css::props::property::CssPropertyType::BoxSizing,
1234    compact = get_box_sizing
1235);
1236
1237get_css_property!(
1238    get_flex_direction,
1239    get_flex_direction,
1240    LayoutFlexDirection,
1241    azul_css::props::property::CssPropertyType::FlexDirection,
1242    compact = get_flex_direction
1243);
1244
1245get_css_property!(
1246    get_align_items,
1247    get_align_items,
1248    LayoutAlignItems,
1249    azul_css::props::property::CssPropertyType::AlignItems,
1250    compact = get_align_items
1251);
1252
1253get_css_property!(
1254    get_align_content,
1255    get_align_content,
1256    LayoutAlignContent,
1257    azul_css::props::property::CssPropertyType::AlignContent,
1258    compact = get_align_content
1259);
1260
1261get_css_property!(
1262    get_font_weight_property,
1263    get_font_weight,
1264    StyleFontWeight,
1265    azul_css::props::property::CssPropertyType::FontWeight,
1266    compact = get_font_weight
1267);
1268
1269get_css_property!(
1270    get_font_style_property,
1271    get_font_style,
1272    StyleFontStyle,
1273    azul_css::props::property::CssPropertyType::FontStyle,
1274    compact = get_font_style
1275);
1276
1277get_css_property!(
1278    get_visibility,
1279    get_visibility,
1280    StyleVisibility,
1281    azul_css::props::property::CssPropertyType::Visibility,
1282    compact = get_visibility
1283);
1284
1285get_css_property!(
1286    get_white_space_property,
1287    get_white_space,
1288    StyleWhiteSpace,
1289    azul_css::props::property::CssPropertyType::WhiteSpace,
1290    compact = get_white_space
1291);
1292
1293// +spec:writing-modes:3af12f - unicode-bidi does not affect direction for layout; we use direction property directly
1294get_css_property!(
1295    get_direction_property,
1296    get_direction,
1297    StyleDirection,
1298    azul_css::props::property::CssPropertyType::Direction,
1299    compact = get_direction
1300);
1301
1302// +spec:display-property:346799 - inline-level elements with unicode-bidi:normal have no effect on text ordering
1303// +spec:writing-modes:3e2632 - unicode-bidi property resolves embedding level for bidi algorithm (LRE/RLE/PDF)
1304// +spec:writing-modes:d2c94f - direction+unicode-bidi properties map to UAX#9 bidirectional algorithm
1305get_css_property!(
1306    get_unicode_bidi_property,
1307    get_unicode_bidi,
1308    StyleUnicodeBidi,
1309    azul_css::props::property::CssPropertyType::UnicodeBidi
1310);
1311
1312// +spec:display-property:db5125 - text-box-trim on inline boxes trims content box to text-box-edge metric
1313// +spec:display-property:dceb24 - text-box-trim on inline boxes: content edges coincide with text baselines
1314get_css_property!(
1315    get_text_box_trim_property,
1316    get_text_box_trim,
1317    StyleTextBoxTrim,
1318    azul_css::props::property::CssPropertyType::TextBoxTrim
1319);
1320
1321get_css_property!(
1322    get_text_box_edge_property,
1323    get_text_box_edge,
1324    StyleTextBoxEdge,
1325    azul_css::props::property::CssPropertyType::TextBoxEdge
1326);
1327
1328get_css_property!(
1329    get_dominant_baseline_property,
1330    get_dominant_baseline,
1331    StyleDominantBaseline,
1332    azul_css::props::property::CssPropertyType::DominantBaseline
1333);
1334
1335get_css_property!(
1336    get_alignment_baseline_property,
1337    get_alignment_baseline,
1338    StyleAlignmentBaseline,
1339    azul_css::props::property::CssPropertyType::AlignmentBaseline
1340);
1341
1342get_css_property!(
1343    get_initial_letter_align_property,
1344    get_initial_letter_align,
1345    StyleInitialLetterAlign,
1346    azul_css::props::property::CssPropertyType::InitialLetterAlign
1347);
1348
1349get_css_property!(
1350    get_initial_letter_wrap_property,
1351    get_initial_letter_wrap,
1352    StyleInitialLetterWrap,
1353    azul_css::props::property::CssPropertyType::InitialLetterWrap
1354);
1355
1356// +spec:overflow:5d15e2 - block-start/block-end scrollbar gutter follows same rules as inline gutters when auto
1357//
1358// Hand-rolled fast path: 99% of nodes don't set scrollbar-gutter, and the
1359// default is `auto`. The compact cache stores the enum in 2 bits of
1360// tier2_cold.hot_flags, so we can return the answer without a cascade walk.
1361pub fn get_scrollbar_gutter_property(
1362    styled_dom: &StyledDom,
1363    node_id: NodeId,
1364    node_state: &StyledNodeState,
1365) -> MultiValue<StyleScrollbarGutter> {
1366    // FAST PATH: 2-bit enum in hot_flags
1367    if node_state.is_normal() {
1368        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1369            let bits = cc.get_scrollbar_gutter_bits(node_id.index());
1370            let val = match bits {
1371                azul_css::compact_cache::SCROLLBAR_GUTTER_AUTO => StyleScrollbarGutter::Auto,
1372                azul_css::compact_cache::SCROLLBAR_GUTTER_STABLE => StyleScrollbarGutter::Stable,
1373                azul_css::compact_cache::SCROLLBAR_GUTTER_BOTH_EDGES => StyleScrollbarGutter::StableBothEdges,
1374                _ => StyleScrollbarGutter::Auto,
1375            };
1376            return MultiValue::Exact(val);
1377        }
1378    }
1379
1380    // SLOW PATH: cascade resolution for pseudo-states or missing cache
1381    let node_data = &styled_dom.node_data.as_container()[node_id];
1382    let author_css = styled_dom
1383        .css_property_cache
1384        .ptr
1385        .get_scrollbar_gutter(node_data, &node_id, node_state);
1386    if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
1387        return MultiValue::Exact(val);
1388    }
1389    MultiValue::Auto
1390}
1391
1392get_css_property!(
1393    get_overflow_clip_margin_property,
1394    get_overflow_clip_margin,
1395    StyleOverflowClipMargin,
1396    azul_css::props::property::CssPropertyType::OverflowClipMargin
1397);
1398
1399get_css_property!(
1400    get_object_fit_property,
1401    get_object_fit,
1402    StyleObjectFit,
1403    azul_css::props::property::CssPropertyType::ObjectFit
1404);
1405
1406// +spec:writing-modes:257296 - text-orientation getter for vertical typesetting (upright/sideways)
1407//
1408// Hand-rolled (not macro-generated) to attach a negative fast-path: most
1409// nodes have no text-orientation declared (default = Mixed), so we avoid a
1410// cascade walk per fc.rs call (which is called ~2× per node).
1411pub fn get_text_orientation_property(
1412    styled_dom: &StyledDom,
1413    node_id: NodeId,
1414    node_state: &StyledNodeState,
1415) -> MultiValue<StyleTextOrientation> {
1416    if node_state.is_normal() {
1417        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1418            if !cc.has_text_orientation(node_id.index()) {
1419                return MultiValue::Auto;
1420            }
1421        }
1422    }
1423    let node_data = &styled_dom.node_data.as_container()[node_id];
1424    if let Some(val) = styled_dom
1425        .css_property_cache
1426        .ptr
1427        .get_text_orientation(node_data, &node_id, node_state)
1428        .and_then(|v| v.get_property().cloned())
1429    {
1430        return MultiValue::Exact(val);
1431    }
1432    let ua = azul_core::ua_css::get_ua_property(
1433        &node_data.node_type,
1434        azul_css::props::property::CssPropertyType::TextOrientation,
1435    );
1436    if let Some(ua_prop) = ua {
1437        if let Some(val) = extract_property_value::<StyleTextOrientation>(ua_prop) {
1438            return MultiValue::Exact(val);
1439        }
1440    }
1441    MultiValue::Auto
1442}
1443
1444get_css_property!(
1445    get_object_position_property,
1446    get_object_position,
1447    StyleObjectPosition,
1448    azul_css::props::property::CssPropertyType::ObjectPosition
1449);
1450
1451get_css_property!(
1452    get_aspect_ratio_property,
1453    get_aspect_ratio,
1454    StyleAspectRatio,
1455    azul_css::props::property::CssPropertyType::AspectRatio
1456);
1457
1458// NOTE: vertical-align does NOT use the compact cache because the compact cache
1459// only stores keyword variants (3 bits = 8 values) and silently drops
1460// Percentage/Length values by mapping them to Baseline. Always use the slow path.
1461pub fn get_vertical_align_property(
1462    styled_dom: &StyledDom,
1463    node_id: NodeId,
1464    node_state: &StyledNodeState,
1465) -> MultiValue<StyleVerticalAlign> {
1466    let node_data = &styled_dom.node_data.as_container()[node_id];
1467
1468    let author_css = styled_dom
1469        .css_property_cache
1470        .ptr
1471        .get_vertical_align(node_data, &node_id, node_state);
1472
1473    if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
1474        return MultiValue::Exact(val);
1475    }
1476
1477    let ua_css = azul_core::ua_css::get_ua_property(
1478        &node_data.node_type,
1479        azul_css::props::property::CssPropertyType::VerticalAlign,
1480    );
1481
1482    if let Some(ua_prop) = ua_css {
1483        if let Some(val) = extract_property_value::<StyleVerticalAlign>(ua_prop) {
1484            return MultiValue::Exact(val);
1485        }
1486    }
1487
1488    MultiValue::Auto
1489}
1490// Complex Property Getters
1491
1492/// Get border radius for all four corners (raw CSS property values)
1493pub fn get_style_border_radius(
1494    styled_dom: &StyledDom,
1495    node_id: NodeId,
1496    node_state: &StyledNodeState,
1497) -> azul_css::props::style::border_radius::StyleBorderRadius {
1498    use azul_css::props::basic::pixel::PixelValue;
1499    // FAST PATH: all four corners live in tier2_cold as i16 px × 10. The
1500    // common case (no rounded corners anywhere) reads four bytes and bails.
1501    if node_state.is_normal() {
1502        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1503            let idx = node_id.index();
1504            let decode = |raw: i16| -> PixelValue {
1505                if raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
1506                    PixelValue::px(0.0)
1507                } else {
1508                    PixelValue::px(raw as f32 / 10.0)
1509                }
1510            };
1511            return StyleBorderRadius {
1512                top_left: decode(cc.get_border_top_left_radius_raw(idx)),
1513                top_right: decode(cc.get_border_top_right_radius_raw(idx)),
1514                bottom_right: decode(cc.get_border_bottom_right_radius_raw(idx)),
1515                bottom_left: decode(cc.get_border_bottom_left_radius_raw(idx)),
1516            };
1517        }
1518    }
1519    let node_data = &styled_dom.node_data.as_container()[node_id];
1520
1521    let top_left = styled_dom
1522        .css_property_cache
1523        .ptr
1524        .get_border_top_left_radius(node_data, &node_id, node_state)
1525        .and_then(|br| br.get_property_or_default())
1526        .map(|v| v.inner)
1527        .unwrap_or_default();
1528
1529    let top_right = styled_dom
1530        .css_property_cache
1531        .ptr
1532        .get_border_top_right_radius(node_data, &node_id, node_state)
1533        .and_then(|br| br.get_property_or_default())
1534        .map(|v| v.inner)
1535        .unwrap_or_default();
1536
1537    let bottom_right = styled_dom
1538        .css_property_cache
1539        .ptr
1540        .get_border_bottom_right_radius(node_data, &node_id, node_state)
1541        .and_then(|br| br.get_property_or_default())
1542        .map(|v| v.inner)
1543        .unwrap_or_default();
1544
1545    let bottom_left = styled_dom
1546        .css_property_cache
1547        .ptr
1548        .get_border_bottom_left_radius(node_data, &node_id, node_state)
1549        .and_then(|br| br.get_property_or_default())
1550        .map(|v| v.inner)
1551        .unwrap_or_default();
1552
1553    StyleBorderRadius {
1554        top_left,
1555        top_right,
1556        bottom_right,
1557        bottom_left,
1558    }
1559}
1560
1561/// Get border radius for all four corners (resolved to pixels)
1562///
1563/// # Arguments
1564/// * `element_size` - The element's own size (width × height) for % resolution. According to CSS
1565///   spec, border-radius % uses element's own dimensions.
1566pub fn get_border_radius(
1567    styled_dom: &StyledDom,
1568    node_id: NodeId,
1569    node_state: &StyledNodeState,
1570    element_size: PhysicalSizeImport,
1571    viewport_size: LogicalSize,
1572) -> BorderRadius {
1573    use azul_css::props::basic::{PhysicalSize, PropertyContext, ResolutionContext};
1574
1575    // FAST PATH: all four corners as i16 px × 10 in tier2_cold. The
1576    // overwhelmingly common case (no rounded corners) reads four bytes and
1577    // returns zeros without a cascade walk.
1578    if node_state.is_normal() {
1579        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1580            let idx = node_id.index();
1581            let tl = cc.get_border_top_left_radius_raw(idx);
1582            let tr = cc.get_border_top_right_radius_raw(idx);
1583            let br = cc.get_border_bottom_right_radius_raw(idx);
1584            let bl = cc.get_border_bottom_left_radius_raw(idx);
1585            // sentinel = "unset" = 0 px (no corner radius)
1586            let thresh = azul_css::compact_cache::I16_SENTINEL_THRESHOLD;
1587            let decode = |raw: i16| -> f32 {
1588                if raw >= thresh { 0.0 } else { raw as f32 / 10.0 }
1589            };
1590            return BorderRadius {
1591                top_left: decode(tl),
1592                top_right: decode(tr),
1593                bottom_right: decode(br),
1594                bottom_left: decode(bl),
1595            };
1596        }
1597    }
1598
1599    let node_data = &styled_dom.node_data.as_container()[node_id];
1600
1601    // Get font sizes for em/rem resolution
1602    let element_font_size = get_element_font_size(styled_dom, node_id, node_state);
1603    let parent_font_size = styled_dom
1604        .node_hierarchy
1605        .as_container()
1606        .get(node_id)
1607        .and_then(|node| node.parent_id())
1608        .map(|p| get_element_font_size(styled_dom, p, node_state))
1609        .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE);
1610    let root_font_size = get_root_font_size(styled_dom, node_state);
1611
1612    // Create resolution context
1613    let context = ResolutionContext {
1614        element_font_size,
1615        parent_font_size,
1616        root_font_size,
1617        containing_block_size: PhysicalSize::new(0.0, 0.0), // Not used for border-radius
1618        element_size: Some(PhysicalSize::new(element_size.width, element_size.height)),
1619        viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
1620    };
1621
1622    let top_left = styled_dom
1623        .css_property_cache
1624        .ptr
1625        .get_border_top_left_radius(node_data, &node_id, node_state)
1626        .and_then(|br| br.get_property().cloned())
1627        .unwrap_or_default();
1628
1629    let top_right = styled_dom
1630        .css_property_cache
1631        .ptr
1632        .get_border_top_right_radius(node_data, &node_id, node_state)
1633        .and_then(|br| br.get_property().cloned())
1634        .unwrap_or_default();
1635
1636    let bottom_right = styled_dom
1637        .css_property_cache
1638        .ptr
1639        .get_border_bottom_right_radius(node_data, &node_id, node_state)
1640        .and_then(|br| br.get_property().cloned())
1641        .unwrap_or_default();
1642
1643    let bottom_left = styled_dom
1644        .css_property_cache
1645        .ptr
1646        .get_border_bottom_left_radius(node_data, &node_id, node_state)
1647        .and_then(|br| br.get_property().cloned())
1648        .unwrap_or_default();
1649
1650    BorderRadius {
1651        top_left: top_left
1652            .inner
1653            .resolve_with_context(&context, PropertyContext::BorderRadius),
1654        top_right: top_right
1655            .inner
1656            .resolve_with_context(&context, PropertyContext::BorderRadius),
1657        bottom_right: bottom_right
1658            .inner
1659            .resolve_with_context(&context, PropertyContext::BorderRadius),
1660        bottom_left: bottom_left
1661            .inner
1662            .resolve_with_context(&context, PropertyContext::BorderRadius),
1663    }
1664}
1665
1666// +spec:stacking-contexts:a93e62 - stack level from z-index for stacking context ordering
1667// +spec:stacking-contexts:ae50ae - z-index specifies stack level; auto resolves to 0 (inherited from parent stacking context)
1668/// Get z-index for stacking context ordering.
1669///
1670/// Returns the resolved integer z-index value:
1671/// - `z-index: auto` → 0 (participates in parent's stacking context)
1672/// - `z-index: <integer>` → that integer value
1673pub fn get_z_index(styled_dom: &StyledDom, node_id: Option<NodeId>) -> i32 {
1674    use azul_css::props::layout::position::LayoutZIndex;
1675
1676    let node_id = match node_id {
1677        Some(id) => id,
1678        None => return 0,
1679    };
1680
1681    let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
1682
1683    // FAST PATH: compact cache for normal state
1684    if node_state.is_normal() {
1685        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1686            let raw = cc.get_z_index(node_id.index());
1687            if raw == azul_css::compact_cache::I16_AUTO {
1688                return 0;
1689            }
1690            if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
1691                return raw as i32;
1692            }
1693            // I16_SENTINEL → fall through to slow path
1694        }
1695    }
1696
1697    // SLOW PATH
1698    let node_data = &styled_dom.node_data.as_container()[node_id];
1699
1700    styled_dom
1701        .css_property_cache
1702        .ptr
1703        .get_z_index(node_data, &node_id, &node_state)
1704        .and_then(|v| v.get_property())
1705        .map(|z| match z {
1706            LayoutZIndex::Auto => 0,
1707            LayoutZIndex::Integer(i) => *i,
1708        })
1709        .unwrap_or(0)
1710}
1711
1712// +spec:positioning:c041c4 - positioned elements with z-index != auto establish stacking contexts
1713// z-index:<integer> ALWAYS establishes new stacking context on positioned elements
1714/// Returns true if z-index is `auto` (the initial value), false if it's an explicit `<integer>`.
1715/// This distinction matters for stacking context creation per §9.9.1.
1716pub fn is_z_index_auto(styled_dom: &StyledDom, node_id: Option<NodeId>) -> bool {
1717    use azul_css::props::layout::position::LayoutZIndex;
1718
1719    let node_id = match node_id {
1720        Some(id) => id,
1721        None => return true,
1722    };
1723
1724    let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
1725
1726    // FAST PATH: compact cache for normal state
1727    if node_state.is_normal() {
1728        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1729            let raw = cc.get_z_index(node_id.index());
1730            if raw == azul_css::compact_cache::I16_AUTO {
1731                return true;
1732            }
1733            if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
1734                return false; // explicit integer
1735            }
1736            // I16_SENTINEL → fall through to slow path
1737        }
1738    }
1739
1740    // SLOW PATH
1741    let node_data = &styled_dom.node_data.as_container()[node_id];
1742
1743    styled_dom
1744        .css_property_cache
1745        .ptr
1746        .get_z_index(node_data, &node_id, &node_state)
1747        .and_then(|v| v.get_property())
1748        .map(|z| matches!(z, LayoutZIndex::Auto))
1749        .unwrap_or(true) // no value = auto
1750}
1751
1752// Rendering Property Getters
1753
1754/// Information about background color for a node
1755///
1756/// # CSS Background Propagation (Special Case for HTML Root)
1757///
1758/// According to CSS Backgrounds and Borders Module Level 3, Section "The Canvas Background
1759/// and the HTML `<body>` Element":
1760///
1761/// For HTML documents where the root element is `<html>`, if the computed value of
1762/// `background-image` on the root element is `none` AND its `background-color` is `transparent`,
1763/// user agents **must propagate** the computed values of the background properties from the
1764/// first `<body>` child element to the root element.
1765///
1766/// This behavior exists for backwards compatibility with older HTML where backgrounds were
1767/// typically set on `<body>` using `bgcolor` attributes, and ensures that the `<body>`
1768/// background covers the entire viewport/canvas even when `<body>` itself has constrained
1769/// dimensions.
1770///
1771/// Implementation: When requesting the background of an `<html>` node, we first check if it
1772/// has a transparent background with no image. If so, we look for a `<body>` child and use
1773/// its background instead.
1774pub fn get_background_color(
1775    styled_dom: &StyledDom,
1776    node_id: NodeId,
1777    node_state: &StyledNodeState,
1778) -> ColorU {
1779    let node_data = &styled_dom.node_data.as_container()[node_id];
1780    let cache = &styled_dom.css_property_cache.ptr;
1781
1782    // Fast path: Get this node's background.
1783    // Negative fast path: if compact cache says `has_background == 0` on a
1784    // normal-state node, skip the cascade walk entirely. Only declared backgrounds
1785    // set the bit, so `false` is a safe "unconditionally transparent" signal.
1786    let get_node_bg = |nid: NodeId, ndata: &azul_core::dom::NodeData, state: &StyledNodeState| {
1787        if state.is_normal() {
1788            if let Some(ref cc) = cache.compact_cache {
1789                if !cc.has_background(nid.index()) {
1790                    return None;
1791                }
1792            }
1793        }
1794        cache
1795            .get_background_content(ndata, &nid, state)
1796            .and_then(|bg| bg.get_property())
1797            .and_then(|bg_vec| bg_vec.get(0).cloned())
1798            .and_then(|first_bg| match &first_bg {
1799                azul_css::props::style::StyleBackgroundContent::Color(color) => Some(color.clone()),
1800                azul_css::props::style::StyleBackgroundContent::Image(_) => None, // Has image, not transparent
1801                _ => None,
1802            })
1803    };
1804
1805    let own_bg = get_node_bg(node_id, node_data, node_state);
1806
1807    // CSS Background Propagation: Special handling for <html> root element
1808    // Only check propagation if this is an Html node AND has transparent background (no
1809    // color/image)
1810    if !matches!(node_data.node_type, NodeType::Html) || own_bg.is_some() {
1811        // Not Html or has its own background - return own background or transparent
1812        return own_bg.unwrap_or(ColorU {
1813            r: 0,
1814            g: 0,
1815            b: 0,
1816            a: 0,
1817        });
1818    }
1819
1820    // Html node with transparent background - check if we should propagate from <body>
1821    let first_child = styled_dom
1822        .node_hierarchy
1823        .as_container()
1824        .get(node_id)
1825        .and_then(|node| node.first_child_id(node_id));
1826
1827    let Some(first_child) = first_child else {
1828        return ColorU {
1829            r: 0,
1830            g: 0,
1831            b: 0,
1832            a: 0,
1833        };
1834    };
1835
1836    let first_child_data = &styled_dom.node_data.as_container()[first_child];
1837
1838    // Check if first child is <body>
1839    if !matches!(first_child_data.node_type, NodeType::Body) {
1840        return ColorU {
1841            r: 0,
1842            g: 0,
1843            b: 0,
1844            a: 0,
1845        };
1846    }
1847
1848    // Propagate <body>'s background to <html> (canvas)
1849    let first_child_state = &styled_dom.styled_nodes.as_container()[first_child].styled_node_state;
1850    get_node_bg(first_child, first_child_data, first_child_state).unwrap_or(ColorU {
1851        r: 0,
1852        g: 0,
1853        b: 0,
1854        a: 0,
1855    })
1856}
1857
1858/// Returns all background content layers for a node (colors, gradients, images).
1859/// This is used for rendering backgrounds that may include linear/radial/conic gradients.
1860///
1861/// CSS Background Propagation (CSS Backgrounds 3, Section 2.11.2):
1862/// For HTML documents, if the root `<html>` element has no background (transparent with no image),
1863/// propagate the background from the first `<body>` child element.
1864pub fn get_background_contents(
1865    styled_dom: &StyledDom,
1866    node_id: NodeId,
1867    node_state: &StyledNodeState,
1868) -> Vec<azul_css::props::style::StyleBackgroundContent> {
1869    use azul_core::dom::NodeType;
1870    use azul_css::props::style::StyleBackgroundContent;
1871
1872    let node_data = &styled_dom.node_data.as_container()[node_id];
1873    let cache = &styled_dom.css_property_cache.ptr;
1874
1875    // Helper to get backgrounds for a node.
1876    // Negative fast path: if compact cache says `has_background == 0` on a normal
1877    // pseudo-state node, return empty without walking the cascade.
1878    let get_node_backgrounds =
1879        |nid: NodeId, ndata: &azul_core::dom::NodeData, state: &StyledNodeState|
1880        -> Vec<StyleBackgroundContent> {
1881            if state.is_normal() {
1882                if let Some(ref cc) = cache.compact_cache {
1883                    if !cc.has_background(nid.index()) {
1884                        return Vec::new();
1885                    }
1886                }
1887            }
1888            cache
1889                .get_background_content(ndata, &nid, state)
1890                .and_then(|bg| bg.get_property())
1891                .map(|bg_vec| bg_vec.iter().cloned().collect())
1892                .unwrap_or_default()
1893        };
1894
1895    let own_backgrounds = get_node_backgrounds(node_id, node_data, node_state);
1896
1897    // CSS Background Propagation: Special handling for <html> root element
1898    // Only check propagation if this is an Html node AND has no backgrounds
1899    if !matches!(node_data.node_type, NodeType::Html) || !own_backgrounds.is_empty() {
1900        return own_backgrounds;
1901    }
1902
1903    // Html node with no backgrounds - check if we should propagate from <body>
1904    let first_child = styled_dom
1905        .node_hierarchy
1906        .as_container()
1907        .get(node_id)
1908        .and_then(|node| node.first_child_id(node_id));
1909
1910    let Some(first_child) = first_child else {
1911        return own_backgrounds;
1912    };
1913
1914    let first_child_data = &styled_dom.node_data.as_container()[first_child];
1915
1916    // Check if first child is <body>
1917    if !matches!(first_child_data.node_type, NodeType::Body) {
1918        return own_backgrounds;
1919    }
1920
1921    // Propagate <body>'s backgrounds to <html> (canvas)
1922    let first_child_state = &styled_dom.styled_nodes.as_container()[first_child].styled_node_state;
1923    get_node_backgrounds(first_child, first_child_data, first_child_state)
1924}
1925
1926/// Information about border rendering
1927pub struct BorderInfo {
1928    pub widths: crate::solver3::display_list::StyleBorderWidths,
1929    pub colors: crate::solver3::display_list::StyleBorderColors,
1930    pub styles: crate::solver3::display_list::StyleBorderStyles,
1931}
1932
1933pub fn get_border_info(
1934    styled_dom: &StyledDom,
1935    node_id: NodeId,
1936    node_state: &StyledNodeState,
1937) -> BorderInfo {
1938    use crate::solver3::display_list::{StyleBorderColors, StyleBorderStyles, StyleBorderWidths};
1939    use azul_css::css::CssPropertyValue;
1940    use azul_css::props::basic::color::ColorU;
1941    use azul_css::props::basic::pixel::PixelValue;
1942    use azul_css::props::style::{
1943        LayoutBorderTopWidth, LayoutBorderRightWidth,
1944        LayoutBorderBottomWidth, LayoutBorderLeftWidth,
1945    };
1946    use azul_css::props::style::border::{
1947        BorderStyle, StyleBorderTopColor, StyleBorderRightColor,
1948        StyleBorderBottomColor, StyleBorderLeftColor,
1949        StyleBorderTopStyle, StyleBorderRightStyle,
1950        StyleBorderBottomStyle, StyleBorderLeftStyle,
1951    };
1952
1953    // FAST PATH: compact cache for normal state
1954    if node_state.is_normal() {
1955        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1956            let idx = node_id.index();
1957
1958            // Border widths: decode from compact i16 (resolved px × 10).
1959            // Previously this block called the slow convenience getters
1960            // despite being in the "fast path" branch — 2014 slow walks
1961            // per width × 4 widths per cold excel.html layout. Fixed
1962            // 2026-04-17.
1963            let make_width_px = |raw: i16| -> Option<PixelValue> {
1964                if raw == azul_css::compact_cache::I16_AUTO
1965                    || raw == azul_css::compact_cache::I16_INITIAL
1966                    || raw >= azul_css::compact_cache::I16_SENTINEL_THRESHOLD
1967                {
1968                    None
1969                } else {
1970                    Some(PixelValue::px(raw as f32 / 10.0))
1971                }
1972            };
1973            let widths = StyleBorderWidths {
1974                top: make_width_px(cc.get_border_top_width_raw(idx))
1975                    .map(|px| CssPropertyValue::Exact(LayoutBorderTopWidth { inner: px })),
1976                right: make_width_px(cc.get_border_right_width_raw(idx))
1977                    .map(|px| CssPropertyValue::Exact(LayoutBorderRightWidth { inner: px })),
1978                bottom: make_width_px(cc.get_border_bottom_width_raw(idx))
1979                    .map(|px| CssPropertyValue::Exact(LayoutBorderBottomWidth { inner: px })),
1980                left: make_width_px(cc.get_border_left_width_raw(idx))
1981                    .map(|px| CssPropertyValue::Exact(LayoutBorderLeftWidth { inner: px })),
1982            };
1983
1984            // Border colors from compact cache
1985            let make_color = |raw: u32| -> Option<ColorU> {
1986                if raw == 0 { None } else {
1987                    Some(ColorU {
1988                        r: ((raw >> 24) & 0xFF) as u8,
1989                        g: ((raw >> 16) & 0xFF) as u8,
1990                        b: ((raw >> 8) & 0xFF) as u8,
1991                        a: (raw & 0xFF) as u8,
1992                    })
1993                }
1994            };
1995
1996            let colors = StyleBorderColors {
1997                top: make_color(cc.get_border_top_color_raw(idx))
1998                    .map(|c| CssPropertyValue::Exact(StyleBorderTopColor { inner: c })),
1999                right: make_color(cc.get_border_right_color_raw(idx))
2000                    .map(|c| CssPropertyValue::Exact(StyleBorderRightColor { inner: c })),
2001                bottom: make_color(cc.get_border_bottom_color_raw(idx))
2002                    .map(|c| CssPropertyValue::Exact(StyleBorderBottomColor { inner: c })),
2003                left: make_color(cc.get_border_left_color_raw(idx))
2004                    .map(|c| CssPropertyValue::Exact(StyleBorderLeftColor { inner: c })),
2005            };
2006
2007            // Border styles from compact cache
2008            let styles = StyleBorderStyles {
2009                top: Some(CssPropertyValue::Exact(StyleBorderTopStyle {
2010                    inner: cc.get_border_top_style(idx),
2011                })),
2012                right: Some(CssPropertyValue::Exact(StyleBorderRightStyle {
2013                    inner: cc.get_border_right_style(idx),
2014                })),
2015                bottom: Some(CssPropertyValue::Exact(StyleBorderBottomStyle {
2016                    inner: cc.get_border_bottom_style(idx),
2017                })),
2018                left: Some(CssPropertyValue::Exact(StyleBorderLeftStyle {
2019                    inner: cc.get_border_left_style(idx),
2020                })),
2021            };
2022
2023            return BorderInfo { widths, colors, styles };
2024        }
2025    }
2026
2027    // SLOW PATH: full cascade
2028    let node_data = &styled_dom.node_data.as_container()[node_id];
2029
2030    // Get all border widths
2031    let widths = StyleBorderWidths {
2032        top: styled_dom
2033            .css_property_cache
2034            .ptr
2035            .get_border_top_width(node_data, &node_id, node_state)
2036            .cloned(),
2037        right: styled_dom
2038            .css_property_cache
2039            .ptr
2040            .get_border_right_width(node_data, &node_id, node_state)
2041            .cloned(),
2042        bottom: styled_dom
2043            .css_property_cache
2044            .ptr
2045            .get_border_bottom_width(node_data, &node_id, node_state)
2046            .cloned(),
2047        left: styled_dom
2048            .css_property_cache
2049            .ptr
2050            .get_border_left_width(node_data, &node_id, node_state)
2051            .cloned(),
2052    };
2053
2054    // Get all border colors
2055    let colors = StyleBorderColors {
2056        top: styled_dom
2057            .css_property_cache
2058            .ptr
2059            .get_border_top_color(node_data, &node_id, node_state)
2060            .cloned(),
2061        right: styled_dom
2062            .css_property_cache
2063            .ptr
2064            .get_border_right_color(node_data, &node_id, node_state)
2065            .cloned(),
2066        bottom: styled_dom
2067            .css_property_cache
2068            .ptr
2069            .get_border_bottom_color(node_data, &node_id, node_state)
2070            .cloned(),
2071        left: styled_dom
2072            .css_property_cache
2073            .ptr
2074            .get_border_left_color(node_data, &node_id, node_state)
2075            .cloned(),
2076    };
2077
2078    // Get all border styles
2079    let styles = StyleBorderStyles {
2080        top: styled_dom
2081            .css_property_cache
2082            .ptr
2083            .get_border_top_style(node_data, &node_id, node_state)
2084            .cloned(),
2085        right: styled_dom
2086            .css_property_cache
2087            .ptr
2088            .get_border_right_style(node_data, &node_id, node_state)
2089            .cloned(),
2090        bottom: styled_dom
2091            .css_property_cache
2092            .ptr
2093            .get_border_bottom_style(node_data, &node_id, node_state)
2094            .cloned(),
2095        left: styled_dom
2096            .css_property_cache
2097            .ptr
2098            .get_border_left_style(node_data, &node_id, node_state)
2099            .cloned(),
2100    };
2101
2102    BorderInfo {
2103        widths,
2104        colors,
2105        styles,
2106    }
2107}
2108
2109/// Convert BorderInfo to InlineBorderInfo for inline elements
2110///
2111/// This resolves the CSS property values to concrete pixel values and colors
2112/// that can be used during text rendering.
2113pub fn get_inline_border_info(
2114    styled_dom: &StyledDom,
2115    node_id: NodeId,
2116    node_state: &StyledNodeState,
2117    border_info: &BorderInfo,
2118) -> Option<crate::text3::cache::InlineBorderInfo> {
2119    use crate::text3::cache::InlineBorderInfo;
2120
2121    // Helper to extract pixel value from border width
2122    fn get_border_width_px(
2123        width: &Option<
2124            azul_css::css::CssPropertyValue<azul_css::props::style::border::LayoutBorderTopWidth>,
2125        >,
2126    ) -> f32 {
2127        width
2128            .as_ref()
2129            .and_then(|v| v.get_property())
2130            .map(|w| w.inner.number.get())
2131            .unwrap_or(0.0)
2132    }
2133
2134    fn get_border_width_px_right(
2135        width: &Option<
2136            azul_css::css::CssPropertyValue<azul_css::props::style::border::LayoutBorderRightWidth>,
2137        >,
2138    ) -> f32 {
2139        width
2140            .as_ref()
2141            .and_then(|v| v.get_property())
2142            .map(|w| w.inner.number.get())
2143            .unwrap_or(0.0)
2144    }
2145
2146    fn get_border_width_px_bottom(
2147        width: &Option<
2148            azul_css::css::CssPropertyValue<
2149                azul_css::props::style::border::LayoutBorderBottomWidth,
2150            >,
2151        >,
2152    ) -> f32 {
2153        width
2154            .as_ref()
2155            .and_then(|v| v.get_property())
2156            .map(|w| w.inner.number.get())
2157            .unwrap_or(0.0)
2158    }
2159
2160    fn get_border_width_px_left(
2161        width: &Option<
2162            azul_css::css::CssPropertyValue<azul_css::props::style::border::LayoutBorderLeftWidth>,
2163        >,
2164    ) -> f32 {
2165        width
2166            .as_ref()
2167            .and_then(|v| v.get_property())
2168            .map(|w| w.inner.number.get())
2169            .unwrap_or(0.0)
2170    }
2171
2172    // Helper to extract color from border color
2173    fn get_border_color_top(
2174        color: &Option<
2175            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderTopColor>,
2176        >,
2177    ) -> ColorU {
2178        color
2179            .as_ref()
2180            .and_then(|v| v.get_property())
2181            .map(|c| c.inner)
2182            .unwrap_or(ColorU::BLACK)
2183    }
2184
2185    fn get_border_color_right(
2186        color: &Option<
2187            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderRightColor>,
2188        >,
2189    ) -> ColorU {
2190        color
2191            .as_ref()
2192            .and_then(|v| v.get_property())
2193            .map(|c| c.inner)
2194            .unwrap_or(ColorU::BLACK)
2195    }
2196
2197    fn get_border_color_bottom(
2198        color: &Option<
2199            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderBottomColor>,
2200        >,
2201    ) -> ColorU {
2202        color
2203            .as_ref()
2204            .and_then(|v| v.get_property())
2205            .map(|c| c.inner)
2206            .unwrap_or(ColorU::BLACK)
2207    }
2208
2209    fn get_border_color_left(
2210        color: &Option<
2211            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderLeftColor>,
2212        >,
2213    ) -> ColorU {
2214        color
2215            .as_ref()
2216            .and_then(|v| v.get_property())
2217            .map(|c| c.inner)
2218            .unwrap_or(ColorU::BLACK)
2219    }
2220
2221    // Extract border-radius (simplified - uses the average of all corners if uniform)
2222    fn get_border_radius_px(
2223        styled_dom: &StyledDom,
2224        node_id: NodeId,
2225        node_state: &StyledNodeState,
2226    ) -> Option<f32> {
2227        let node_data = &styled_dom.node_data.as_container()[node_id];
2228
2229        let top_left = styled_dom
2230            .css_property_cache
2231            .ptr
2232            .get_border_top_left_radius(node_data, &node_id, node_state)
2233            .and_then(|br| br.get_property().cloned())
2234            .map(|v| v.inner.number.get());
2235
2236        let top_right = styled_dom
2237            .css_property_cache
2238            .ptr
2239            .get_border_top_right_radius(node_data, &node_id, node_state)
2240            .and_then(|br| br.get_property().cloned())
2241            .map(|v| v.inner.number.get());
2242
2243        let bottom_left = styled_dom
2244            .css_property_cache
2245            .ptr
2246            .get_border_bottom_left_radius(node_data, &node_id, node_state)
2247            .and_then(|br| br.get_property().cloned())
2248            .map(|v| v.inner.number.get());
2249
2250        let bottom_right = styled_dom
2251            .css_property_cache
2252            .ptr
2253            .get_border_bottom_right_radius(node_data, &node_id, node_state)
2254            .and_then(|br| br.get_property().cloned())
2255            .map(|v| v.inner.number.get());
2256
2257        // If any radius is defined, use the maximum (for inline, uniform radius is most common)
2258        let radii: Vec<f32> = [top_left, top_right, bottom_left, bottom_right]
2259            .into_iter()
2260            .filter_map(|r| r)
2261            .collect();
2262
2263        if radii.is_empty() {
2264            None
2265        } else {
2266            Some(radii.into_iter().fold(0.0f32, |a, b| a.max(b)))
2267        }
2268    }
2269
2270    let top = get_border_width_px(&border_info.widths.top);
2271    let right = get_border_width_px_right(&border_info.widths.right);
2272    let bottom = get_border_width_px_bottom(&border_info.widths.bottom);
2273    let left = get_border_width_px_left(&border_info.widths.left);
2274
2275    // Fetch padding values for inline elements
2276    fn resolve_padding(mv: MultiValue<PixelValue>) -> f32 {
2277        match mv {
2278            MultiValue::Exact(pv) => {
2279                super::calc::resolve_pixel_value(&pv, 0.0, DEFAULT_FONT_SIZE, DEFAULT_FONT_SIZE)
2280            }
2281            _ => 0.0,
2282        }
2283    }
2284
2285    let p_top = resolve_padding(get_css_padding_top(styled_dom, node_id, node_state));
2286    let p_right = resolve_padding(get_css_padding_right(styled_dom, node_id, node_state));
2287    let p_bottom = resolve_padding(get_css_padding_bottom(styled_dom, node_id, node_state));
2288    let p_left = resolve_padding(get_css_padding_left(styled_dom, node_id, node_state));
2289
2290    // Only return Some if there's actually a border or padding
2291    let has_border = top > 0.0 || right > 0.0 || bottom > 0.0 || left > 0.0;
2292    let has_padding = p_top > 0.0 || p_right > 0.0 || p_bottom > 0.0 || p_left > 0.0;
2293    if !has_border && !has_padding {
2294        return None;
2295    }
2296
2297    // CSS 2.2 §8.6: detect direction for visual-order border/padding rendering in bidi
2298    let is_rtl = matches!(
2299        get_direction_property(styled_dom, node_id, node_state),
2300        MultiValue::Exact(StyleDirection::Rtl)
2301    );
2302
2303    Some(InlineBorderInfo {
2304        top,
2305        right,
2306        bottom,
2307        left,
2308        top_color: get_border_color_top(&border_info.colors.top),
2309        right_color: get_border_color_right(&border_info.colors.right),
2310        bottom_color: get_border_color_bottom(&border_info.colors.bottom),
2311        left_color: get_border_color_left(&border_info.colors.left),
2312        radius: get_border_radius_px(styled_dom, node_id, node_state),
2313        padding_top: p_top,
2314        padding_right: p_right,
2315        padding_bottom: p_bottom,
2316        padding_left: p_left,
2317        is_first_fragment: true,
2318        is_last_fragment: true,
2319        is_rtl,
2320    })
2321}
2322
2323// Selection and Caret Styling
2324
2325/// Style information for text selection rendering
2326#[derive(Debug, Clone, Copy, Default)]
2327pub struct SelectionStyle {
2328    /// Background color of the selection highlight
2329    pub bg_color: ColorU,
2330    /// Text color when selected (overrides normal text color)
2331    pub text_color: Option<ColorU>,
2332    /// Border radius for selection rectangles
2333    pub radius: f32,
2334}
2335
2336/// Get selection style for a node
2337pub fn get_selection_style(
2338    styled_dom: &StyledDom, 
2339    node_id: Option<NodeId>,
2340    system_style: Option<&std::sync::Arc<azul_css::system::SystemStyle>>,
2341) -> SelectionStyle {
2342    let Some(node_id) = node_id else {
2343        return SelectionStyle::default();
2344    };
2345
2346    let node_data = &styled_dom.node_data.as_container()[node_id];
2347    let node_state = &StyledNodeState::default();
2348
2349    // Try to get selection background from CSS, otherwise use system color, otherwise hard-coded default
2350    let default_bg = system_style
2351        .and_then(|ss| ss.colors.selection_background.as_option().copied())
2352        .unwrap_or(ColorU {
2353            r: 51,
2354            g: 153,
2355            b: 255, // Standard blue selection color
2356            a: 128, // Semi-transparent
2357        });
2358
2359    let bg_color = styled_dom
2360        .css_property_cache
2361        .ptr
2362        .get_selection_background_color(node_data, &node_id, node_state)
2363        .and_then(|c| c.get_property().cloned())
2364        .map(|c| c.inner)
2365        .unwrap_or(default_bg);
2366
2367    // Try to get selection text color from CSS, otherwise use system color
2368    let default_text = system_style
2369        .and_then(|ss| ss.colors.selection_text.as_option().copied());
2370
2371    let text_color = styled_dom
2372        .css_property_cache
2373        .ptr
2374        .get_selection_color(node_data, &node_id, node_state)
2375        .and_then(|c| c.get_property().cloned())
2376        .map(|c| c.inner)
2377        .or(default_text);
2378
2379    let radius = styled_dom
2380        .css_property_cache
2381        .ptr
2382        .get_selection_radius(node_data, &node_id, node_state)
2383        .and_then(|r| r.get_property().cloned())
2384        .map(|r| r.inner.to_pixels_internal(0.0, 16.0, 16.0)) // percent=0, em=16px default font size
2385        .unwrap_or(0.0);
2386
2387    SelectionStyle {
2388        bg_color,
2389        text_color,
2390        radius,
2391    }
2392}
2393
2394/// Style information for caret rendering.
2395#[derive(Debug, Clone, Copy)]
2396pub struct CaretStyle {
2397    /// Color of the caret bar
2398    pub color: ColorU,
2399    /// Width of the caret bar in pixels
2400    pub width: f32,
2401    /// Blink animation duration in milliseconds (0 = no blink)
2402    pub animation_duration: u32,
2403}
2404
2405impl Default for CaretStyle {
2406    fn default() -> Self {
2407        Self {
2408            color: ColorU::BLACK,
2409            width: 2.0,
2410            animation_duration: 500,
2411        }
2412    }
2413}
2414
2415/// Get caret style for a node
2416pub fn get_caret_style(styled_dom: &StyledDom, node_id: Option<NodeId>) -> CaretStyle {
2417    let Some(node_id) = node_id else {
2418        return CaretStyle::default();
2419    };
2420
2421    let node_data = &styled_dom.node_data.as_container()[node_id];
2422    let node_state = &StyledNodeState::default();
2423
2424    let color = styled_dom
2425        .css_property_cache
2426        .ptr
2427        .get_caret_color(node_data, &node_id, node_state)
2428        .and_then(|c| c.get_property().cloned())
2429        .map(|c| c.inner)
2430        .unwrap_or(ColorU::BLACK);
2431
2432    let width = styled_dom
2433        .css_property_cache
2434        .ptr
2435        .get_caret_width(node_data, &node_id, node_state)
2436        .and_then(|w| w.get_property().cloned())
2437        .map(|w| w.inner.to_pixels_internal(0.0, 16.0, 16.0)) // 16.0 as default em size
2438        .unwrap_or(2.0); // 2px width by default
2439
2440    let animation_duration = styled_dom
2441        .css_property_cache
2442        .ptr
2443        .get_caret_animation_duration(node_data, &node_id, node_state)
2444        .and_then(|d| d.get_property().cloned())
2445        .map(|d| d.inner.inner) // Duration.inner is the u32 milliseconds value
2446        .unwrap_or(500); // 500ms blink by default
2447
2448    CaretStyle {
2449        color,
2450        width,
2451        animation_duration,
2452    }
2453}
2454
2455// Scrollbar Information
2456
2457/// Get scrollbar information from a layout node.
2458///
2459/// Scrollbar requirements are computed during the layout phase in two paths:
2460/// - BFC layout: `compute_scrollbar_info()` + `merge_scrollbar_info()` in cache.rs
2461/// - Taffy layout: set in the measure callback in taffy_bridge.rs
2462///
2463/// If neither path set `scrollbar_info`, the node genuinely does not need
2464/// scrollbars. The previous heuristic (>3 children = force overflow) caused
2465/// false-positive scrollbars on normal containers.
2466pub fn get_scrollbar_info_from_layout(node: &LayoutNode) -> ScrollbarRequirements {
2467    node.scrollbar_info
2468        .clone()
2469        .unwrap_or_default()
2470}
2471
2472/// Resolve the **layout-effective** scrollbar width for a node, in pixels.
2473///
2474/// This combines three inputs:
2475/// 1. CSS `scrollbar-width` property on the node (`auto` → 16, `thin` → 8, `none` → 0)
2476/// 2. OS-level `ScrollbarPreferences.visibility` (overlay scrollbars → 0 layout reservation)
2477/// 3. Custom `-azul-scrollbar-style` width override
2478///
2479/// For **overlay** scrollbars (macOS `WhenScrolling`, or equivalent), this returns `0.0`
2480/// because overlay scrollbars are painted on top of content and do not consume layout space.
2481/// The scrollbar is still *rendered*, but no space is reserved during layout.
2482// +spec:overflow:b83014 - overlay scrollbars do not create scrollbar gutters
2483///
2484/// During display-list generation, use `get_scrollbar_style()` instead — that returns
2485/// the full visual style including the *paint* width (which may be non-zero for overlay).
2486pub fn get_layout_scrollbar_width_px<T: crate::font_traits::ParsedFontTrait>(
2487    ctx: &crate::solver3::LayoutContext<'_, T>,
2488    dom_id: NodeId,
2489    styled_node_state: &StyledNodeState,
2490) -> f32 {
2491    // Resolve the full scrollbar style (includes per-node CSS overrides + system style).
2492    // `reserve_width_px` already accounts for overlay vs legacy:
2493    //   overlay (WhenScrolling) → 0.0
2494    //   legacy (Always)         → visual_width_px
2495    let style = get_scrollbar_style(
2496        ctx.styled_dom,
2497        dom_id,
2498        styled_node_state,
2499        ctx.system_style.as_deref(),
2500    );
2501    style.reserve_width_px
2502}
2503
2504get_css_property!(
2505    get_display_property_internal,
2506    get_display,
2507    LayoutDisplay,
2508    azul_css::props::property::CssPropertyType::Display,
2509    compact = get_display
2510);
2511
2512pub fn get_display_property(
2513    styled_dom: &StyledDom,
2514    dom_id: Option<NodeId>,
2515) -> MultiValue<LayoutDisplay> {
2516    let Some(id) = dom_id else {
2517        return MultiValue::Exact(LayoutDisplay::Inline);
2518    };
2519    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2520    get_display_property_internal(styled_dom, id, node_state)
2521}
2522
2523/// CSS Display Module Level 3: Blockification of display values.
2524///
2525/// When an element is floated, absolutely positioned, or is the root element,
2526/// its computed display value may be "blockified" per the table in CSS Display 3 §2.7.
2527/// This function returns the blockified display value without mutating any state.
2528pub fn blockify_display(raw_display: LayoutDisplay) -> LayoutDisplay {
2529    match raw_display {
2530        // Inline-level display types become their block-level equivalents
2531        LayoutDisplay::Inline => LayoutDisplay::Block,
2532        // Per CSS Display 3 §2.7: inline-block blockifies to block
2533        // (for legacy reasons, loses its flow-root nature)
2534        LayoutDisplay::InlineBlock => LayoutDisplay::Block,
2535        LayoutDisplay::InlineFlex => LayoutDisplay::Flex,
2536        LayoutDisplay::InlineTable => LayoutDisplay::Table,
2537        LayoutDisplay::InlineGrid => LayoutDisplay::Grid,
2538        // CSS 2.2 §9.7: table-internal display values blockify to block
2539        // for absolutely positioned, floated, or root elements
2540        LayoutDisplay::TableRowGroup
2541        | LayoutDisplay::TableColumn
2542        | LayoutDisplay::TableColumnGroup
2543        | LayoutDisplay::TableHeaderGroup
2544        | LayoutDisplay::TableFooterGroup
2545        | LayoutDisplay::TableRow
2546        | LayoutDisplay::TableCell
2547        | LayoutDisplay::TableCaption => LayoutDisplay::Block,
2548        // Already block-level types are unchanged
2549        other => other,
2550    }
2551}
2552
2553/// // +spec:positioning:c31c24 - blockification is a computed-value change for absolute/float/root elements
2554/// Resolves the computed display value for an element, applying blockification
2555/// rules per CSS Display Module Level 3 §2.7.
2556// +spec:display-property:641ac5 - computed display value applies blockification/inlinification (not "as specified")
2557///
2558/// This centralizes the blockification decision so that all layout phases
2559/// (layout_tree, sizing, positioning) use consistent display values.
2560// +spec:floats:52aea6 - computed display blockified for floated/positioned/root elements
2561// +spec:positioning:ce02a1 - out-of-flow boxes (floated or absolutely positioned) get blockified display
2562pub fn get_computed_display(
2563    raw_display: LayoutDisplay,
2564    is_absolute_or_fixed: bool,
2565    is_floated: bool,
2566    is_root: bool,
2567    is_flex_grid_child: bool,
2568) -> LayoutDisplay {
2569    if raw_display == LayoutDisplay::None {
2570        return LayoutDisplay::None;
2571    }
2572    // +spec:positioning:69468c - absolute/fixed blockifies the box
2573    if is_absolute_or_fixed || is_floated || is_root || is_flex_grid_child {
2574        blockify_display(raw_display)
2575    } else {
2576        raw_display
2577    }
2578}
2579
2580// +spec:font-metrics:f7affa - vertical-align shorthand: maps CSS vertical-align values to inline layout alignment
2581/// Reads the CSS `vertical-align` property for a DOM node and converts it to
2582/// the text3 `VerticalAlign` enum used during inline layout.
2583// +spec:display-property:24c160 - vertical-align aligns inline-level box within the line
2584pub fn get_vertical_align_for_node(
2585    styled_dom: &StyledDom,
2586    dom_id: NodeId,
2587) -> crate::text3::cache::VerticalAlign {
2588    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2589    let va = match get_vertical_align_property(styled_dom, dom_id, node_state) {
2590        MultiValue::Exact(v) => v,
2591        _ => StyleVerticalAlign::default(),
2592    };
2593    match va {
2594        StyleVerticalAlign::Baseline => crate::text3::cache::VerticalAlign::Baseline,
2595        StyleVerticalAlign::Top => crate::text3::cache::VerticalAlign::Top,
2596        StyleVerticalAlign::Middle => crate::text3::cache::VerticalAlign::Middle,
2597        StyleVerticalAlign::Bottom => crate::text3::cache::VerticalAlign::Bottom,
2598        StyleVerticalAlign::Sub => crate::text3::cache::VerticalAlign::Sub,
2599        StyleVerticalAlign::Superscript => crate::text3::cache::VerticalAlign::Super,
2600        StyleVerticalAlign::TextTop => crate::text3::cache::VerticalAlign::TextTop,
2601        StyleVerticalAlign::TextBottom => crate::text3::cache::VerticalAlign::TextBottom,
2602        // +spec:line-height:b41ee3 - percentage vertical-align: raise/lower by % of line-height, 0% = baseline
2603        StyleVerticalAlign::Percentage(p) => {
2604            let font_size = get_element_font_size(styled_dom, dom_id, node_state);
2605            let line_height = get_line_height_value(styled_dom, dom_id, node_state)
2606                .map(|lh| lh.inner.normalized() * font_size)
2607                .unwrap_or(font_size * 1.2);
2608            crate::text3::cache::VerticalAlign::Offset(p.normalized() * line_height)
2609        }
2610        // §10.8.1: <length> is absolute offset from baseline
2611        StyleVerticalAlign::Length(l) => {
2612            let font_size = get_element_font_size(styled_dom, dom_id, node_state);
2613            let px = super::calc::resolve_pixel_value(&l, 0.0, font_size, font_size);
2614            crate::text3::cache::VerticalAlign::Offset(px)
2615        }
2616    }
2617}
2618
2619pub fn get_style_properties(
2620    styled_dom: &StyledDom,
2621    dom_id: NodeId,
2622    system_style: Option<&std::sync::Arc<azul_css::system::SystemStyle>>,
2623    viewport_size: azul_css::props::basic::PhysicalSize,
2624) -> StyleProperties {
2625    use azul_css::props::basic::{PhysicalSize, PropertyContext, ResolutionContext};
2626
2627    let node_data = &styled_dom.node_data.as_container()[dom_id];
2628    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
2629    let cache = &styled_dom.css_property_cache.ptr;
2630
2631    use azul_css::props::basic::font::{StyleFontFamily, StyleFontFamilyVec};
2632
2633    // Fast path: use compact cache reverse map (works for inherited values on text nodes).
2634    // Slow path: only for non-normal pseudo states (:hover, :focus, etc.)
2635    let font_families = if node_state.is_normal() {
2636        cache.compact_cache.as_ref()
2637            .and_then(|cc| {
2638                let fh = cc.tier2b_text[dom_id.index()].font_family_hash;
2639                if fh == 0 { return None; }
2640                cc.font_hash_to_families.get(&fh).cloned()
2641            })
2642            .unwrap_or_else(|| {
2643                StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
2644            })
2645    } else {
2646        cache
2647            .get_font_family(node_data, &dom_id, node_state)
2648            .and_then(|v| v.get_property().cloned())
2649            .unwrap_or_else(|| {
2650                StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
2651            })
2652    };
2653
2654    // Get parent's font-size for proper em resolution in font-size property.
2655    // FAST PATH: `get_parent_font_size` goes through `get_element_font_size`
2656    // which hits the memoised `resolved_font_sizes_px` Vec (O(1) array index).
2657    // The old code here walked the full CSS cascade for every call — 1485
2658    // slow walks per cold excel.html layout. Replaced 2026-04-17.
2659    let parent_font_size = get_parent_font_size(styled_dom, dom_id, node_state);
2660
2661    let root_font_size = get_root_font_size(styled_dom, node_state);
2662
2663    // Create resolution context for font-size (em refers to parent)
2664    let font_size_context = ResolutionContext {
2665        element_font_size: azul_css::props::basic::pixel::DEFAULT_FONT_SIZE, /* Not used for font-size property */
2666        parent_font_size,
2667        root_font_size,
2668        containing_block_size: PhysicalSize::new(0.0, 0.0),
2669        element_size: None,
2670        viewport_size,
2671    };
2672
2673    // Get font-size: either from this node's CSS, or inherit from parent
2674    // font-size is an inheritable property, so if the node doesn't have
2675    // an explicit font-size, it should inherit from the parent (not default to 16px)
2676    let font_size = {
2677        // FAST PATH: compact cache for normal state.
2678        // Sentinel/inherit/initial → inherit from parent directly (which is
2679        // what the slow cascade walk would fall back to via `.unwrap_or(parent_font_size)`
2680        // anyway — avoid the walk entirely).
2681        let mut fast_font_size: Option<f32> = None;
2682        let mut compact_said_inherit = false;
2683        if node_state.is_normal() {
2684            if let Some(ref cc) = cache.compact_cache {
2685                let raw = cc.get_font_size_raw(dom_id.index());
2686                if raw == azul_css::compact_cache::U32_SENTINEL
2687                    || raw == azul_css::compact_cache::U32_INHERIT
2688                    || raw == azul_css::compact_cache::U32_INITIAL
2689                {
2690                    compact_said_inherit = true;
2691                } else if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
2692                    fast_font_size = Some(pv.resolve_with_context(
2693                        &font_size_context,
2694                        PropertyContext::FontSize,
2695                    ));
2696                }
2697            }
2698        }
2699        if let Some(fs) = fast_font_size {
2700            fs
2701        } else if compact_said_inherit {
2702            parent_font_size
2703        } else {
2704            cache
2705                .get_font_size(node_data, &dom_id, node_state)
2706                .and_then(|v| v.get_property().cloned())
2707                .map(|v| {
2708                    v.inner
2709                        .resolve_with_context(&font_size_context, PropertyContext::FontSize)
2710                })
2711                .unwrap_or(parent_font_size)
2712        }
2713    };
2714
2715    let color_from_cache = {
2716        // FAST PATH: compact cache for text color
2717        let mut fast_color = None;
2718        if node_state.is_normal() {
2719            if let Some(ref cc) = cache.compact_cache {
2720                let raw = cc.get_text_color_raw(dom_id.index());
2721                if raw != 0 {
2722                    // Decode 0xRRGGBBAA → ColorU
2723                    fast_color = Some(ColorU {
2724                        r: (raw >> 24) as u8,
2725                        g: (raw >> 16) as u8,
2726                        b: (raw >> 8) as u8,
2727                        a: raw as u8,
2728                    });
2729                }
2730            }
2731        }
2732        fast_color.or_else(|| {
2733            cache
2734                .get_text_color(node_data, &dom_id, node_state)
2735                .and_then(|v| v.get_property().cloned())
2736                .map(|v| v.inner)
2737        })
2738    };
2739
2740    // CSS initial value for 'color' is UA-dependent but conventionally black.
2741    // Do NOT use system_style.colors.text here — that reflects the OS theme
2742    // (e.g. white on macOS dark mode) and would produce white text on
2743    // explicitly light-colored backgrounds.  System colors (CanvasText etc.)
2744    // should only be used when referenced through CSS system-color keywords.
2745    let color = color_from_cache.unwrap_or(ColorU::BLACK);
2746
2747    // +spec:font-metrics:e480da - line-height: normal/number/length/percentage resolution
2748    let line_height = {
2749        // FAST PATH: compact cache for line-height (stored as normalized × 1000 i16).
2750        // When the cache returns Some → we have a resolved value.
2751        // When it returns None AND node_state is normal → the compact cache stored
2752        // the sentinel, which means "line-height: normal" (the spec default).
2753        // Previously we fell through to a cascade walk here — but the default
2754        // has already been authoritatively decided by the builder, so the walk
2755        // would only ever re-confirm "no value, normal". 1600 pure-waste walks
2756        // per cold excel.html layout. Short-circuit to Normal directly.
2757        let mut fast_lh = None;
2758        let mut sentinel_normal = false;
2759        if node_state.is_normal() {
2760            if let Some(ref cc) = cache.compact_cache {
2761                if let Some(normalized) = cc.get_line_height(dom_id.index()) {
2762                    fast_lh = Some(crate::text3::cache::LineHeight::Px(normalized / 100.0 * font_size));
2763                } else {
2764                    // Sentinel in compact cache = "normal" (CSS default).
2765                    sentinel_normal = true;
2766                }
2767            }
2768        }
2769        if sentinel_normal {
2770            crate::text3::cache::LineHeight::Normal
2771        } else {
2772            fast_lh.unwrap_or_else(|| {
2773                cache
2774                    .get_line_height(node_data, &dom_id, node_state)
2775                    .and_then(|v| v.get_property().cloned())
2776                    .map(|v| crate::text3::cache::LineHeight::Px(v.inner.normalized() * font_size))
2777                    .unwrap_or(crate::text3::cache::LineHeight::Normal)
2778            })
2779        }
2780    };
2781
2782    // Get background color for INLINE elements only
2783    // CSS background-color is NOT inherited. For block-level elements (th, td, div, etc.),
2784    // the background is painted separately by paint_element_background() in display_list.rs.
2785    // Only inline elements (span, em, strong, a, etc.) should have their background color
2786    // propagated through StyleProperties for the text rendering pipeline.
2787    //
2788    // FAST PATH: use the compact-cache-backed display getter. The old code
2789    // here called `cache.get_display(..)` (the 3-arg convenience method on
2790    // CssPropertyCache) which routes through `get_property_slow` — 1485 slow
2791    // walks per cold excel.html layout. Replaced 2026-04-17.
2792    use azul_css::props::layout::LayoutDisplay;
2793    let display = match get_display_property(styled_dom, Some(dom_id)) {
2794        MultiValue::Exact(v) => v,
2795        _ => LayoutDisplay::Inline,
2796    };
2797
2798    // For inline and inline-block elements, get background content and border info
2799    // Block elements have their backgrounds/borders painted by display_list.rs
2800    let (background_color, background_content, border) =
2801        if matches!(display, LayoutDisplay::Inline | LayoutDisplay::InlineBlock) {
2802            let bg = get_background_color(styled_dom, dom_id, node_state);
2803            let bg_color = if bg.a > 0 { Some(bg) } else { None };
2804
2805            // Get full background contents (including gradients)
2806            let bg_contents = get_background_contents(styled_dom, dom_id, node_state);
2807
2808            // Get border info for inline elements
2809            let border_info = get_border_info(styled_dom, dom_id, node_state);
2810            let inline_border =
2811                get_inline_border_info(styled_dom, dom_id, node_state, &border_info);
2812
2813            (bg_color, bg_contents, inline_border)
2814        } else {
2815            // Block-level elements: background/border is painted by display_list.rs
2816            // via push_backgrounds_and_border() in DisplayListBuilder
2817            (None, Vec::new(), None)
2818        };
2819
2820    // Query font-weight from CSS cache
2821    let font_weight = match get_font_weight_property(styled_dom, dom_id, node_state) {
2822        MultiValue::Exact(v) => v,
2823        _ => StyleFontWeight::Normal,
2824    };
2825
2826    // Query font-style from CSS cache
2827    let font_style = match get_font_style_property(styled_dom, dom_id, node_state) {
2828        MultiValue::Exact(v) => v,
2829        _ => StyleFontStyle::Normal,
2830    };
2831
2832    // Convert StyleFontWeight/StyleFontStyle to fontconfig types
2833    let fc_weight = super::fc::convert_font_weight(font_weight);
2834    let fc_style = super::fc::convert_font_style(font_style);
2835
2836    // Check if any font family is a FontRef - if so, use FontStack::Ref
2837    // This allows embedded fonts (like Material Icons) to bypass fontconfig
2838    let font_stack = {
2839        // Look for a Ref in the font families
2840        let font_ref = (0..font_families.len())
2841            .find_map(|i| {
2842                match font_families.get(i).unwrap() {
2843                    azul_css::props::basic::font::StyleFontFamily::Ref(r) => Some(r.clone()),
2844                    _ => None,
2845                }
2846            });
2847        
2848        // Get platform for resolving system font types
2849        let platform = system_style.map(|ss| &ss.platform);
2850
2851        if let Some(font_ref) = font_ref {
2852            // Use FontStack::Ref for embedded fonts
2853            FontStack::Ref(font_ref)
2854        } else {
2855            // Build regular font stack from all font families
2856            let mut stack = Vec::with_capacity(font_families.len() + 3);
2857
2858            for i in 0..font_families.len() {
2859                let family = font_families.get(i).unwrap();
2860
2861                // Handle SystemFontType specially - resolve to actual OS font names
2862                // (e.g., "system:ui" → ["System Font", "Helvetica Neue", "Lucida Grande"] on macOS)
2863                if let azul_css::props::basic::font::StyleFontFamily::SystemType(system_type) = family {
2864                    if let Some(platform) = platform {
2865                        let font_names = system_type.get_fallback_chain(platform);
2866                        let system_weight = if system_type.is_bold() {
2867                            rust_fontconfig::FcWeight::Bold
2868                        } else {
2869                            fc_weight
2870                        };
2871                        let system_style_val = if system_type.is_italic() {
2872                            crate::text3::cache::FontStyle::Italic
2873                        } else {
2874                            fc_style
2875                        };
2876                        for font_name in font_names {
2877                            stack.push(crate::text3::cache::FontSelector {
2878                                family: font_name.to_string(),
2879                                weight: system_weight,
2880                                style: system_style_val,
2881                                unicode_ranges: Vec::new(),
2882                            });
2883                        }
2884                    } else {
2885                        // No platform info - fall back to generic sans-serif
2886                        stack.push(crate::text3::cache::FontSelector {
2887                            family: "sans-serif".to_string(),
2888                            weight: fc_weight,
2889                            style: fc_style,
2890                            unicode_ranges: Vec::new(),
2891                        });
2892                    }
2893                } else {
2894                    stack.push(crate::text3::cache::FontSelector {
2895                        family: family.as_string(),
2896                        weight: fc_weight,
2897                        style: fc_style,
2898                        unicode_ranges: Vec::new(),
2899                    });
2900                }
2901            }
2902
2903            // Add generic fallbacks (serif/sans-serif will be resolved based on Unicode ranges later)
2904            let generic_fallbacks = ["sans-serif", "serif", "monospace"];
2905            for fallback in &generic_fallbacks {
2906                if !stack
2907                    .iter()
2908                    .any(|f| f.family.to_lowercase() == fallback.to_lowercase())
2909                {
2910                    stack.push(crate::text3::cache::FontSelector {
2911                        family: fallback.to_string(),
2912                        weight: rust_fontconfig::FcWeight::Normal,
2913                        style: crate::text3::cache::FontStyle::Normal,
2914                        unicode_ranges: Vec::new(),
2915                    });
2916                }
2917            }
2918
2919            FontStack::Stack(stack)
2920        }
2921    };
2922
2923    // Get letter-spacing from CSS
2924    let letter_spacing = {
2925        // FAST PATH: compact cache for letter-spacing (i16 resolved px × 10)
2926        let mut fast_ls = None;
2927        if node_state.is_normal() {
2928            if let Some(ref cc) = cache.compact_cache {
2929                if let Some(px_val) = cc.get_letter_spacing(dom_id.index()) {
2930                    fast_ls = Some(crate::text3::cache::Spacing::Px(px_val.round() as i32));
2931                }
2932            }
2933        }
2934        fast_ls.unwrap_or_else(|| {
2935            cache
2936                .get_letter_spacing(node_data, &dom_id, node_state)
2937                .and_then(|v| v.get_property().cloned())
2938                .map(|v| {
2939                    let px_value = v.inner.resolve_with_context(&font_size_context, PropertyContext::FontSize);
2940                    crate::text3::cache::Spacing::Px(px_value.round() as i32)
2941                })
2942                .unwrap_or_default()
2943        })
2944    };
2945
2946    // Get word-spacing from CSS
2947    let word_spacing = {
2948        // FAST PATH: compact cache for word-spacing (i16 resolved px × 10)
2949        let mut fast_ws = None;
2950        if node_state.is_normal() {
2951            if let Some(ref cc) = cache.compact_cache {
2952                if let Some(px_val) = cc.get_word_spacing(dom_id.index()) {
2953                    fast_ws = Some(crate::text3::cache::Spacing::Px(px_val.round() as i32));
2954                }
2955            }
2956        }
2957        fast_ws.unwrap_or_else(|| {
2958            cache
2959                .get_word_spacing(node_data, &dom_id, node_state)
2960                .and_then(|v| v.get_property().cloned())
2961                .map(|v| {
2962                    let px_value = v.inner.resolve_with_context(&font_size_context, PropertyContext::FontSize);
2963                    crate::text3::cache::Spacing::Px(px_value.round() as i32)
2964                })
2965                .unwrap_or_default()
2966        })
2967    };
2968
2969    // Get text-decoration from CSS.
2970    //
2971    // Fast path: the compact cache keeps a `has_text_decoration` flag. If
2972    // unset (the overwhelmingly common case — plain body text has no
2973    // decoration set), skip the 4-pseudo-state × 6-layer cascade walk
2974    // entirely. Only nodes that actually set text-decoration pay the walk.
2975    let text_decoration = {
2976        let mut skip_walk = false;
2977        if node_state.is_normal() {
2978            if let Some(ref cc) = cache.compact_cache {
2979                if !cc.has_text_decoration(dom_id.index()) {
2980                    skip_walk = true;
2981                }
2982            }
2983        }
2984        if skip_walk {
2985            crate::text3::cache::TextDecoration::default()
2986        } else {
2987            cache
2988                .get_text_decoration(node_data, &dom_id, node_state)
2989                .and_then(|v| v.get_property().cloned())
2990                .map(|v| crate::text3::cache::TextDecoration::from_css(v))
2991                .unwrap_or_default()
2992        }
2993    };
2994
2995    // Get tab-size (tab-size) from CSS.
2996    //
2997    // tab-size defaults to `I16_SENTINEL` in the compact cache builder
2998    // (spec default is "8", meaning 8 space widths). The old fallback
2999    // called `cache.get_tab_size(..)` (slow cascade) for every node whose
3000    // raw was SENTINEL — virtually every node, because almost nothing sets
3001    // tab-size. That was 1485 pure-waste slow walks per cold layout.
3002    //
3003    // New behaviour: sentinel → 8.0 directly. Only walk the cascade when
3004    // the compact cache is genuinely unavailable (no `compact_cache`) or
3005    // the node is in a pseudo-state that bypassed the cache.
3006    let tab_size = {
3007        let mut fast_tab = None;
3008        if node_state.is_normal() {
3009            if let Some(ref cc) = cache.compact_cache {
3010                let raw = cc.get_tab_size_raw(dom_id.index());
3011                if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
3012                    fast_tab = Some(raw as f32 / 10.0);
3013                } else {
3014                    // Sentinel / Inherit / Initial → spec default is 8.
3015                    fast_tab = Some(8.0);
3016                }
3017            }
3018        }
3019        fast_tab.unwrap_or_else(|| {
3020            cache
3021                .get_tab_size(node_data, &dom_id, node_state)
3022                .and_then(|v| v.get_property().cloned())
3023                .map(|v| v.inner.number.get())
3024                .unwrap_or(8.0)
3025        })
3026    };
3027
3028    let properties = StyleProperties {
3029        font_stack,
3030        font_size_px: font_size,
3031        color,
3032        background_color,
3033        background_content,
3034        border,
3035        line_height,
3036        letter_spacing,
3037        word_spacing,
3038        text_decoration,
3039        tab_size,
3040        // These still use defaults - could be extended in future:
3041        // font_features, font_variations, text_transform, writing_mode, 
3042        // text_orientation, text_combine_upright, font_variant_*
3043        ..Default::default()
3044    };
3045
3046    properties
3047}
3048
3049pub fn get_list_style_type(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> StyleListStyleType {
3050    let Some(id) = dom_id else {
3051        return StyleListStyleType::default();
3052    };
3053    let node_data = &styled_dom.node_data.as_container()[id];
3054    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3055    styled_dom
3056        .css_property_cache
3057        .ptr
3058        .get_list_style_type(node_data, &id, node_state)
3059        .and_then(|v| v.get_property().copied())
3060        .unwrap_or_default()
3061}
3062
3063pub fn get_list_style_position(
3064    styled_dom: &StyledDom,
3065    dom_id: Option<NodeId>,
3066) -> StyleListStylePosition {
3067    let Some(id) = dom_id else {
3068        return StyleListStylePosition::default();
3069    };
3070    let node_data = &styled_dom.node_data.as_container()[id];
3071    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3072    styled_dom
3073        .css_property_cache
3074        .ptr
3075        .get_list_style_position(node_data, &id, node_state)
3076        .and_then(|v| v.get_property().copied())
3077        .unwrap_or_default()
3078}
3079
3080// New: Taffy Bridge Getters - Box Model Properties with Ua Css Fallback
3081
3082use azul_css::props::layout::{
3083    LayoutInsetBottom, LayoutLeft, LayoutMarginBottom, LayoutMarginLeft, LayoutMarginRight,
3084    LayoutMarginTop, LayoutMaxHeight, LayoutMaxWidth, LayoutMinHeight, LayoutMinWidth,
3085    LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutPaddingTop, LayoutRight,
3086    LayoutTop,
3087};
3088
3089/// Get inset (position) properties - returns MultiValue<PixelValue>
3090get_css_property_pixel!(
3091    get_css_left,
3092    get_left,
3093    azul_css::props::property::CssPropertyType::Left,
3094    compact_i16 = get_left
3095);
3096get_css_property_pixel!(
3097    get_css_right,
3098    get_right,
3099    azul_css::props::property::CssPropertyType::Right,
3100    compact_i16 = get_right
3101);
3102get_css_property_pixel!(
3103    get_css_top,
3104    get_top,
3105    azul_css::props::property::CssPropertyType::Top,
3106    compact_i16 = get_top
3107);
3108get_css_property_pixel!(
3109    get_css_bottom,
3110    get_bottom,
3111    azul_css::props::property::CssPropertyType::Bottom,
3112    compact_i16 = get_bottom
3113);
3114
3115/// Get margin properties - returns MultiValue<PixelValue>
3116get_css_property_pixel!(
3117    get_css_margin_left,
3118    get_margin_left,
3119    azul_css::props::property::CssPropertyType::MarginLeft,
3120    compact_i16 = get_margin_left_raw
3121);
3122get_css_property_pixel!(
3123    get_css_margin_right,
3124    get_margin_right,
3125    azul_css::props::property::CssPropertyType::MarginRight,
3126    compact_i16 = get_margin_right_raw
3127);
3128get_css_property_pixel!(
3129    get_css_margin_top,
3130    get_margin_top,
3131    azul_css::props::property::CssPropertyType::MarginTop,
3132    compact_i16 = get_margin_top_raw
3133);
3134get_css_property_pixel!(
3135    get_css_margin_bottom,
3136    get_margin_bottom,
3137    azul_css::props::property::CssPropertyType::MarginBottom,
3138    compact_i16 = get_margin_bottom_raw
3139);
3140
3141/// Get padding properties - returns MultiValue<PixelValue>
3142get_css_property_pixel!(
3143    get_css_padding_left,
3144    get_padding_left,
3145    azul_css::props::property::CssPropertyType::PaddingLeft,
3146    compact_i16 = get_padding_left_raw
3147);
3148get_css_property_pixel!(
3149    get_css_padding_right,
3150    get_padding_right,
3151    azul_css::props::property::CssPropertyType::PaddingRight,
3152    compact_i16 = get_padding_right_raw
3153);
3154get_css_property_pixel!(
3155    get_css_padding_top,
3156    get_padding_top,
3157    azul_css::props::property::CssPropertyType::PaddingTop,
3158    compact_i16 = get_padding_top_raw
3159);
3160get_css_property_pixel!(
3161    get_css_padding_bottom,
3162    get_padding_bottom,
3163    azul_css::props::property::CssPropertyType::PaddingBottom,
3164    compact_i16 = get_padding_bottom_raw
3165);
3166
3167/// Get min/max size properties
3168get_css_property!(
3169    get_css_min_width,
3170    get_min_width,
3171    LayoutMinWidth,
3172    azul_css::props::property::CssPropertyType::MinWidth,
3173    compact_u32_struct = get_min_width_raw
3174);
3175
3176get_css_property!(
3177    get_css_min_height,
3178    get_min_height,
3179    LayoutMinHeight,
3180    azul_css::props::property::CssPropertyType::MinHeight,
3181    compact_u32_struct = get_min_height_raw
3182);
3183
3184get_css_property!(
3185    get_css_max_width,
3186    get_max_width,
3187    LayoutMaxWidth,
3188    azul_css::props::property::CssPropertyType::MaxWidth,
3189    compact_u32_struct = get_max_width_raw
3190);
3191
3192get_css_property!(
3193    get_css_max_height,
3194    get_max_height,
3195    LayoutMaxHeight,
3196    azul_css::props::property::CssPropertyType::MaxHeight,
3197    compact_u32_struct = get_max_height_raw
3198);
3199
3200/// Get border width properties (no UA CSS fallback needed, defaults to 0)
3201get_css_property_pixel!(
3202    get_css_border_left_width,
3203    get_border_left_width,
3204    azul_css::props::property::CssPropertyType::BorderLeftWidth,
3205    compact_i16 = get_border_left_width_raw
3206);
3207get_css_property_pixel!(
3208    get_css_border_right_width,
3209    get_border_right_width,
3210    azul_css::props::property::CssPropertyType::BorderRightWidth,
3211    compact_i16 = get_border_right_width_raw
3212);
3213get_css_property_pixel!(
3214    get_css_border_top_width,
3215    get_border_top_width,
3216    azul_css::props::property::CssPropertyType::BorderTopWidth,
3217    compact_i16 = get_border_top_width_raw
3218);
3219get_css_property_pixel!(
3220    get_css_border_bottom_width,
3221    get_border_bottom_width,
3222    azul_css::props::property::CssPropertyType::BorderBottomWidth,
3223    compact_i16 = get_border_bottom_width_raw
3224);
3225
3226// Fragmentation (page breaking) properties
3227
3228/// Get break-before property for paged media
3229pub fn get_break_before(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> PageBreak {
3230    let Some(id) = dom_id else {
3231        return PageBreak::Auto;
3232    };
3233    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3234    // Negative fast path: break-* is almost never declared.
3235    if node_state.is_normal() {
3236        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
3237            if !cc.has_break(id.index()) {
3238                return PageBreak::Auto;
3239            }
3240        }
3241    }
3242    let node_data = &styled_dom.node_data.as_container()[id];
3243    styled_dom
3244        .css_property_cache
3245        .ptr
3246        .get_break_before(node_data, &id, node_state)
3247        .and_then(|v| v.get_property().cloned())
3248        .unwrap_or(PageBreak::Auto)
3249}
3250
3251/// Get break-after property for paged media
3252pub fn get_break_after(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> PageBreak {
3253    let Some(id) = dom_id else {
3254        return PageBreak::Auto;
3255    };
3256    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3257    if node_state.is_normal() {
3258        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
3259            if !cc.has_break(id.index()) {
3260                return PageBreak::Auto;
3261            }
3262        }
3263    }
3264    let node_data = &styled_dom.node_data.as_container()[id];
3265    styled_dom
3266        .css_property_cache
3267        .ptr
3268        .get_break_after(node_data, &id, node_state)
3269        .and_then(|v| v.get_property().cloned())
3270        .unwrap_or(PageBreak::Auto)
3271}
3272
3273/// Check if a PageBreak value forces a page break (always, page, left, right, etc.)
3274pub fn is_forced_page_break(page_break: PageBreak) -> bool {
3275    matches!(
3276        page_break,
3277        PageBreak::Always
3278            | PageBreak::Page
3279            | PageBreak::Left
3280            | PageBreak::Right
3281            | PageBreak::Recto
3282            | PageBreak::Verso
3283            | PageBreak::All
3284    )
3285}
3286
3287/// Get break-inside property for paged media
3288pub fn get_break_inside(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> BreakInside {
3289    let Some(id) = dom_id else {
3290        return BreakInside::Auto;
3291    };
3292    let node_data = &styled_dom.node_data.as_container()[id];
3293    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3294    styled_dom
3295        .css_property_cache
3296        .ptr
3297        .get_break_inside(node_data, &id, node_state)
3298        .and_then(|v| v.get_property().cloned())
3299        .unwrap_or(BreakInside::Auto)
3300}
3301
3302/// Get orphans property (minimum lines at bottom of page)
3303pub fn get_orphans(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> u32 {
3304    let Some(id) = dom_id else {
3305        return 2; // Default value
3306    };
3307    let node_data = &styled_dom.node_data.as_container()[id];
3308    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3309    styled_dom
3310        .css_property_cache
3311        .ptr
3312        .get_orphans(node_data, &id, node_state)
3313        .and_then(|v| v.get_property().cloned())
3314        .map(|o| o.inner)
3315        .unwrap_or(2)
3316}
3317
3318/// Get widows property (minimum lines at top of page)
3319pub fn get_widows(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> u32 {
3320    let Some(id) = dom_id else {
3321        return 2; // Default value
3322    };
3323    let node_data = &styled_dom.node_data.as_container()[id];
3324    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3325    styled_dom
3326        .css_property_cache
3327        .ptr
3328        .get_widows(node_data, &id, node_state)
3329        .and_then(|v| v.get_property().cloned())
3330        .map(|w| w.inner)
3331        .unwrap_or(2)
3332}
3333
3334/// Get box-decoration-break property
3335pub fn get_box_decoration_break(
3336    styled_dom: &StyledDom,
3337    dom_id: Option<NodeId>,
3338) -> BoxDecorationBreak {
3339    let Some(id) = dom_id else {
3340        return BoxDecorationBreak::Slice;
3341    };
3342    let node_data = &styled_dom.node_data.as_container()[id];
3343    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
3344    styled_dom
3345        .css_property_cache
3346        .ptr
3347        .get_box_decoration_break(node_data, &id, node_state)
3348        .and_then(|v| v.get_property().cloned())
3349        .unwrap_or(BoxDecorationBreak::Slice)
3350}
3351
3352// Helper functions for break properties
3353
3354/// Check if a PageBreak value is avoid
3355pub fn is_avoid_page_break(page_break: &PageBreak) -> bool {
3356    matches!(page_break, PageBreak::Avoid | PageBreak::AvoidPage)
3357}
3358
3359/// Check if a BreakInside value prevents breaks
3360pub fn is_avoid_break_inside(break_inside: &BreakInside) -> bool {
3361    matches!(
3362        break_inside,
3363        BreakInside::Avoid | BreakInside::AvoidPage | BreakInside::AvoidColumn
3364    )
3365}
3366
3367// Font Chain Resolution - Pre-Layout Font Loading
3368
3369use std::collections::HashMap;
3370
3371use rust_fontconfig::{
3372    FcFontCache, FcWeight, FontFallbackChain, PatternMatch, UnicodeRange,
3373    DEFAULT_UNICODE_FALLBACK_SCRIPTS,
3374};
3375
3376use crate::text3::cache::{FontChainKey, FontChainKeyOrRef, FontSelector, FontStack, FontStyle};
3377
3378/// Result of collecting font stacks from a StyledDom
3379/// Contains all unique font stacks and the mapping from StyleFontFamiliesHash to FontChainKey
3380#[derive(Debug, Clone)]
3381pub struct CollectedFontStacks {
3382    /// All unique font stacks found in the document (system/file fonts via fontconfig)
3383    pub font_stacks: Vec<Vec<FontSelector>>,
3384    /// Map from the font stack hash to the index in font_stacks
3385    pub hash_to_index: HashMap<u64, usize>,
3386    /// Direct FontRefs that bypass fontconfig (e.g., embedded icon fonts)
3387    /// These are keyed by their pointer address for uniqueness
3388    pub font_refs: HashMap<usize, azul_css::props::basic::font::FontRef>,
3389}
3390
3391/// Resolved font chains ready for use in layout
3392/// This is the result of resolving font stacks against FcFontCache
3393#[derive(Debug, Clone)]
3394pub struct ResolvedFontChains {
3395    /// Map from FontChainKeyOrRef to the resolved FontFallbackChain
3396    /// For FontChainKeyOrRef::Ref variants, the FontFallbackChain contains
3397    /// a single-font chain that covers the entire Unicode range.
3398    pub chains: HashMap<FontChainKeyOrRef, FontFallbackChain>,
3399}
3400
3401impl ResolvedFontChains {
3402    /// Get a font chain by its key
3403    pub fn get(&self, key: &FontChainKeyOrRef) -> Option<&FontFallbackChain> {
3404        self.chains.get(key)
3405    }
3406    
3407    /// Get a font chain by FontChainKey (for system fonts)
3408    pub fn get_by_chain_key(&self, key: &FontChainKey) -> Option<&FontFallbackChain> {
3409        self.chains.get(&FontChainKeyOrRef::Chain(key.clone()))
3410    }
3411
3412    /// Get a font chain for a font stack (via fontconfig)
3413    pub fn get_for_font_stack(&self, font_stack: &[FontSelector]) -> Option<&FontFallbackChain> {
3414        let key = FontChainKeyOrRef::Chain(FontChainKey::from_selectors(font_stack));
3415        self.chains.get(&key)
3416    }
3417    
3418    /// Get a font chain for a FontRef pointer
3419    pub fn get_for_font_ref(&self, ptr: usize) -> Option<&FontFallbackChain> {
3420        self.chains.get(&FontChainKeyOrRef::Ref(ptr))
3421    }
3422
3423    /// Consume self and return the inner HashMap with FontChainKeyOrRef keys
3424    ///
3425    /// This is useful when you need access to both Chain and Ref variants.
3426    pub fn into_inner(self) -> HashMap<FontChainKeyOrRef, FontFallbackChain> {
3427        self.chains
3428    }
3429
3430    /// Consume self and return only the fontconfig-resolved chains
3431    /// 
3432    /// This filters out FontRef entries and returns only the chains
3433    /// resolved via fontconfig. This is what FontManager expects.
3434    pub fn into_fontconfig_chains(self) -> HashMap<FontChainKey, FontFallbackChain> {
3435        self.chains
3436            .into_iter()
3437            .filter_map(|(key, chain)| {
3438                match key {
3439                    FontChainKeyOrRef::Chain(chain_key) => Some((chain_key, chain)),
3440                    FontChainKeyOrRef::Ref(_) => None,
3441                }
3442            })
3443            .collect()
3444    }
3445
3446    /// Get the number of resolved chains
3447    pub fn len(&self) -> usize {
3448        self.chains.len()
3449    }
3450
3451    /// Check if there are no resolved chains
3452    pub fn is_empty(&self) -> bool {
3453        self.chains.is_empty()
3454    }
3455    
3456    /// Get the number of direct FontRefs
3457    pub fn font_refs_len(&self) -> usize {
3458        self.chains.keys().filter(|k| k.is_ref()).count()
3459    }
3460}
3461
3462/// Collect all unique font stacks from a StyledDom
3463///
3464/// This is a pure function that iterates over all nodes in the DOM and
3465/// extracts the font-family property from each node that has text content.
3466///
3467/// # Arguments
3468/// * `styled_dom` - The styled DOM to extract font stacks from
3469/// * `platform` - The current platform for resolving system font types
3470///
3471/// # Returns
3472/// A `CollectedFontStacks` containing all unique font stacks and a hash-to-index mapping
3473pub fn collect_font_stacks_from_styled_dom(
3474    styled_dom: &StyledDom,
3475    platform: &azul_css::system::Platform,
3476) -> CollectedFontStacks {
3477    use azul_css::compact_cache::{FONT_WEIGHT_SHIFT, FONT_WEIGHT_MASK, FONT_STYLE_SHIFT, FONT_STYLE_MASK};
3478
3479    let mut font_stacks = Vec::new();
3480    let mut hash_to_index: HashMap<u64, usize> = HashMap::new();
3481    let mut font_refs: HashMap<usize, azul_css::props::basic::font::FontRef> = HashMap::new();
3482
3483    let node_data = styled_dom.node_data.as_container();
3484    let cache = &styled_dom.css_property_cache.ptr;
3485    let compact = match cache.compact_cache.as_ref() {
3486        Some(c) => c,
3487        None => return CollectedFontStacks { font_stacks, hash_to_index, font_refs },
3488    };
3489
3490    // Phase 1: Scan compact cache arrays (just u64 reads) to find unique
3491    // (font_family_hash, weight, style) tuples. Record one representative
3492    // node index per unique tuple for the expensive CSS lookup in Phase 2.
3493    // Key: (font_family_hash, weight_encoded, style_encoded) → representative node index
3494    let mut unique_font_keys: HashMap<(u64, u8, u8), usize> = HashMap::new();
3495    let node_count = node_data.internal.len();
3496
3497    for i in 0..node_count {
3498        // Only text nodes need fonts
3499        if !matches!(node_data.internal[i].node_type, NodeType::Text(_)) {
3500            continue;
3501        }
3502        let fh = compact.tier2b_text[i].font_family_hash;
3503        let t1 = compact.tier1_enums[i];
3504        let weight_bits = ((t1 >> FONT_WEIGHT_SHIFT) & FONT_WEIGHT_MASK) as u8;
3505        let style_bits = ((t1 >> FONT_STYLE_SHIFT) & FONT_STYLE_MASK) as u8;
3506        let key = (fh, weight_bits, style_bits);
3507        unique_font_keys.entry(key).or_insert(i);
3508    }
3509
3510    // Phase 2: For each unique tuple, do ONE expensive CSS lookup on the
3511    // representative node to get the actual font-family names.
3512    let styled_nodes = styled_dom.styled_nodes.as_container();
3513
3514    for (&(fh, _wb, _sb), &repr_idx) in &unique_font_keys {
3515        let dom_id = match NodeId::from_usize(repr_idx) {
3516            Some(id) => id,
3517            None => continue,
3518        };
3519        let node_state = &styled_nodes[dom_id].styled_node_state;
3520
3521        // Use reverse map from compact cache: hash → actual font families.
3522        // This works for ALL nodes including text nodes that inherit font-family
3523        // via compact cache (where get_property_slow would return None).
3524        let font_families = compact.font_hash_to_families
3525            .get(&fh)
3526            .cloned()
3527            .unwrap_or_else(|| {
3528                StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
3529            });
3530
3531        // Check for embedded FontRef
3532        if let Some(first_family) = font_families.get(0) {
3533            if let StyleFontFamily::Ref(font_ref) = first_family {
3534                let ptr = font_ref.parsed as usize;
3535                font_refs.entry(ptr).or_insert_with(|| font_ref.clone());
3536                continue;
3537            }
3538        }
3539
3540        let font_weight = match get_font_weight_property(styled_dom, dom_id, node_state) {
3541            MultiValue::Exact(v) => v,
3542            _ => StyleFontWeight::Normal,
3543        };
3544        let font_style = match get_font_style_property(styled_dom, dom_id, node_state) {
3545            MultiValue::Exact(v) => v,
3546            _ => StyleFontStyle::Normal,
3547        };
3548
3549        let fc_weight = super::fc::convert_font_weight(font_weight);
3550        let fc_style = super::fc::convert_font_style(font_style);
3551
3552        let mut font_stack = Vec::with_capacity(font_families.len() + 3);
3553
3554        for i in 0..font_families.len() {
3555            let family = font_families.get(i).unwrap();
3556            if matches!(family, StyleFontFamily::Ref(_)) {
3557                continue;
3558            }
3559            if let StyleFontFamily::SystemType(system_type) = family {
3560                let font_names = system_type.get_fallback_chain(platform);
3561                let system_weight = if system_type.is_bold() { FcWeight::Bold } else { fc_weight };
3562                let system_style = if system_type.is_italic() { FontStyle::Italic } else { fc_style };
3563                for font_name in font_names {
3564                    font_stack.push(FontSelector {
3565                        family: font_name.to_string(),
3566                        weight: system_weight,
3567                        style: system_style,
3568                        unicode_ranges: Vec::new(),
3569                    });
3570                }
3571            } else {
3572                font_stack.push(FontSelector {
3573                    family: family.as_string(),
3574                    weight: fc_weight,
3575                    style: fc_style,
3576                    unicode_ranges: Vec::new(),
3577                });
3578            }
3579        }
3580
3581        // Add generic fallbacks
3582        for fallback in &["sans-serif", "serif", "monospace"] {
3583            if !font_stack.iter().any(|f| f.family.eq_ignore_ascii_case(fallback)) {
3584                font_stack.push(FontSelector {
3585                    family: fallback.to_string(),
3586                    weight: FcWeight::Normal,
3587                    style: FontStyle::Normal,
3588                    unicode_ranges: Vec::new(),
3589                });
3590            }
3591        }
3592
3593        if font_stack.is_empty() {
3594            continue;
3595        }
3596
3597        let key = FontChainKey::from_selectors(&font_stack);
3598        let hash = {
3599            use std::hash::{Hash, Hasher};
3600            let mut hasher = std::collections::hash_map::DefaultHasher::new();
3601            key.hash(&mut hasher);
3602            hasher.finish()
3603        };
3604
3605        if !hash_to_index.contains_key(&hash) {
3606            let idx = font_stacks.len();
3607            font_stacks.push(font_stack);
3608            hash_to_index.insert(hash, idx);
3609        }
3610    }
3611
3612    CollectedFontStacks {
3613        font_stacks,
3614        hash_to_index,
3615        font_refs,
3616    }
3617}
3618
3619/// Resolve all font chains for the collected font stacks
3620///
3621/// This is a pure function that takes the collected font stacks and resolves
3622/// them against the FcFontCache to produce FontFallbackChains.
3623///
3624/// # Arguments
3625/// * `collected` - The collected font stacks from `collect_font_stacks_from_styled_dom`
3626/// * `fc_cache` - The fontconfig cache to resolve fonts against
3627///
3628/// # Returns
3629/// A `ResolvedFontChains` containing all resolved font chains
3630/// Walk every text node in `styled_dom` and collect the set of
3631/// non-ASCII codepoints actually present in the document.
3632///
3633/// Used by [`prune_chain_to_used_chars`] to drop CSS-fallback fonts
3634/// from a resolved chain when the *first* match in a `css_fallbacks`
3635/// group already covers everything the page asks for. ASCII (`< 0x80`)
3636/// is universally covered by every Latin font we'd resolve, so we
3637/// skip it here to keep the set small. Unicode characters in the
3638/// returned set are deduped + sorted via `BTreeSet`.
3639///
3640/// Cost: O(total text length). Cheap relative to layout itself.
3641pub fn collect_used_codepoints(
3642    styled_dom: &StyledDom,
3643) -> std::collections::BTreeSet<u32> {
3644    let mut out = std::collections::BTreeSet::new();
3645    let node_data = styled_dom.node_data.as_container();
3646    for node in node_data.internal.iter() {
3647        let azul_core::dom::NodeType::Text(s) = &node.node_type else {
3648            continue;
3649        };
3650        for c in s.as_str().chars() {
3651            let cp = c as u32;
3652            if cp >= 0x80 {
3653                out.insert(cp);
3654            }
3655        }
3656    }
3657    out
3658}
3659
3660/// Like [`collect_used_codepoints`] but keeps ASCII. The fast-probe
3661/// path (`FcFontRegistry::request_fonts_fast`) *does* need ASCII:
3662/// "the font has to cover every codepoint I will render" is only
3663/// true if we tell it every codepoint, and "Segoe UI" not being
3664/// installed on macOS means even ASCII has to fall through to a
3665/// system default.
3666///
3667/// `collect_used_codepoints` strips ASCII because its caller
3668/// (`prune_chain_to_used_chars`) runs *after* resolution to trim an
3669/// already-resolved chain and every Latin-covering font passes ASCII
3670/// trivially. That assumption doesn't hold during probing.
3671pub fn collect_used_codepoints_all(
3672    styled_dom: &StyledDom,
3673) -> std::collections::BTreeSet<char> {
3674    let mut out = std::collections::BTreeSet::new();
3675    let node_data = styled_dom.node_data.as_container();
3676    for node in node_data.internal.iter() {
3677        let azul_core::dom::NodeType::Text(s) = &node.node_type else {
3678            continue;
3679        };
3680        for c in s.as_str().chars() {
3681            out.insert(c);
3682        }
3683    }
3684    out
3685}
3686
3687/// Trim a [`FontFallbackChain`] down to the minimum set of `FontMatch`
3688/// entries needed to cover `used_chars` (typically from
3689/// [`collect_used_codepoints`]).
3690///
3691/// For each `css_fallbacks` group, walk matches in the resolver's
3692/// preferred order and keep them until every codepoint in
3693/// `used_chars` is covered (per the OS/2 unicode-range bits cached
3694/// in `FontMatch.unicode_ranges`). Always keeps at least the first
3695/// match per group so a font listed in CSS doesn't disappear.
3696///
3697/// `unicode_fallbacks` is filtered to only include fonts whose
3698/// ranges intersect `used_chars` — Phase-6's
3699/// [`scripts_present_in_styled_dom`] already scopes the *script
3700/// blocks* but a single block (e.g. CJK Unified, U+4E00..U+9FFF)
3701/// can have hundreds of matching system fonts; this prunes them
3702/// down to the few that actually cover the codepoints used.
3703///
3704/// On excel.html (~ASCII-only) this drops the per-chain
3705/// `css_fallbacks` from 5 → 1 in each group, eliminating ~20 of
3706/// the 26 fonts that would otherwise be parsed by
3707/// `load_fonts_from_disk`.
3708pub fn prune_chain_to_used_chars(
3709    chain: &mut rust_fontconfig::FontFallbackChain,
3710    used_chars: &std::collections::BTreeSet<u32>,
3711) {
3712    fn fm_covers(fm: &rust_fontconfig::FontMatch, cp: u32) -> bool {
3713        fm.unicode_ranges
3714            .iter()
3715            .any(|r| cp >= r.start && cp <= r.end)
3716    }
3717
3718    for group in &mut chain.css_fallbacks {
3719        if group.fonts.is_empty() {
3720            continue;
3721        }
3722        // Track which non-ASCII chars still need coverage as we walk
3723        // matches in order. We always keep at least the first match.
3724        let mut needed: Vec<u32> = used_chars.iter().copied().collect();
3725        needed.retain(|&cp| !fm_covers(&group.fonts[0], cp));
3726        let mut keep = 1;
3727        for fm in group.fonts.iter().skip(1) {
3728            if needed.is_empty() {
3729                break;
3730            }
3731            keep += 1;
3732            needed.retain(|&cp| !fm_covers(fm, cp));
3733        }
3734        group.fonts.truncate(keep);
3735    }
3736
3737    chain.unicode_fallbacks.retain(|fm| {
3738        used_chars.iter().any(|&cp| fm_covers(fm, cp))
3739    });
3740}
3741
3742/// Scan text-node content in `styled_dom` and return the subset of
3743/// [`rust_fontconfig::DEFAULT_UNICODE_FALLBACK_SCRIPTS`] whose code-point
3744/// ranges actually appear in any text. Short-circuits once all seven
3745/// ranges have been seen.
3746///
3747/// Callers pass the result as `scripts_hint` to
3748/// [`resolve_font_chains`] / [`collect_and_resolve_font_chains_with_registration`];
3749/// `rust_fontconfig::FcFontCache::resolve_font_chain_with_scripts` then
3750/// only pulls in Unicode-fallback fonts for scripts the document
3751/// actually uses. An ASCII-only page returns an empty vector, which
3752/// avoids dragging Arial Unicode MS, CJK fonts, etc. into the
3753/// resolved chain and therefore into the eager-load step.
3754pub fn scripts_present_in_styled_dom(styled_dom: &StyledDom) -> Vec<UnicodeRange> {
3755    let scripts = DEFAULT_UNICODE_FALLBACK_SCRIPTS;
3756    let mut seen = vec![false; scripts.len()];
3757    let mut hits = 0usize;
3758    let node_data = styled_dom.node_data.as_container();
3759    'outer: for node in node_data.internal.iter() {
3760        let text: &str = match &node.node_type {
3761            azul_core::dom::NodeType::Text(s) => s.as_str(),
3762            _ => continue,
3763        };
3764        for c in text.chars() {
3765            let cp = c as u32;
3766            // Cheap reject: everything below the first fallback-script
3767            // range (Cyrillic starts at U+0400) is covered by the CSS
3768            // fallbacks' own glyphs — no reason to probe.
3769            if cp < 0x0400 {
3770                continue;
3771            }
3772            for (idx, r) in scripts.iter().enumerate() {
3773                if !seen[idx] && cp >= r.start && cp <= r.end {
3774                    seen[idx] = true;
3775                    hits += 1;
3776                    if hits == scripts.len() {
3777                        break 'outer;
3778                    }
3779                    break;
3780                }
3781            }
3782        }
3783    }
3784    scripts
3785        .iter()
3786        .enumerate()
3787        .filter_map(|(i, r)| if seen[i] { Some(*r) } else { None })
3788        .collect()
3789}
3790
3791/// Resolve font chains for a collected set of stacks.
3792///
3793/// `scripts_hint`:
3794/// - `None` keeps the original "all 7 default scripts" behaviour
3795///   (Cyrillic / Arabic / Devanagari / Hiragana / Katakana / CJK /
3796///   Hangul) — equivalent to passing
3797///   `Some(rust_fontconfig::DEFAULT_UNICODE_FALLBACK_SCRIPTS)`.
3798/// - `Some(&[])` attaches *no* Unicode fallbacks, suitable for
3799///   ASCII-only documents. Combined with `prune_chain_to_used_chars`
3800///   this is what eliminates Arial Unicode MS / CJK / Arabic font
3801///   loads on Latin-only pages.
3802/// - `Some(ranges)` attaches fallbacks only for the listed scripts.
3803///   Production callers compute this via
3804///   [`scripts_present_in_styled_dom`].
3805pub fn resolve_font_chains(
3806    collected: &CollectedFontStacks,
3807    fc_cache: &FcFontCache,
3808    scripts_hint: Option<&[UnicodeRange]>,
3809) -> ResolvedFontChains {
3810    resolve_font_chains_with_registry(collected, fc_cache, None, scripts_hint)
3811}
3812
3813/// Registry-aware variant of [`resolve_font_chains`]. When `registry`
3814/// is `Some`, each chain resolution goes through
3815/// [`rust_fontconfig::registry::FcFontRegistry::request_and_resolve_with_scripts`]
3816/// which priority-bumps the builder for families not yet in the
3817/// snapshot and waits for them — the "scout-on-demand" path that
3818/// avoids the eager common-stack pre-parse.
3819///
3820/// When `registry` is `None`, falls back to
3821/// [`rust_fontconfig::FcFontCache::resolve_font_chain_with_scripts`]
3822/// against the passed-in snapshot, which is what
3823/// [`resolve_font_chains`] does and what every code path did before
3824/// Phase 3.
3825pub fn resolve_font_chains_with_registry(
3826    collected: &CollectedFontStacks,
3827    fc_cache: &FcFontCache,
3828    registry: Option<&rust_fontconfig::registry::FcFontRegistry>,
3829    scripts_hint: Option<&[UnicodeRange]>,
3830) -> ResolvedFontChains {
3831    let mut chains = HashMap::new();
3832
3833    // Resolve system/file font stacks via fontconfig
3834    for font_stack in &collected.font_stacks {
3835        if font_stack.is_empty() {
3836            continue;
3837        }
3838
3839        // Build font families list
3840        let font_families: Vec<String> = font_stack
3841            .iter()
3842            .map(|s| s.family.clone())
3843            .filter(|f| !f.is_empty())
3844            .collect();
3845
3846        let font_families = if font_families.is_empty() {
3847            vec!["sans-serif".to_string()]
3848        } else {
3849            font_families
3850        };
3851
3852        let weight = font_stack[0].weight;
3853        let is_italic = font_stack[0].style == FontStyle::Italic;
3854        let is_oblique = font_stack[0].style == FontStyle::Oblique;
3855
3856        let cache_key = FontChainKeyOrRef::Chain(FontChainKey {
3857            font_families: font_families.clone(),
3858            weight,
3859            italic: is_italic,
3860            oblique: is_oblique,
3861        });
3862
3863        // Skip if already resolved
3864        if chains.contains_key(&cache_key) {
3865            continue;
3866        }
3867
3868        // Resolve the font chain
3869        // IMPORTANT: Use False (not DontCare) when style is Normal.
3870        // DontCare means "accept italic too" which can match italic fonts.
3871        // False means "must NOT be italic" which correctly prefers Normal.
3872        let italic = if is_italic {
3873            PatternMatch::True
3874        } else {
3875            PatternMatch::False
3876        };
3877        let oblique = if is_oblique {
3878            PatternMatch::True
3879        } else {
3880            PatternMatch::False
3881        };
3882
3883        // Registry-aware resolve: scout-on-demand path when available.
3884        // See `resolve_font_chains_with_registry` doc for rationale.
3885        let chain = if let Some(reg) = registry {
3886            reg.request_and_resolve_with_scripts(
3887                &font_families, weight, italic, oblique, scripts_hint,
3888            )
3889        } else {
3890            let mut trace = Vec::new();
3891            fc_cache.resolve_font_chain_with_scripts(
3892                &font_families, weight, italic, oblique, scripts_hint, &mut trace,
3893            )
3894        };
3895
3896        chains.insert(cache_key, chain);
3897    }
3898
3899    // NOTE: FontRefs bypass fontconfig entirely — the shaping code checks
3900    // style.font_stack for FontStack::Ref and uses the font data directly.
3901    // No entries are inserted into `chains` for them.
3902
3903    ResolvedFontChains { chains }
3904}
3905
3906/// Convenience function that collects and resolves font chains in one call
3907///
3908/// # Arguments
3909/// * `styled_dom` - The styled DOM to extract font stacks from
3910/// * `fc_cache` - The fontconfig cache to resolve fonts against
3911/// * `platform` - The current platform for resolving system font types
3912///
3913/// # Returns
3914/// A `ResolvedFontChains` containing all resolved font chains
3915/// Collect font stacks, register embedded fonts, and resolve font chains
3916/// in a single pass over the DOM nodes. Replaces the old two-pass approach
3917/// where `register_embedded_fonts_from_styled_dom` + `collect_and_resolve_font_chains`
3918/// each independently scanned all nodes.
3919pub fn collect_and_resolve_font_chains_with_registration<T: crate::font_traits::ParsedFontTrait>(
3920    styled_dom: &StyledDom,
3921    fc_cache: &FcFontCache,
3922    font_manager: &crate::text3::cache::FontManager<T>,
3923    platform: &azul_css::system::Platform,
3924) -> ResolvedFontChains {
3925    let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
3926
3927    // Register embedded FontRefs (from the same scan, no second pass)
3928    for (_ptr, font_ref) in &collected.font_refs {
3929        font_manager.register_embedded_font(font_ref);
3930    }
3931
3932    // Fast path (rust-fontconfig 4.2): when a registry is attached
3933    // we can resolve each stack by cmap-probing candidate files
3934    // against the codepoints the DOM actually uses, instead of
3935    // letting `request_fonts` eagerly parse every CSS fallback
3936    // via allsorts. On excel.html this drops `font_chain_resolve`
3937    // from ~128 ms / 49 faces parsed to ~5 ms / 3 faces.
3938    //
3939    // Falls back to the legacy pattern-map resolver when:
3940    //   - no registry is present (offline `FcFontCache` callers)
3941    //   - the DOM has no text codepoints (no shaping to be done,
3942    //     so cmap-probing has nothing to check and partial-cover
3943    //     entries would be surprising)
3944    if let Some(registry) = font_manager.registry.as_deref() {
3945        let used_chars = collect_used_codepoints_all(styled_dom);
3946        if !used_chars.is_empty() {
3947            return resolve_font_chains_fast(
3948                &collected,
3949                registry,
3950                &used_chars,
3951            );
3952        }
3953    }
3954
3955    // Legacy path: pattern-map resolver. Only reached when the
3956    // caller passes an `FcFontCache` without a live registry
3957    // (ad-hoc tests, the PDF writer, etc.).
3958    let scripts = scripts_present_in_styled_dom(styled_dom);
3959    let mut resolved = resolve_font_chains_with_registry(
3960        &collected,
3961        fc_cache,
3962        font_manager.registry.as_deref(),
3963        Some(&scripts),
3964    );
3965
3966    let used_chars = collect_used_codepoints(styled_dom);
3967    for chain in resolved.chains.values_mut() {
3968        prune_chain_to_used_chars(chain, &used_chars);
3969    }
3970    resolved
3971}
3972
3973/// Fast-path resolver backed by [`FcFontRegistry::request_fonts_fast`].
3974///
3975/// Iterates `collected.font_stacks`, shapes each `(stack, weight,
3976/// italic, oblique)` combo into a cmap-probe request carrying the
3977/// DOM's codepoint set, calls the registry, and returns a
3978/// `ResolvedFontChains` keyed by `FontChainKeyOrRef::Chain` — the
3979/// same keys the legacy resolver emits, so downstream code
3980/// (`load_missing_for_chains`, `shape_with_font_fallback`) is
3981/// unchanged.
3982pub fn resolve_font_chains_fast(
3983    collected: &CollectedFontStacks,
3984    registry: &rust_fontconfig::registry::FcFontRegistry,
3985    codepoints: &std::collections::BTreeSet<char>,
3986) -> ResolvedFontChains {
3987    use rust_fontconfig::PatternMatch;
3988
3989    static DBG: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
3990    let dbg = *DBG.get_or_init(|| std::env::var_os("AZ_FAST_RESOLVE_DEBUG").is_some());
3991
3992    let mut chains: HashMap<FontChainKeyOrRef, rust_fontconfig::FontFallbackChain> =
3993        HashMap::new();
3994
3995    for font_stack in &collected.font_stacks {
3996        if font_stack.is_empty() {
3997            continue;
3998        }
3999
4000        let font_families: Vec<String> = font_stack
4001            .iter()
4002            .map(|s| s.family.clone())
4003            .filter(|f| !f.is_empty())
4004            .collect();
4005
4006        let font_families = if font_families.is_empty() {
4007            vec!["sans-serif".to_string()]
4008        } else {
4009            font_families
4010        };
4011
4012        let weight = font_stack[0].weight;
4013        let is_italic = font_stack[0].style == FontStyle::Italic;
4014        let is_oblique = font_stack[0].style == FontStyle::Oblique;
4015
4016        let cache_key = FontChainKeyOrRef::Chain(FontChainKey {
4017            font_families: font_families.clone(),
4018            weight,
4019            italic: is_italic,
4020            oblique: is_oblique,
4021        });
4022
4023        if chains.contains_key(&cache_key) {
4024            continue;
4025        }
4026
4027        let italic_match = if is_italic {
4028            PatternMatch::True
4029        } else {
4030            PatternMatch::False
4031        };
4032
4033        let request = vec![(font_families.clone(), codepoints.clone())];
4034        let mut chains_out = registry.request_fonts_fast(&request, weight, italic_match);
4035        if dbg {
4036            let total_fonts: usize = chains_out
4037                .iter()
4038                .map(|c| c.css_fallbacks.iter().map(|g| g.fonts.len()).sum::<usize>())
4039                .sum();
4040            eprintln!(
4041                "[FAST] stack {:?} w={:?} i={:?} → {} groups, {} faces",
4042                font_families,
4043                weight,
4044                italic_match,
4045                chains_out.first().map(|c| c.css_fallbacks.len()).unwrap_or(0),
4046                total_fonts,
4047            );
4048        }
4049        if let Some(chain) = chains_out.pop() {
4050            chains.insert(cache_key, chain);
4051        }
4052    }
4053
4054    ResolvedFontChains { chains }
4055}
4056
4057/// Legacy wrapper: collect + resolve without registration. Kept for
4058/// backward compatibility; defaults to the full 7-script unicode
4059/// fallback set.
4060pub fn collect_and_resolve_font_chains(
4061    styled_dom: &StyledDom,
4062    fc_cache: &FcFontCache,
4063    platform: &azul_css::system::Platform,
4064) -> ResolvedFontChains {
4065    let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
4066    resolve_font_chains(&collected, fc_cache, None)
4067}
4068
4069/// Legacy wrapper: register only. Prefer `collect_and_resolve_font_chains_with_registration`.
4070pub fn register_embedded_fonts_from_styled_dom<T: crate::font_traits::ParsedFontTrait>(
4071    styled_dom: &StyledDom,
4072    font_manager: &crate::text3::cache::FontManager<T>,
4073    platform: &azul_css::system::Platform,
4074) {
4075    let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
4076    for (_ptr, font_ref) in &collected.font_refs {
4077        font_manager.register_embedded_font(font_ref);
4078    }
4079}
4080
4081// Font Loading Functions
4082
4083use std::collections::HashSet;
4084
4085use rust_fontconfig::FontId;
4086
4087/// Extract all unique FontIds from resolved font chains
4088///
4089/// This function collects all FontIds that are referenced in the font chains,
4090/// which represents the complete set of fonts that may be needed for rendering.
4091pub fn collect_font_ids_from_chains(chains: &ResolvedFontChains) -> HashSet<FontId> {
4092    let mut font_ids = HashSet::new();
4093
4094    // M12.7: hashbrown's RawIterRange (the .values() iterator below) mis-lifts
4095    // to wasm and loops forever on an empty map; is_empty() is len-based, so
4096    // bail out before iterating when there are no chains (web bare-body case).
4097    if chains.chains.is_empty() {
4098        return font_ids;
4099    }
4100
4101    for chain in chains.chains.values() {
4102        // Collect from CSS fallbacks
4103        for group in &chain.css_fallbacks {
4104            for font in &group.fonts {
4105                font_ids.insert(font.id);
4106            }
4107        }
4108
4109        // Collect from Unicode fallbacks
4110        for font in &chain.unicode_fallbacks {
4111            font_ids.insert(font.id);
4112        }
4113    }
4114
4115    font_ids
4116}
4117
4118/// Compute which fonts need to be loaded (diff with already loaded fonts)
4119///
4120/// # Arguments
4121/// * `required_fonts` - Set of FontIds that are needed
4122/// * `already_loaded` - Set of FontIds that are already loaded
4123///
4124/// # Returns
4125/// Set of FontIds that need to be loaded
4126pub fn compute_fonts_to_load(
4127    required_fonts: &HashSet<FontId>,
4128    already_loaded: &HashSet<FontId>,
4129) -> HashSet<FontId> {
4130    // M12.7: `.difference()` drives hashbrown's RawIterRange, which mis-lifts
4131    // to wasm and loops on an empty map. Nothing required → nothing to load.
4132    if required_fonts.is_empty() {
4133        return HashSet::new();
4134    }
4135    required_fonts.difference(already_loaded).cloned().collect()
4136}
4137
4138/// Result of loading fonts
4139#[derive(Debug)]
4140pub struct FontLoadResult<T> {
4141    /// Successfully loaded fonts
4142    pub loaded: HashMap<FontId, T>,
4143    /// FontIds that failed to load, with error messages
4144    pub failed: Vec<(FontId, String)>,
4145}
4146
4147/// Load fonts from disk using the provided loader function
4148///
4149/// This is a generic function that works with any font loading implementation.
4150/// The `load_fn` parameter should be a function that takes font bytes and an index,
4151/// and returns a parsed font or an error.
4152///
4153/// # Arguments
4154/// * `font_ids` - Set of FontIds to load
4155/// * `fc_cache` - The fontconfig cache to get font paths from
4156/// * `load_fn` - Function to load and parse font bytes
4157///
4158/// # Returns
4159/// A `FontLoadResult` containing successfully loaded fonts and any failures
4160pub fn load_fonts_from_disk<T, F>(
4161    font_ids: &HashSet<FontId>,
4162    fc_cache: &FcFontCache,
4163    load_fn: F,
4164) -> FontLoadResult<T>
4165where
4166    // Bytes come in as `Arc<FontBytes>` so the loader can retain
4167    // them cheaply (one `Arc::clone` per retained copy). On disk the
4168    // backing is an mmap, so untouched glyf/CFF pages don't count
4169    // toward RSS — the layout shaper only faults in pages it reads.
4170    F: Fn(std::sync::Arc<rust_fontconfig::FontBytes>, usize) -> Result<T, crate::text3::cache::LayoutError>,
4171{
4172    let mut loaded = HashMap::new();
4173    let mut failed = Vec::new();
4174
4175    for font_id in font_ids {
4176        // Get font bytes from fc_cache as a shared mmap. Faces backed
4177        // by the same .ttc all observe the same `Arc<FontBytes>` via
4178        // rust_fontconfig's `shared_bytes` dedup.
4179        let font_bytes = match fc_cache.get_font_bytes(font_id) {
4180            Some(bytes) => bytes,
4181            None => {
4182                failed.push((
4183                    *font_id,
4184                    format!("Could not get font bytes for {:?}", font_id),
4185                ));
4186                continue;
4187            }
4188        };
4189
4190        // Get font index (for font collections like .ttc files)
4191        let font_index = fc_cache
4192            .get_font_by_id(font_id)
4193            .and_then(|source| match source {
4194                rust_fontconfig::OwnedFontSource::Disk(path) => Some(path.font_index),
4195                rust_fontconfig::OwnedFontSource::Memory(font) => Some(font.font_index),
4196            })
4197            .unwrap_or(0) as usize;
4198
4199        // Load the font using the provided function
4200        match load_fn(font_bytes, font_index) {
4201            Ok(font) => {
4202                loaded.insert(*font_id, font);
4203            }
4204            Err(e) => {
4205                failed.push((
4206                    *font_id,
4207                    format!("Failed to parse font {:?}: {:?}", font_id, e),
4208                ));
4209            }
4210        }
4211    }
4212
4213    FontLoadResult { loaded, failed }
4214}
4215
4216/// Convenience function to load all required fonts for a styled DOM
4217///
4218/// This function:
4219/// 1. Collects all font stacks from the DOM
4220/// 2. Resolves them to font chains
4221/// 3. Extracts all required FontIds
4222/// 4. Computes which fonts need to be loaded (diff with already loaded)
4223/// 5. Loads the missing fonts
4224///
4225/// # Arguments
4226/// * `styled_dom` - The styled DOM to extract font requirements from
4227/// * `fc_cache` - The fontconfig cache
4228/// * `already_loaded` - Set of FontIds that are already loaded
4229/// * `load_fn` - Function to load and parse font bytes
4230/// * `platform` - The current platform for resolving system font types
4231///
4232/// # Returns
4233/// A tuple of (ResolvedFontChains, FontLoadResult)
4234pub fn resolve_and_load_fonts<T, F>(
4235    styled_dom: &StyledDom,
4236    fc_cache: &FcFontCache,
4237    already_loaded: &HashSet<FontId>,
4238    load_fn: F,
4239    platform: &azul_css::system::Platform,
4240) -> (ResolvedFontChains, FontLoadResult<T>)
4241where
4242    F: Fn(std::sync::Arc<rust_fontconfig::FontBytes>, usize) -> Result<T, crate::text3::cache::LayoutError>,
4243{
4244    // Step 1-2: Collect and resolve font chains
4245    let chains = collect_and_resolve_font_chains(styled_dom, fc_cache, platform);
4246
4247    // Step 3: Extract all required FontIds
4248    let required_fonts = collect_font_ids_from_chains(&chains);
4249
4250    // Step 4: Compute diff
4251    let fonts_to_load = compute_fonts_to_load(&required_fonts, already_loaded);
4252
4253    // Step 5: Load missing fonts
4254    let load_result = load_fonts_from_disk(&fonts_to_load, fc_cache, load_fn);
4255
4256    (chains, load_result)
4257}
4258
4259// ============================================================================
4260// Scrollbar Style Getters
4261// ============================================================================
4262
4263use azul_css::props::style::scrollbar::{
4264    LayoutScrollbarWidth, ScrollbarVisibilityMode,
4265    StyleScrollbarColor,
4266};
4267
4268/// Computed scrollbar style for a node.
4269///
4270/// All visual defaults (colors, width) come from the UA CSS conditional rules
4271/// in `core/src/ua_css.rs` — individual `CssPropertyWithConditions` entries for
4272/// `scrollbar-color` and `scrollbar-width`, keyed on `@os` / `@theme`.
4273///
4274/// Overlay behaviour (fade timing, visibility, clip) is derived from the
4275/// resolved `scrollbar-width` mode:
4276///   - `thin`  → overlay:  fade 500/200 ms, `WhenScrolling`, clip = true
4277///   - `auto`  → classic:  no fade, `Always`, clip = false
4278///   - `none`  → hidden:   no fade, `Always`, clip = false
4279///
4280/// Per-node CSS overrides (in priority order):
4281///   1. `-azul-scrollbar-style`  (full `ScrollbarInfo` override)
4282///   2. `scrollbar-width`        (overrides width + overlay mode)
4283///   3. `scrollbar-color`        (overrides thumb / track colours)
4284#[derive(Debug, Clone)]
4285pub struct ComputedScrollbarStyle {
4286    /// The scrollbar width mode (auto/thin/none)
4287    pub width_mode: LayoutScrollbarWidth,
4288    /// Visual width in pixels — used for rendering track + thumb.
4289    /// Non-zero even for overlay scrollbars.
4290    pub visual_width_px: f32,
4291    /// Reserve width in pixels — layout space subtracted from content area.
4292    /// 0 for overlay scrollbars, equal to `visual_width_px` for legacy.
4293    pub reserve_width_px: f32,
4294    /// Thumb color
4295    pub thumb_color: ColorU,
4296    /// Track color
4297    pub track_color: ColorU,
4298    /// Button color (for scroll arrows)
4299    pub button_color: ColorU,
4300    /// Corner color (where scrollbars meet)
4301    pub corner_color: ColorU,
4302    /// Whether to clip the scrollbar to the container's border-radius
4303    pub clip_to_container_border: bool,
4304    /// Delay in ms before scrollbar starts fading out (0 = never fade)
4305    pub fade_delay_ms: u32,
4306    /// Duration of fade-out animation in ms (0 = instant)
4307    pub fade_duration_ms: u32,
4308    /// Scrollbar visibility mode (always / when-scrolling / auto)
4309    pub visibility: ScrollbarVisibilityMode,
4310    /// Whether to show top/bottom (or left/right) arrow buttons.
4311    /// When false, the track spans the entire scrollbar length.
4312    pub show_scroll_buttons: bool,
4313    /// Size of each arrow button in px (square: width = height).
4314    /// Only used when `show_scroll_buttons == true`.
4315    pub scroll_button_size_px: f32,
4316    /// Whether to show the corner rect where V and H scrollbars meet.
4317    pub show_corner_rect: bool,
4318    /// Thumb color when hovered (None = use thumb_color)
4319    pub thumb_color_hover: Option<ColorU>,
4320    /// Thumb color when pressed/active (None = use thumb_color)
4321    pub thumb_color_active: Option<ColorU>,
4322    /// Track color when hovered (None = use track_color)
4323    pub track_color_hover: Option<ColorU>,
4324    /// Visual width when hovered (None = use visual_width_px)
4325    pub visual_width_px_hover: Option<f32>,
4326    /// Visual width when pressed (None = use visual_width_px)
4327    pub visual_width_px_active: Option<f32>,
4328}
4329
4330impl Default for ComputedScrollbarStyle {
4331    fn default() -> Self {
4332        // Evaluate UA CSS rules with a default context (no OS info).
4333        // Picks the unconditional fallback: classic light, auto width.
4334        let ctx = azul_css::dynamic_selector::DynamicSelectorContext::default();
4335        let ua = azul_core::ua_css::evaluate_ua_scrollbar_css(&ctx);
4336        Self::from_ua_resolved(&ua)
4337    }
4338}
4339
4340impl ComputedScrollbarStyle {
4341    /// Build from resolved UA scrollbar CSS properties.
4342    ///
4343    /// Each property is read individually from the resolved UA CSS.
4344    fn from_ua_resolved(ua: &azul_core::ua_css::ResolvedUaScrollbar) -> Self {
4345        let width_mode = ua.width;
4346        let visibility = ua.visibility;
4347        let fade_delay_ms = ua.fade_delay.ms;
4348        let fade_duration_ms = ua.fade_duration.ms;
4349
4350        let visual_width_px = match width_mode {
4351            LayoutScrollbarWidth::Thin => 8.0,
4352            LayoutScrollbarWidth::Auto => 12.0,
4353            LayoutScrollbarWidth::None => 0.0,
4354        };
4355
4356        // Overlay scrollbars don't reserve layout space.
4357        let reserve_width_px = if visibility == ScrollbarVisibilityMode::WhenScrolling {
4358            0.0
4359        } else {
4360            visual_width_px
4361        };
4362
4363        let clip = visibility == ScrollbarVisibilityMode::WhenScrolling;
4364
4365        // Overlay scrollbars hide buttons and corner by default.
4366        let is_overlay = visibility == ScrollbarVisibilityMode::WhenScrolling;
4367        let show_scroll_buttons = !is_overlay;
4368        let scroll_button_size_px = if is_overlay { 0.0 } else { visual_width_px };
4369        let show_corner_rect = !is_overlay;
4370
4371        let (thumb_color, track_color) = match ua.color {
4372            StyleScrollbarColor::Custom(c) => (c.thumb, c.track),
4373            _ => (ColorU::TRANSPARENT, ColorU::TRANSPARENT),
4374        };
4375
4376        // Compute hover / active variants:
4377        // Hover: lighten thumb by ~20%, widen by +4px
4378        // Active: darken thumb by ~10%, widen by +4px
4379        let thumb_hover = ColorU {
4380            r: thumb_color.r.saturating_add(30),
4381            g: thumb_color.g.saturating_add(30),
4382            b: thumb_color.b.saturating_add(30),
4383            a: thumb_color.a.saturating_add(40).min(255),
4384        };
4385        let thumb_active = ColorU {
4386            r: thumb_color.r.saturating_sub(15),
4387            g: thumb_color.g.saturating_sub(15),
4388            b: thumb_color.b.saturating_sub(15),
4389            a: 255,  // fully opaque when pressed
4390        };
4391        let track_hover = ColorU {
4392            r: track_color.r,
4393            g: track_color.g,
4394            b: track_color.b,
4395            a: track_color.a.saturating_add(40).min(255),
4396        };
4397        let hover_width = visual_width_px + 4.0;
4398        let active_width = visual_width_px + 4.0;
4399
4400        Self {
4401            width_mode,
4402            visual_width_px,
4403            reserve_width_px,
4404            thumb_color,
4405            track_color,
4406            button_color: ColorU::TRANSPARENT,
4407            corner_color: ColorU::TRANSPARENT,
4408            clip_to_container_border: clip,
4409            fade_delay_ms,
4410            fade_duration_ms,
4411            visibility,
4412            show_scroll_buttons,
4413            scroll_button_size_px,
4414            show_corner_rect,
4415            thumb_color_hover: Some(thumb_hover),
4416            thumb_color_active: Some(thumb_active),
4417            track_color_hover: Some(track_hover),
4418            visual_width_px_hover: Some(hover_width),
4419            visual_width_px_active: Some(active_width),
4420        }
4421    }
4422}
4423
4424/// Get the computed scrollbar style for a node.
4425///
4426/// Resolution order (later wins):
4427///   1. UA scrollbar CSS (`CssPropertyWithConditions` in `ua_css.rs`,
4428///      evaluated via `@os` / `@theme` conditions)
4429///   2. CSS `-azul-scrollbar-style` (full `ScrollbarInfo` customisation)
4430///   3. CSS `scrollbar-width`  (overrides width only)
4431///   4. CSS `scrollbar-color`  (overrides thumb / track colours)
4432///   5. CSS `-azul-scrollbar-visibility` (overrides visibility + clip)
4433///   6. CSS `-azul-scrollbar-fade-delay` (overrides fade delay)
4434///   7. CSS `-azul-scrollbar-fade-duration` (overrides fade duration)
4435///
4436/// When `system_style` is `None`, falls back to the unconditional UA rule
4437/// (classic light scrollbar).
4438pub fn get_scrollbar_style(
4439    styled_dom: &StyledDom,
4440    node_id: NodeId,
4441    node_state: &StyledNodeState,
4442    system_style: Option<&azul_css::system::SystemStyle>,
4443) -> ComputedScrollbarStyle {
4444    let node_data = &styled_dom.node_data.as_container()[node_id];
4445
4446    // Step 1: Evaluate UA scrollbar CSS using the DynamicSelector system.
4447    let ctx = match system_style {
4448        Some(sys) => {
4449            azul_css::dynamic_selector::DynamicSelectorContext::from_system_style(sys)
4450        }
4451        None => azul_css::dynamic_selector::DynamicSelectorContext::default(),
4452    };
4453    let ua = azul_core::ua_css::evaluate_ua_scrollbar_css(&ctx);
4454    let result = ComputedScrollbarStyle::from_ua_resolved(&ua);
4455
4456    // FAST PATH: 99% of nodes have no scrollbar CSS. Bail before walking 8 × cascade.
4457    if node_state.is_normal() {
4458        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
4459            if !cc.has_scrollbar_css(node_id.index()) {
4460                return result;
4461            }
4462        }
4463    }
4464    let mut result = result;
4465
4466    // Step 2: Check individual scrollbar part backgrounds
4467    if let Some(track) = styled_dom
4468        .css_property_cache
4469        .ptr
4470        .get_scrollbar_track(node_data, &node_id, node_state)
4471        .and_then(|v| v.get_property())
4472    {
4473        result.track_color = extract_color_from_background(track);
4474    }
4475    if let Some(thumb) = styled_dom
4476        .css_property_cache
4477        .ptr
4478        .get_scrollbar_thumb(node_data, &node_id, node_state)
4479        .and_then(|v| v.get_property())
4480    {
4481        result.thumb_color = extract_color_from_background(thumb);
4482    }
4483    if let Some(button) = styled_dom
4484        .css_property_cache
4485        .ptr
4486        .get_scrollbar_button(node_data, &node_id, node_state)
4487        .and_then(|v| v.get_property())
4488    {
4489        result.button_color = extract_color_from_background(button);
4490    }
4491    if let Some(corner) = styled_dom
4492        .css_property_cache
4493        .ptr
4494        .get_scrollbar_corner(node_data, &node_id, node_state)
4495        .and_then(|v| v.get_property())
4496    {
4497        result.corner_color = extract_color_from_background(corner);
4498    }
4499
4500    // Step 3: Check for scrollbar-width (overrides width only, not overlay)
4501    if let Some(scrollbar_width) = styled_dom
4502        .css_property_cache
4503        .ptr
4504        .get_scrollbar_width(node_data, &node_id, node_state)
4505        .and_then(|v| v.get_property())
4506    {
4507        result.width_mode = *scrollbar_width;
4508        let w = match scrollbar_width {
4509            LayoutScrollbarWidth::Auto => 12.0,
4510            LayoutScrollbarWidth::Thin => 8.0,
4511            LayoutScrollbarWidth::None => 0.0,
4512        };
4513        result.visual_width_px = w;
4514        if result.visibility != ScrollbarVisibilityMode::WhenScrolling {
4515            result.reserve_width_px = w;
4516        }
4517    }
4518
4519    // Step 4: Check for scrollbar-color (overrides thumb/track colors)
4520    if let Some(scrollbar_color) = styled_dom
4521        .css_property_cache
4522        .ptr
4523        .get_scrollbar_color(node_data, &node_id, node_state)
4524        .and_then(|v| v.get_property())
4525    {
4526        match scrollbar_color {
4527            StyleScrollbarColor::Auto => { /* keep */ }
4528            StyleScrollbarColor::Custom(custom) => {
4529                result.thumb_color = custom.thumb;
4530                result.track_color = custom.track;
4531            }
4532        }
4533    }
4534
4535    // Step 5: Check for -azul-scrollbar-visibility
4536    if let Some(vis) = styled_dom
4537        .css_property_cache
4538        .ptr
4539        .get_scrollbar_visibility(node_data, &node_id, node_state)
4540        .and_then(|v| v.get_property())
4541    {
4542        result.visibility = *vis;
4543        result.clip_to_container_border = *vis == ScrollbarVisibilityMode::WhenScrolling;
4544        // Overlay mode: no reserved layout space, hide buttons and corner
4545        let is_overlay = *vis == ScrollbarVisibilityMode::WhenScrolling;
4546        if is_overlay {
4547            result.reserve_width_px = 0.0;
4548            result.show_scroll_buttons = false;
4549            result.scroll_button_size_px = 0.0;
4550            result.show_corner_rect = false;
4551        } else {
4552            result.reserve_width_px = result.visual_width_px;
4553        }
4554    }
4555
4556    // Step 6: Check for -azul-scrollbar-fade-delay
4557    if let Some(delay) = styled_dom
4558        .css_property_cache
4559        .ptr
4560        .get_scrollbar_fade_delay(node_data, &node_id, node_state)
4561        .and_then(|v| v.get_property())
4562    {
4563        result.fade_delay_ms = delay.ms;
4564    }
4565
4566    // Step 7: Check for -azul-scrollbar-fade-duration
4567    if let Some(dur) = styled_dom
4568        .css_property_cache
4569        .ptr
4570        .get_scrollbar_fade_duration(node_data, &node_id, node_state)
4571        .and_then(|v| v.get_property())
4572    {
4573        result.fade_duration_ms = dur.ms;
4574    }
4575
4576    result
4577}
4578
4579/// Cached wrapper for [`get_scrollbar_style`] that reuses the
4580/// memo stored on `LayoutContext`. The underlying call performs
4581/// 9 cascade walks per node (track/thumb/button/corner/width/
4582/// color/visibility/fade-delay/fade-duration). The BFC, Taffy,
4583/// and display-list callers all hit the same node many times
4584/// inside a single layout pass, so caching turns ~21 rebuilds per
4585/// node into one.
4586///
4587/// Falls back to the uncached `get_scrollbar_style` when no ctx
4588/// is available (shouldn't happen in the current code paths).
4589pub fn get_scrollbar_style_cached<T: crate::font_traits::ParsedFontTrait>(
4590    ctx: &crate::solver3::LayoutContext<'_, T>,
4591    node_id: NodeId,
4592    node_state: &StyledNodeState,
4593) -> ComputedScrollbarStyle {
4594    if let Some(s) = ctx.scrollbar_style_cache.borrow().get(&node_id) {
4595        return s.clone();
4596    }
4597    let style = get_scrollbar_style(
4598        ctx.styled_dom,
4599        node_id,
4600        node_state,
4601        ctx.system_style.as_deref(),
4602    );
4603    ctx.scrollbar_style_cache.borrow_mut().insert(node_id, style.clone());
4604    style
4605}
4606
4607/// Helper to extract a solid color from a StyleBackgroundContent
4608fn extract_color_from_background(
4609    bg: &azul_css::props::style::background::StyleBackgroundContent,
4610) -> ColorU {
4611    use azul_css::props::style::background::StyleBackgroundContent;
4612    match bg {
4613        StyleBackgroundContent::Color(c) => *c,
4614        _ => ColorU::TRANSPARENT,
4615    }
4616}
4617
4618/// Check if a node should clip its scrollbar to the container's border-radius
4619pub fn should_clip_scrollbar_to_border(
4620    styled_dom: &StyledDom,
4621    node_id: NodeId,
4622    node_state: &StyledNodeState,
4623) -> bool {
4624    let style = get_scrollbar_style(styled_dom, node_id, node_state, None);
4625    style.clip_to_container_border
4626}
4627
4628/// Get the scrollbar visual width in pixels for a node (used for rendering)
4629pub fn get_scrollbar_width_px(
4630    styled_dom: &StyledDom,
4631    node_id: NodeId,
4632    node_state: &StyledNodeState,
4633) -> f32 {
4634    let style = get_scrollbar_style(styled_dom, node_id, node_state, None);
4635    style.visual_width_px
4636}
4637
4638/// Checks if text in a node is selectable based on CSS `user-select` property.
4639///
4640/// Returns `true` if the text can be selected (default behavior),
4641/// `false` if `user-select: none` is set.
4642pub fn is_text_selectable(
4643    styled_dom: &StyledDom,
4644    node_id: NodeId,
4645    node_state: &StyledNodeState,
4646) -> bool {
4647    let node_data = &styled_dom.node_data.as_container()[node_id];
4648    
4649    styled_dom
4650        .css_property_cache
4651        .ptr
4652        .get_user_select(node_data, &node_id, node_state)
4653        .and_then(|v| v.get_property())
4654        .map(|us| *us != StyleUserSelect::None)
4655        .unwrap_or(true) // Default: text is selectable
4656}
4657
4658/// Checks if a node has the `contenteditable` attribute set directly.
4659///
4660/// Returns `true` if:
4661/// - The node has `contenteditable: true` set via `.set_contenteditable(true)`
4662/// - OR the node has `contenteditable` attribute set to `true`
4663///
4664/// This does NOT check inheritance - use `is_node_contenteditable_inherited` for that.
4665pub fn is_node_contenteditable(styled_dom: &StyledDom, node_id: NodeId) -> bool {
4666    use azul_core::dom::AttributeType;
4667    
4668    let node_data = &styled_dom.node_data.as_container()[node_id];
4669    
4670    // First check the direct contenteditable field (primary method)
4671    if node_data.is_contenteditable() {
4672        return true;
4673    }
4674    
4675    // Also check the attribute for backwards compatibility
4676    // Only return true if the attribute value is explicitly true
4677    node_data.attributes().as_ref().iter().any(|attr| {
4678        matches!(attr, AttributeType::ContentEditable(true))
4679    })
4680}
4681// =============================================================================
4682// Additional ExtractPropertyValue impls (not in compact cache tier 1/2)
4683// =============================================================================
4684
4685use azul_css::props::layout::text::LayoutTextJustify;
4686use azul_css::props::layout::table::{LayoutTableLayout, StyleBorderCollapse, StyleCaptionSide, StyleEmptyCells};
4687use azul_css::props::style::text::StyleHyphens;
4688use azul_css::props::style::text::StyleWordBreak;
4689use azul_css::props::style::text::StyleOverflowWrap;
4690use azul_css::props::style::text::StyleLineBreak;
4691use azul_css::props::style::text::StyleTextAlignLast;
4692use azul_css::props::style::effects::StyleCursor;
4693use azul_css::props::style::effects::StyleObjectFit;
4694use azul_css::props::style::effects::StyleObjectPosition;
4695use azul_css::props::style::effects::StyleAspectRatio;
4696use azul_css::props::style::effects::StyleTextOrientation;
4697
4698impl ExtractPropertyValue<LayoutTextJustify> for CssProperty {
4699    fn extract(&self) -> Option<LayoutTextJustify> {
4700        match self {
4701            Self::TextJustify(CssPropertyValue::Exact(v)) => Some(*v),
4702            _ => None,
4703        }
4704    }
4705}
4706
4707impl ExtractPropertyValue<StyleHyphens> for CssProperty {
4708    fn extract(&self) -> Option<StyleHyphens> {
4709        match self {
4710            Self::Hyphens(CssPropertyValue::Exact(v)) => Some(*v),
4711            _ => None,
4712        }
4713    }
4714}
4715
4716impl ExtractPropertyValue<StyleWordBreak> for CssProperty {
4717    fn extract(&self) -> Option<StyleWordBreak> {
4718        match self {
4719            Self::WordBreak(CssPropertyValue::Exact(v)) => Some(*v),
4720            _ => None,
4721        }
4722    }
4723}
4724
4725impl ExtractPropertyValue<StyleOverflowWrap> for CssProperty {
4726    fn extract(&self) -> Option<StyleOverflowWrap> {
4727        match self {
4728            Self::OverflowWrap(CssPropertyValue::Exact(v)) => Some(*v),
4729            _ => None,
4730        }
4731    }
4732}
4733
4734impl ExtractPropertyValue<StyleLineBreak> for CssProperty {
4735    fn extract(&self) -> Option<StyleLineBreak> {
4736        match self {
4737            Self::LineBreak(CssPropertyValue::Exact(v)) => Some(*v),
4738            _ => None,
4739        }
4740    }
4741}
4742
4743impl ExtractPropertyValue<StyleTextAlignLast> for CssProperty {
4744    fn extract(&self) -> Option<StyleTextAlignLast> {
4745        match self {
4746            Self::TextAlignLast(CssPropertyValue::Exact(v)) => Some(*v),
4747            _ => None,
4748        }
4749    }
4750}
4751
4752impl ExtractPropertyValue<StyleObjectFit> for CssProperty {
4753    fn extract(&self) -> Option<StyleObjectFit> {
4754        match self {
4755            Self::ObjectFit(CssPropertyValue::Exact(v)) => Some(*v),
4756            _ => None,
4757        }
4758    }
4759}
4760
4761impl ExtractPropertyValue<StyleTextOrientation> for CssProperty {
4762    fn extract(&self) -> Option<StyleTextOrientation> {
4763        match self {
4764            Self::TextOrientation(CssPropertyValue::Exact(v)) => Some(*v),
4765            _ => None,
4766        }
4767    }
4768}
4769
4770impl ExtractPropertyValue<StyleObjectPosition> for CssProperty {
4771    fn extract(&self) -> Option<StyleObjectPosition> {
4772        match self {
4773            Self::ObjectPosition(CssPropertyValue::Exact(v)) => Some(*v),
4774            _ => None,
4775        }
4776    }
4777}
4778
4779impl ExtractPropertyValue<StyleAspectRatio> for CssProperty {
4780    fn extract(&self) -> Option<StyleAspectRatio> {
4781        match self {
4782            Self::AspectRatio(CssPropertyValue::Exact(v)) => Some(*v),
4783            _ => None,
4784        }
4785    }
4786}
4787
4788impl ExtractPropertyValue<LayoutTableLayout> for CssProperty {
4789    fn extract(&self) -> Option<LayoutTableLayout> {
4790        match self {
4791            Self::TableLayout(CssPropertyValue::Exact(v)) => Some(*v),
4792            _ => None,
4793        }
4794    }
4795}
4796
4797impl ExtractPropertyValue<StyleBorderCollapse> for CssProperty {
4798    fn extract(&self) -> Option<StyleBorderCollapse> {
4799        match self {
4800            Self::BorderCollapse(CssPropertyValue::Exact(v)) => Some(*v),
4801            _ => None,
4802        }
4803    }
4804}
4805
4806impl ExtractPropertyValue<StyleCaptionSide> for CssProperty {
4807    fn extract(&self) -> Option<StyleCaptionSide> {
4808        match self {
4809            Self::CaptionSide(CssPropertyValue::Exact(v)) => Some(*v),
4810            _ => None,
4811        }
4812    }
4813}
4814
4815impl ExtractPropertyValue<StyleEmptyCells> for CssProperty {
4816    fn extract(&self) -> Option<StyleEmptyCells> {
4817        match self {
4818            Self::EmptyCells(CssPropertyValue::Exact(v)) => Some(*v),
4819            _ => None,
4820        }
4821    }
4822}
4823
4824impl ExtractPropertyValue<StyleCursor> for CssProperty {
4825    fn extract(&self) -> Option<StyleCursor> {
4826        match self {
4827            Self::Cursor(CssPropertyValue::Exact(v)) => Some(v.clone()),
4828            _ => None,
4829        }
4830    }
4831}
4832
4833// =============================================================================
4834// Additional macro-based getters (not covered by compact cache fast-path getters)
4835// =============================================================================
4836
4837get_css_property!(
4838    get_text_justify,
4839    get_text_justify,
4840    LayoutTextJustify,
4841    CssPropertyType::TextJustify
4842);
4843
4844get_css_property!(
4845    get_hyphens,
4846    get_hyphens,
4847    StyleHyphens,
4848    CssPropertyType::Hyphens
4849);
4850
4851get_css_property!(
4852    get_word_break,
4853    get_word_break,
4854    StyleWordBreak,
4855    CssPropertyType::WordBreak
4856);
4857
4858get_css_property!(
4859    get_overflow_wrap,
4860    get_overflow_wrap,
4861    StyleOverflowWrap,
4862    CssPropertyType::OverflowWrap
4863);
4864
4865get_css_property!(
4866    get_line_break,
4867    get_line_break,
4868    StyleLineBreak,
4869    CssPropertyType::LineBreak
4870);
4871
4872get_css_property!(
4873    get_text_align_last,
4874    get_text_align_last,
4875    StyleTextAlignLast,
4876    CssPropertyType::TextAlignLast
4877);
4878
4879get_css_property!(
4880    get_table_layout,
4881    get_table_layout,
4882    LayoutTableLayout,
4883    CssPropertyType::TableLayout
4884);
4885
4886get_css_property!(
4887    get_border_collapse,
4888    get_border_collapse,
4889    StyleBorderCollapse,
4890    CssPropertyType::BorderCollapse,
4891    compact = get_border_collapse
4892);
4893
4894get_css_property!(
4895    get_caption_side,
4896    get_caption_side,
4897    StyleCaptionSide,
4898    CssPropertyType::CaptionSide
4899);
4900
4901get_css_property!(
4902    get_empty_cells,
4903    get_empty_cells,
4904    StyleEmptyCells,
4905    CssPropertyType::EmptyCells
4906);
4907
4908get_css_property!(
4909    get_cursor_property,
4910    get_cursor,
4911    StyleCursor,
4912    CssPropertyType::Cursor
4913);
4914
4915// =============================================================================
4916// Handwritten getters (Option<T>, special logic, or non-standard returns)
4917// =============================================================================
4918
4919/// Get height property value for IFC text layout height reference.
4920pub fn get_height_value(
4921    styled_dom: &StyledDom,
4922    node_id: NodeId,
4923    node_state: &StyledNodeState,
4924) -> Option<LayoutHeight> {
4925    let node_data = &styled_dom.node_data.as_container()[node_id];
4926    styled_dom.css_property_cache.ptr
4927        .get_height(node_data, &node_id, node_state)
4928        .and_then(|v| v.get_property())
4929        .cloned()
4930}
4931
4932/// Get shape-inside property. Returns Option<ShapeInside> (cloned).
4933pub fn get_shape_inside(
4934    styled_dom: &StyledDom,
4935    node_id: NodeId,
4936    node_state: &StyledNodeState,
4937) -> Option<azul_css::props::layout::shape::ShapeInside> {
4938    let node_data = &styled_dom.node_data.as_container()[node_id];
4939    styled_dom.css_property_cache.ptr
4940        .get_shape_inside(node_data, &node_id, node_state)
4941        .and_then(|v| v.get_property())
4942        .cloned()
4943}
4944
4945/// Get shape-outside property. Returns Option<ShapeOutside> (cloned).
4946pub fn get_shape_outside(
4947    styled_dom: &StyledDom,
4948    node_id: NodeId,
4949    node_state: &StyledNodeState,
4950) -> Option<azul_css::props::layout::shape::ShapeOutside> {
4951    let node_data = &styled_dom.node_data.as_container()[node_id];
4952    styled_dom.css_property_cache.ptr
4953        .get_shape_outside(node_data, &node_id, node_state)
4954        .and_then(|v| v.get_property())
4955        .cloned()
4956}
4957
4958/// Get line-height as the full StyleLineHeight value for caller resolution.
4959pub fn get_line_height_value(
4960    styled_dom: &StyledDom,
4961    node_id: NodeId,
4962    node_state: &StyledNodeState,
4963) -> Option<azul_css::props::style::text::StyleLineHeight> {
4964    let node_data = &styled_dom.node_data.as_container()[node_id];
4965    styled_dom.css_property_cache.ptr
4966        .get_line_height(node_data, &node_id, node_state)
4967        .and_then(|v| v.get_property())
4968        .cloned()
4969}
4970
4971/// Get text-indent as the full StyleTextIndent value for caller resolution.
4972pub fn get_text_indent_value(
4973    styled_dom: &StyledDom,
4974    node_id: NodeId,
4975    node_state: &StyledNodeState,
4976) -> Option<azul_css::props::style::text::StyleTextIndent> {
4977    let node_data = &styled_dom.node_data.as_container()[node_id];
4978    styled_dom.css_property_cache.ptr
4979        .get_text_indent(node_data, &node_id, node_state)
4980        .and_then(|v| v.get_property())
4981        .cloned()
4982}
4983
4984/// Get column-count property. Returns Option<ColumnCount>.
4985pub fn get_column_count(
4986    styled_dom: &StyledDom,
4987    node_id: NodeId,
4988    node_state: &StyledNodeState,
4989) -> Option<azul_css::props::layout::column::ColumnCount> {
4990    let node_data = &styled_dom.node_data.as_container()[node_id];
4991    styled_dom.css_property_cache.ptr
4992        .get_column_count(node_data, &node_id, node_state)
4993        .and_then(|v| v.get_property())
4994        .cloned()
4995}
4996
4997/// Get initial-letter property. Returns Option<StyleInitialLetter>.
4998pub fn get_initial_letter(
4999    styled_dom: &StyledDom,
5000    node_id: NodeId,
5001    node_state: &StyledNodeState,
5002) -> Option<azul_css::props::style::text::StyleInitialLetter> {
5003    let node_data = &styled_dom.node_data.as_container()[node_id];
5004    styled_dom.css_property_cache.ptr
5005        .get_initial_letter(node_data, &node_id, node_state)
5006        .and_then(|v| v.get_property())
5007        .cloned()
5008}
5009
5010/// Get line-clamp property. Returns Option<StyleLineClamp>.
5011pub fn get_line_clamp(
5012    styled_dom: &StyledDom,
5013    node_id: NodeId,
5014    node_state: &StyledNodeState,
5015) -> Option<azul_css::props::style::text::StyleLineClamp> {
5016    let node_data = &styled_dom.node_data.as_container()[node_id];
5017    styled_dom.css_property_cache.ptr
5018        .get_line_clamp(node_data, &node_id, node_state)
5019        .and_then(|v| v.get_property())
5020        .cloned()
5021}
5022
5023/// Get hanging-punctuation property. Returns Option<StyleHangingPunctuation>.
5024pub fn get_hanging_punctuation(
5025    styled_dom: &StyledDom,
5026    node_id: NodeId,
5027    node_state: &StyledNodeState,
5028) -> Option<azul_css::props::style::text::StyleHangingPunctuation> {
5029    let node_data = &styled_dom.node_data.as_container()[node_id];
5030    styled_dom.css_property_cache.ptr
5031        .get_hanging_punctuation(node_data, &node_id, node_state)
5032        .and_then(|v| v.get_property())
5033        .cloned()
5034}
5035
5036/// Get text-combine-upright property. Returns Option<StyleTextCombineUpright>.
5037pub fn get_text_combine_upright(
5038    styled_dom: &StyledDom,
5039    node_id: NodeId,
5040    node_state: &StyledNodeState,
5041) -> Option<azul_css::props::style::text::StyleTextCombineUpright> {
5042    let node_data = &styled_dom.node_data.as_container()[node_id];
5043    styled_dom.css_property_cache.ptr
5044        .get_text_combine_upright(node_data, &node_id, node_state)
5045        .and_then(|v| v.get_property())
5046        .cloned()
5047}
5048
5049/// Get exclusion-margin value. Returns f32 (default 0.0).
5050pub fn get_exclusion_margin(
5051    styled_dom: &StyledDom,
5052    node_id: NodeId,
5053    node_state: &StyledNodeState,
5054) -> f32 {
5055    let node_data = &styled_dom.node_data.as_container()[node_id];
5056    styled_dom.css_property_cache.ptr
5057        .get_exclusion_margin(node_data, &node_id, node_state)
5058        .and_then(|v| v.get_property())
5059        .map(|v| v.inner.get() as f32)
5060        .unwrap_or(0.0)
5061}
5062
5063/// Get hyphenation-language property. Returns Option<StyleHyphenationLanguage>.
5064pub fn get_hyphenation_language(
5065    styled_dom: &StyledDom,
5066    node_id: NodeId,
5067    node_state: &StyledNodeState,
5068) -> Option<azul_css::props::style::azul_exclusion::StyleHyphenationLanguage> {
5069    let node_data = &styled_dom.node_data.as_container()[node_id];
5070    styled_dom.css_property_cache.ptr
5071        .get_hyphenation_language(node_data, &node_id, node_state)
5072        .and_then(|v| v.get_property())
5073        .cloned()
5074}
5075
5076/// Get border-spacing property.
5077pub fn get_border_spacing(
5078    styled_dom: &StyledDom,
5079    node_id: NodeId,
5080    node_state: &StyledNodeState,
5081) -> azul_css::props::layout::table::LayoutBorderSpacing {
5082    use azul_css::props::basic::pixel::PixelValue;
5083
5084    // FAST PATH: compact cache for normal state
5085    if node_state.is_normal() {
5086        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5087            let h_raw = cc.get_border_spacing_h_raw(node_id.index());
5088            let v_raw = cc.get_border_spacing_v_raw(node_id.index());
5089            // Both 0 means no border-spacing set (default)
5090            // Sentinel means non-px unit → slow path
5091            if h_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
5092                && v_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
5093            {
5094                return azul_css::props::layout::table::LayoutBorderSpacing {
5095                    horizontal: PixelValue::px(h_raw as f32 / 10.0),
5096                    vertical: PixelValue::px(v_raw as f32 / 10.0),
5097                };
5098            }
5099        }
5100    }
5101
5102    // SLOW PATH
5103    let node_data = &styled_dom.node_data.as_container()[node_id];
5104    styled_dom.css_property_cache.ptr
5105        .get_border_spacing(node_data, &node_id, node_state)
5106        .and_then(|v| v.get_property())
5107        .cloned()
5108        .unwrap_or_default()
5109}
5110
5111/// Get opacity value. Returns f32 (default 1.0).
5112///
5113/// GPU fast path: the compact cache encodes opacity as a u8 (0-254, 255 = unset).
5114/// Avoids the 4-pseudo-state × 6-layer cascade walk for animations reading opacity
5115/// across every node each frame.
5116pub fn get_opacity(
5117    styled_dom: &StyledDom,
5118    node_id: NodeId,
5119    node_state: &StyledNodeState,
5120) -> f32 {
5121    // FAST PATH: compact cache for normal state
5122    if node_state.is_normal() {
5123        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5124            let raw = cc.get_opacity_raw(node_id.index());
5125            if raw == azul_css::compact_cache::OPACITY_SENTINEL {
5126                return 1.0;
5127            }
5128            return (raw as f32) / 254.0;
5129        }
5130    }
5131    // SLOW PATH: fall back to cascade walk (state != normal, or no compact cache)
5132    let node_data = &styled_dom.node_data.as_container()[node_id];
5133    styled_dom.css_property_cache.ptr
5134        .get_opacity(node_data, &node_id, node_state)
5135        .and_then(|v| v.get_property())
5136        .map(|v| v.inner.normalized())
5137        .unwrap_or(1.0)
5138}
5139
5140/// Get filter property. Returns Option with cloned filter list.
5141pub fn get_filter(
5142    styled_dom: &StyledDom,
5143    node_id: NodeId,
5144    node_state: &StyledNodeState,
5145) -> Option<azul_css::props::style::filter::StyleFilterVec> {
5146    if node_state.is_normal() {
5147        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5148            if !cc.has_filter(node_id.index()) { return None; }
5149        }
5150    }
5151    let node_data = &styled_dom.node_data.as_container()[node_id];
5152    styled_dom.css_property_cache.ptr
5153        .get_filter(node_data, &node_id, node_state)
5154        .and_then(|v| v.get_property())
5155        .cloned()
5156}
5157
5158/// Get backdrop-filter property. Returns Option with cloned filter list.
5159pub fn get_backdrop_filter(
5160    styled_dom: &StyledDom,
5161    node_id: NodeId,
5162    node_state: &StyledNodeState,
5163) -> Option<azul_css::props::style::filter::StyleFilterVec> {
5164    if node_state.is_normal() {
5165        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5166            if !cc.has_backdrop_filter(node_id.index()) { return None; }
5167        }
5168    }
5169    let node_data = &styled_dom.node_data.as_container()[node_id];
5170    styled_dom.css_property_cache.ptr
5171        .get_backdrop_filter(node_data, &node_id, node_state)
5172        .and_then(|v| v.get_property())
5173        .cloned()
5174}
5175
5176/// Compact-cache negative fast path for all 4 box-shadow sides.
5177/// Most nodes have no shadow; cheap to check one bit vs. 4 cascade walks.
5178#[inline]
5179fn box_shadow_fast_bail(
5180    styled_dom: &StyledDom,
5181    node_id: NodeId,
5182    node_state: &StyledNodeState,
5183) -> bool {
5184    if !node_state.is_normal() { return false; }
5185    if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5186        return !cc.has_box_shadow(node_id.index());
5187    }
5188    false
5189}
5190
5191/// Get box-shadow for left side. Returns Option<StyleBoxShadow> (cloned).
5192pub fn get_box_shadow_left(
5193    styled_dom: &StyledDom,
5194    node_id: NodeId,
5195    node_state: &StyledNodeState,
5196) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
5197    if box_shadow_fast_bail(styled_dom, node_id, node_state) { return None; }
5198    let node_data = &styled_dom.node_data.as_container()[node_id];
5199    styled_dom.css_property_cache.ptr
5200        .get_box_shadow_left(node_data, &node_id, node_state)
5201        .and_then(|v| v.get_property())
5202        .map(|v| (**v).clone())
5203}
5204
5205/// Get box-shadow for right side. Returns Option<StyleBoxShadow> (cloned).
5206pub fn get_box_shadow_right(
5207    styled_dom: &StyledDom,
5208    node_id: NodeId,
5209    node_state: &StyledNodeState,
5210) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
5211    if box_shadow_fast_bail(styled_dom, node_id, node_state) { return None; }
5212    let node_data = &styled_dom.node_data.as_container()[node_id];
5213    styled_dom.css_property_cache.ptr
5214        .get_box_shadow_right(node_data, &node_id, node_state)
5215        .and_then(|v| v.get_property())
5216        .map(|v| (**v).clone())
5217}
5218
5219/// Get box-shadow for top side. Returns Option<StyleBoxShadow> (cloned).
5220pub fn get_box_shadow_top(
5221    styled_dom: &StyledDom,
5222    node_id: NodeId,
5223    node_state: &StyledNodeState,
5224) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
5225    if box_shadow_fast_bail(styled_dom, node_id, node_state) { return None; }
5226    let node_data = &styled_dom.node_data.as_container()[node_id];
5227    styled_dom.css_property_cache.ptr
5228        .get_box_shadow_top(node_data, &node_id, node_state)
5229        .and_then(|v| v.get_property())
5230        .map(|v| (**v).clone())
5231}
5232
5233/// Get box-shadow for bottom side. Returns Option<StyleBoxShadow> (cloned).
5234pub fn get_box_shadow_bottom(
5235    styled_dom: &StyledDom,
5236    node_id: NodeId,
5237    node_state: &StyledNodeState,
5238) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
5239    if box_shadow_fast_bail(styled_dom, node_id, node_state) { return None; }
5240    let node_data = &styled_dom.node_data.as_container()[node_id];
5241    styled_dom.css_property_cache.ptr
5242        .get_box_shadow_bottom(node_data, &node_id, node_state)
5243        .and_then(|v| v.get_property())
5244        .map(|v| (**v).clone())
5245}
5246
5247/// Get text-shadow property. Returns Option<StyleBoxShadow> (cloned).
5248pub fn get_text_shadow(
5249    styled_dom: &StyledDom,
5250    node_id: NodeId,
5251    node_state: &StyledNodeState,
5252) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
5253    if node_state.is_normal() {
5254        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5255            if !cc.has_text_shadow(node_id.index()) { return None; }
5256        }
5257    }
5258    let node_data = &styled_dom.node_data.as_container()[node_id];
5259    styled_dom.css_property_cache.ptr
5260        .get_text_shadow(node_data, &node_id, node_state)
5261        .and_then(|v| v.get_property())
5262        .map(|v| (**v).clone())
5263}
5264
5265/// Get transform property. Returns Option (non-empty transform list, cloned).
5266///
5267/// GPU fast path: the compact cache keeps a `has_transform` flag. If unset,
5268/// skips the cascade walk entirely — which is the overwhelming case since most
5269/// nodes have no transform. Only nodes that actually have a transform pay the
5270/// slow-walk cost to retrieve the parsed value.
5271pub fn get_transform(
5272    styled_dom: &StyledDom,
5273    node_id: NodeId,
5274    node_state: &StyledNodeState,
5275) -> Option<azul_css::props::style::transform::StyleTransformVec> {
5276    // FAST PATH: bit check in compact cache
5277    if node_state.is_normal() {
5278        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5279            if !cc.has_transform(node_id.index()) {
5280                return None;
5281            }
5282            // has_transform set → fall through to cascade walk for the value
5283        }
5284    }
5285    let node_data = &styled_dom.node_data.as_container()[node_id];
5286    styled_dom.css_property_cache.ptr
5287        .get_transform(node_data, &node_id, node_state)
5288        .and_then(|v| v.get_property())
5289        .cloned()
5290}
5291
5292/// Get counter-reset property. Returns Option<CounterReset> (cloned).
5293pub fn get_counter_reset(
5294    styled_dom: &StyledDom,
5295    node_id: NodeId,
5296    node_state: &StyledNodeState,
5297) -> Option<azul_css::props::style::content::CounterReset> {
5298    let node_data = &styled_dom.node_data.as_container()[node_id];
5299    styled_dom.css_property_cache.ptr
5300        .get_counter_reset(node_data, &node_id, node_state)
5301        .and_then(|v| v.get_property())
5302        .cloned()
5303}
5304
5305/// Get counter-increment property. Returns Option<CounterIncrement> (cloned).
5306pub fn get_counter_increment(
5307    styled_dom: &StyledDom,
5308    node_id: NodeId,
5309    node_state: &StyledNodeState,
5310) -> Option<azul_css::props::style::content::CounterIncrement> {
5311    let node_data = &styled_dom.node_data.as_container()[node_id];
5312    styled_dom.css_property_cache.ptr
5313        .get_counter_increment(node_data, &node_id, node_state)
5314        .and_then(|v| v.get_property())
5315        .cloned()
5316}
5317
5318/// W3C-conformant contenteditable inheritance check.
5319///
5320/// In the W3C model, the `contenteditable` attribute is **inherited**:
5321/// - A node is editable if it has `contenteditable="true"` set directly
5322/// - OR if its parent has `isContentEditable` as true
5323/// - UNLESS the node explicitly sets `contenteditable="false"`
5324///
5325/// This function traverses up the DOM tree to determine editability.
5326///
5327/// # Returns
5328///
5329/// - `true` if the node is editable (either directly or via inheritance)
5330/// - `false` if the node is not editable or has `contenteditable="false"`
5331///
5332/// # Example
5333///
5334/// ```html
5335/// <div contenteditable="true">
5336///   A                              <!-- editable (inherited) -->
5337///   <div contenteditable="false">
5338///     B                            <!-- NOT editable (explicitly false) -->
5339///   </div>
5340///   C                              <!-- editable (inherited) -->
5341/// </div>
5342/// ```
5343pub fn is_node_contenteditable_inherited(styled_dom: &StyledDom, node_id: NodeId) -> bool {
5344    use azul_core::dom::AttributeType;
5345    
5346    let node_data_container = styled_dom.node_data.as_container();
5347    let hierarchy = styled_dom.node_hierarchy.as_container();
5348    
5349    let mut current_node_id = Some(node_id);
5350    
5351    while let Some(nid) = current_node_id {
5352        let node_data = &node_data_container[nid];
5353        
5354        // First check the direct contenteditable field (set via set_contenteditable())
5355        // This takes precedence as it's the API-level setting
5356        if node_data.is_contenteditable() {
5357            return true;
5358        }
5359        
5360        // Then check for explicit contenteditable attribute on this node
5361        // This handles HTML-style contenteditable="true" or contenteditable="false"
5362        for attr in node_data.attributes().as_ref().iter() {
5363            if let AttributeType::ContentEditable(is_editable) = attr {
5364                // If explicitly set to true, node is editable
5365                // If explicitly set to false, node is NOT editable (blocks inheritance)
5366                return *is_editable;
5367            }
5368        }
5369        
5370        // No explicit setting on this node, check parent for inheritance
5371        current_node_id = hierarchy.get(nid).and_then(|h| h.parent_id());
5372    }
5373    
5374    // Reached root without finding contenteditable - not editable
5375    false
5376}
5377
5378/// Find the contenteditable ancestor of a node.
5379///
5380/// When focus lands on a text node inside a contenteditable container,
5381/// we need to find the actual container that has the `contenteditable` attribute.
5382///
5383/// # Returns
5384///
5385/// - `Some(node_id)` of the contenteditable ancestor (may be the node itself)
5386/// - `None` if no contenteditable ancestor exists
5387pub fn find_contenteditable_ancestor(styled_dom: &StyledDom, node_id: NodeId) -> Option<NodeId> {
5388    use azul_core::dom::AttributeType;
5389    
5390    let node_data_container = styled_dom.node_data.as_container();
5391    let hierarchy = styled_dom.node_hierarchy.as_container();
5392    
5393    let mut current_node_id = Some(node_id);
5394    
5395    while let Some(nid) = current_node_id {
5396        let node_data = &node_data_container[nid];
5397        
5398        // First check the direct contenteditable field (set via set_contenteditable())
5399        if node_data.is_contenteditable() {
5400            return Some(nid);
5401        }
5402        
5403        // Then check for contenteditable attribute on this node
5404        for attr in node_data.attributes().as_ref().iter() {
5405            if let AttributeType::ContentEditable(is_editable) = attr {
5406                if *is_editable {
5407                    return Some(nid);
5408                } else {
5409                    // Explicitly not editable - stop search
5410                    return None;
5411                }
5412            }
5413        }
5414        
5415        // Check parent
5416        current_node_id = hierarchy.get(nid).and_then(|h| h.parent_id());
5417    }
5418    
5419    None
5420}
5421
5422// --- Taffy bridge property getters ---
5423//
5424// These getters return `Option<CssPropertyValue<T>>` (cloned from cache) for use
5425// by taffy_bridge.rs. The conversion from CssPropertyValue to taffy types is done
5426// in taffy_bridge.rs itself. Routing access through these functions centralizes
5427// all CSS property lookups for future cache optimizations (e.g., FxHash migration).
5428
5429macro_rules! get_css_property_value {
5430    ($fn_name:ident, $cache_method:ident, $ret_type:ty) => {
5431        pub fn $fn_name(
5432            styled_dom: &StyledDom,
5433            node_id: NodeId,
5434            node_state: &StyledNodeState,
5435        ) -> Option<$ret_type> {
5436            let node_data = &styled_dom.node_data.as_container()[node_id];
5437            styled_dom
5438                .css_property_cache
5439                .ptr
5440                .$cache_method(node_data, &node_id, node_state)
5441                .cloned()
5442        }
5443    };
5444}
5445
5446// Flexbox properties
5447get_css_property_value!(get_flex_direction_prop, get_flex_direction, LayoutFlexDirectionValue);
5448get_css_property_value!(get_flex_wrap_prop, get_flex_wrap, LayoutFlexWrapValue);
5449get_css_property_value!(get_flex_grow_prop, get_flex_grow, LayoutFlexGrowValue);
5450get_css_property_value!(get_flex_shrink_prop, get_flex_shrink, LayoutFlexShrinkValue);
5451get_css_property_value!(get_flex_basis_prop, get_flex_basis, LayoutFlexBasisValue);
5452
5453// Alignment properties
5454get_css_property_value!(get_align_items_prop, get_align_items, LayoutAlignItemsValue);
5455get_css_property_value!(get_align_self_prop, get_align_self, LayoutAlignSelfValue);
5456get_css_property_value!(get_align_content_prop, get_align_content, LayoutAlignContentValue);
5457get_css_property_value!(get_justify_content_prop, get_justify_content, LayoutJustifyContentValue);
5458get_css_property_value!(get_justify_items_prop, get_justify_items, LayoutJustifyItemsValue);
5459get_css_property_value!(get_justify_self_prop, get_justify_self, LayoutJustifySelfValue);
5460
5461// Gap
5462get_css_property_value!(get_gap_prop, get_gap, LayoutGapValue);
5463
5464// Grid properties
5465get_css_property_value!(get_grid_template_rows_prop, get_grid_template_rows, LayoutGridTemplateRowsValue);
5466get_css_property_value!(get_grid_template_columns_prop, get_grid_template_columns, LayoutGridTemplateColumnsValue);
5467get_css_property_value!(get_grid_auto_rows_prop, get_grid_auto_rows, LayoutGridAutoRowsValue);
5468get_css_property_value!(get_grid_auto_columns_prop, get_grid_auto_columns, LayoutGridAutoColumnsValue);
5469get_css_property_value!(get_grid_auto_flow_prop, get_grid_auto_flow, LayoutGridAutoFlowValue);
5470get_css_property_value!(get_grid_column_prop, get_grid_column, LayoutGridColumnValue);
5471get_css_property_value!(get_grid_row_prop, get_grid_row, LayoutGridRowValue);
5472
5473/// Get grid-template-areas property.
5474/// Uses the generic `get_property()` since CssPropertyCache lacks a specific getter.
5475/// Returns the inner `GridTemplateAreas` value (already unwrapped from CssPropertyValue).
5476pub fn get_grid_template_areas_prop(
5477    styled_dom: &StyledDom,
5478    node_id: NodeId,
5479    node_state: &StyledNodeState,
5480) -> Option<GridTemplateAreas> {
5481    let node_data = &styled_dom.node_data.as_container()[node_id];
5482    styled_dom
5483        .css_property_cache
5484        .ptr
5485        .get_property(node_data, &node_id, node_state, &CssPropertyType::GridTemplateAreas)
5486        .and_then(|p| {
5487            if let CssProperty::GridTemplateAreas(v) = p {
5488                v.get_property().cloned()
5489            } else {
5490                None
5491            }
5492        })
5493}
5494
5495/// Get clip-path property. Returns the ClipPath value for the node.
5496///
5497/// CSS Masking Module Level 1, section 3:
5498/// The clip-path property creates a clipping region that determines which parts
5499/// of an element are visible. Returns None for `clip-path: none` (default).
5500pub fn get_clip_path(
5501    styled_dom: &StyledDom,
5502    node_id: NodeId,
5503    node_state: &StyledNodeState,
5504) -> Option<azul_css::props::layout::shape::ClipPath> {
5505    // Negative fast path: most nodes have `clip-path: none`.
5506    if node_state.is_normal() {
5507        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
5508            if !cc.has_clip_path(node_id.index()) {
5509                return None;
5510            }
5511        }
5512    }
5513    let node_data = &styled_dom.node_data.as_container()[node_id];
5514    styled_dom.css_property_cache.ptr
5515        .get_clip_path(node_data, &node_id, node_state)
5516        .and_then(|v| v.get_property())
5517        .cloned()
5518}