Skip to main content

azul_layout/solver3/
getters.rs

1//! Getter functions for CSS properties from the styled DOM
2//!
3//! This module provides clean, consistent access to CSS properties with proper
4//! fallbacks and type conversions.
5
6use azul_core::{
7    dom::{NodeId, NodeType},
8    geom::LogicalSize,
9    id::NodeId as CoreNodeId,
10    styled_dom::{StyledDom, StyledNodeState},
11};
12use azul_css::{
13    css::CssPropertyValue,
14    props::{
15        basic::{
16            font::{StyleFontFamily, StyleFontFamilyVec, StyleFontWeight, StyleFontStyle},
17            pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
18            ColorU, PhysicalSize, PixelValue, PropertyContext, ResolutionContext,
19        },
20        layout::{
21            BoxDecorationBreak, BreakInside, LayoutBoxSizing, LayoutClear, LayoutDisplay,
22            LayoutFlexDirection, LayoutFlexWrap, LayoutFloat, LayoutHeight,
23            LayoutJustifyContent, LayoutAlignItems, LayoutAlignContent, LayoutOverflow,
24            LayoutPosition, LayoutWidth, LayoutWritingMode, Orphans, PageBreak, Widows,
25            grid::GridTemplateAreas,
26        },
27        property::{CssProperty, CssPropertyType,
28            LayoutFlexBasisValue, LayoutFlexDirectionValue, LayoutFlexWrapValue,
29            LayoutFlexGrowValue, LayoutFlexShrinkValue,
30            LayoutAlignItemsValue, LayoutAlignSelfValue, LayoutAlignContentValue,
31            LayoutJustifyContentValue, LayoutJustifyItemsValue, LayoutJustifySelfValue,
32            LayoutGapValue,
33            LayoutGridTemplateColumnsValue, LayoutGridTemplateRowsValue,
34            LayoutGridAutoColumnsValue, LayoutGridAutoRowsValue,
35            LayoutGridAutoFlowValue, LayoutGridColumnValue, LayoutGridRowValue,
36        },
37        style::{
38            border_radius::StyleBorderRadius,
39            lists::{StyleListStylePosition, StyleListStyleType},
40            StyleDirection, StyleTextAlign, StyleUserSelect, StyleVerticalAlign,
41            StyleVisibility, StyleWhiteSpace,
42        },
43    },
44};
45
46use crate::{
47    font_traits::{ParsedFontTrait, StyleProperties},
48    solver3::{
49        display_list::{BorderRadius, PhysicalSizeImport},
50        layout_tree::LayoutNode,
51        scrollbar::ScrollbarRequirements,
52    },
53};
54
55// Font-size resolution helper functions
56
57/// Helper function to get element's computed font-size
58pub fn get_element_font_size(
59    styled_dom: &StyledDom,
60    dom_id: NodeId,
61    node_state: &StyledNodeState,
62) -> f32 {
63    let node_data = &styled_dom.node_data.as_container()[dom_id];
64    let cache = &styled_dom.css_property_cache.ptr;
65
66    // Try to get from dependency chain first (proper resolution)
67    let cached_font_size = cache
68        .dependency_chains
69        .get(dom_id.index())
70        .and_then(|chains| chains.get(&azul_css::props::property::CssPropertyType::FontSize))
71        .and_then(|chain| chain.cached_pixels);
72
73    if let Some(cached) = cached_font_size {
74        return cached;
75    }
76
77    // Fallback: get from property cache and resolve manually
78    let parent_font_size = styled_dom
79        .node_hierarchy
80        .as_container()
81        .get(dom_id)
82        .and_then(|node| node.parent_id())
83        .and_then(|parent_id| {
84            // Check parent's dependency chain first (avoids recursion)
85            cache
86                .dependency_chains
87                .get(parent_id.index())
88                .and_then(|chains| {
89                    chains.get(&azul_css::props::property::CssPropertyType::FontSize)
90                })
91                .and_then(|chain| chain.cached_pixels)
92        })
93        .unwrap_or(DEFAULT_FONT_SIZE);
94
95    // Get root font-size (avoid recursion by checking cache first)
96    let root_font_size = {
97        let root_id = NodeId::new(0);
98        cache
99            .dependency_chains
100            .get(root_id.index())
101            .and_then(|chains| chains.get(&azul_css::props::property::CssPropertyType::FontSize))
102            .and_then(|chain| chain.cached_pixels)
103            .unwrap_or(DEFAULT_FONT_SIZE)
104    };
105
106    // Resolve font-size with proper context
107    cache
108        .get_font_size(node_data, &dom_id, node_state)
109        .and_then(|v| v.get_property().cloned())
110        .map(|v| {
111            let context = ResolutionContext {
112                element_font_size: DEFAULT_FONT_SIZE, // Not used for FontSize property
113                parent_font_size,
114                root_font_size,
115                containing_block_size: PhysicalSize::new(0.0, 0.0),
116                element_size: None,
117                viewport_size: PhysicalSize::new(0.0, 0.0), // Not used for font-size resolution
118            };
119
120            v.inner
121                .resolve_with_context(&context, PropertyContext::FontSize)
122        })
123        .unwrap_or(DEFAULT_FONT_SIZE)
124}
125
126/// Helper function to get parent's computed font-size
127pub fn get_parent_font_size(
128    styled_dom: &StyledDom,
129    dom_id: NodeId,
130    node_state: &StyledNodeState,
131) -> f32 {
132    styled_dom
133        .node_hierarchy
134        .as_container()
135        .get(dom_id)
136        .and_then(|node| node.parent_id())
137        .map(|parent_id| get_element_font_size(styled_dom, parent_id, node_state))
138        .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE)
139}
140
141/// Helper function to get root element's font-size
142pub fn get_root_font_size(styled_dom: &StyledDom, node_state: &StyledNodeState) -> f32 {
143    // Root is always NodeId(0) in Azul
144    get_element_font_size(styled_dom, NodeId::new(0), node_state)
145}
146
147/// A value that can be Auto, Initial, Inherit, or an explicit value.
148/// This preserves CSS cascade semantics better than Option<T>.
149#[derive(Debug, Copy, Clone, PartialEq)]
150pub enum MultiValue<T> {
151    /// CSS 'auto' keyword
152    Auto,
153    /// CSS 'initial' keyword - use initial value
154    Initial,
155    /// CSS 'inherit' keyword - inherit from parent
156    Inherit,
157    /// Explicit value (e.g., "10px", "50%")
158    Exact(T),
159}
160
161impl<T> MultiValue<T> {
162    /// Returns true if this is an Auto value
163    pub fn is_auto(&self) -> bool {
164        matches!(self, MultiValue::Auto)
165    }
166
167    /// Returns true if this is an explicit value
168    pub fn is_exact(&self) -> bool {
169        matches!(self, MultiValue::Exact(_))
170    }
171
172    /// Gets the exact value if present
173    pub fn exact(self) -> Option<T> {
174        match self {
175            MultiValue::Exact(v) => Some(v),
176            _ => None,
177        }
178    }
179
180    /// Gets the exact value or returns the provided default
181    pub fn unwrap_or(self, default: T) -> T {
182        match self {
183            MultiValue::Exact(v) => v,
184            _ => default,
185        }
186    }
187
188    /// Gets the exact value or returns T::default()
189    pub fn unwrap_or_default(self) -> T
190    where
191        T: Default,
192    {
193        match self {
194            MultiValue::Exact(v) => v,
195            _ => T::default(),
196        }
197    }
198
199    /// Maps the inner value if Exact, otherwise returns self unchanged
200    pub fn map<U, F>(self, f: F) -> MultiValue<U>
201    where
202        F: FnOnce(T) -> U,
203    {
204        match self {
205            MultiValue::Exact(v) => MultiValue::Exact(f(v)),
206            MultiValue::Auto => MultiValue::Auto,
207            MultiValue::Initial => MultiValue::Initial,
208            MultiValue::Inherit => MultiValue::Inherit,
209        }
210    }
211}
212
213// Implement helper methods for LayoutOverflow specifically
214impl MultiValue<LayoutOverflow> {
215    /// Returns true if this overflow value causes content to be clipped.
216    /// This includes Hidden, Clip, Auto, and Scroll (all values except Visible).
217    pub fn is_clipped(&self) -> bool {
218        matches!(
219            self,
220            MultiValue::Exact(
221                LayoutOverflow::Hidden
222                    | LayoutOverflow::Clip
223                    | LayoutOverflow::Auto
224                    | LayoutOverflow::Scroll
225            )
226        )
227    }
228
229    pub fn is_scroll(&self) -> bool {
230        matches!(
231            self,
232            MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
233        )
234    }
235
236    pub fn is_auto_overflow(&self) -> bool {
237        matches!(self, MultiValue::Exact(LayoutOverflow::Auto))
238    }
239
240    pub fn is_hidden(&self) -> bool {
241        matches!(self, MultiValue::Exact(LayoutOverflow::Hidden))
242    }
243
244    pub fn is_hidden_or_clip(&self) -> bool {
245        matches!(
246            self,
247            MultiValue::Exact(LayoutOverflow::Hidden | LayoutOverflow::Clip)
248        )
249    }
250
251    pub fn is_scroll_explicit(&self) -> bool {
252        matches!(self, MultiValue::Exact(LayoutOverflow::Scroll))
253    }
254
255    pub fn is_visible_or_clip(&self) -> bool {
256        matches!(
257            self,
258            MultiValue::Exact(LayoutOverflow::Visible | LayoutOverflow::Clip)
259        )
260    }
261}
262
263// Implement helper methods for LayoutPosition
264impl MultiValue<LayoutPosition> {
265    pub fn is_absolute_or_fixed(&self) -> bool {
266        matches!(
267            self,
268            MultiValue::Exact(LayoutPosition::Absolute | LayoutPosition::Fixed)
269        )
270    }
271}
272
273// Implement helper methods for LayoutFloat
274impl MultiValue<LayoutFloat> {
275    pub fn is_none(&self) -> bool {
276        matches!(
277            self,
278            MultiValue::Auto
279                | MultiValue::Initial
280                | MultiValue::Inherit
281                | MultiValue::Exact(LayoutFloat::None)
282        )
283    }
284}
285
286impl<T: Default> Default for MultiValue<T> {
287    fn default() -> Self {
288        MultiValue::Auto
289    }
290}
291
292/// Helper macro to reduce boilerplate for simple CSS property getters
293/// Returns the inner PixelValue wrapped in MultiValue
294macro_rules! get_css_property_pixel {
295    // Variant WITH compact cache fast path for i16-encoded resolved px properties
296    ($fn_name:ident, $cache_method:ident, $ua_property:expr, compact_i16 = $compact_method:ident) => {
297        pub fn $fn_name(
298            styled_dom: &StyledDom,
299            node_id: NodeId,
300            node_state: &StyledNodeState,
301        ) -> MultiValue<PixelValue> {
302            // FAST PATH: compact cache for normal state (O(1) array lookup)
303            if node_state.is_normal() {
304                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
305                    let raw = cc.$compact_method(node_id.index());
306                    if raw == azul_css::compact_cache::I16_AUTO {
307                        return MultiValue::Auto;
308                    }
309                    if raw == azul_css::compact_cache::I16_INITIAL {
310                        return MultiValue::Initial;
311                    }
312                    if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
313                        // Valid value: decode i16 ×10 → px
314                        return MultiValue::Exact(PixelValue::px(raw as f32 / 10.0));
315                    }
316                    // I16_SENTINEL or I16_INHERIT → fall through to slow path
317                }
318            }
319
320            let node_data = &styled_dom.node_data.as_container()[node_id];
321
322            let author_css = styled_dom
323                .css_property_cache
324                .ptr
325                .$cache_method(node_data, &node_id, node_state);
326
327            if let Some(ref val) = author_css {
328                if val.is_auto() {
329                    return MultiValue::Auto;
330                }
331                if let Some(exact) = val.get_property().copied() {
332                    return MultiValue::Exact(exact.inner);
333                }
334            }
335
336            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
337
338            if let Some(ua_prop) = ua_css {
339                if let Some(inner) = ua_prop.get_pixel_inner() {
340                    return MultiValue::Exact(inner);
341                }
342            }
343
344            MultiValue::Initial
345        }
346    };
347    // Variant WITHOUT compact cache (original behavior)
348    ($fn_name:ident, $cache_method:ident, $ua_property:expr) => {
349        pub fn $fn_name(
350            styled_dom: &StyledDom,
351            node_id: NodeId,
352            node_state: &StyledNodeState,
353        ) -> MultiValue<PixelValue> {
354            let node_data = &styled_dom.node_data.as_container()[node_id];
355
356            // 1. Check author CSS first (includes inline styles - highest priority)
357            let author_css = styled_dom
358                .css_property_cache
359                .ptr
360                .$cache_method(node_data, &node_id, node_state);
361
362            // FIX: Check for Auto FIRST - CssPropertyValue::Auto is a valid value
363            // that should NOT fall through to UA CSS. Previously, get_property()
364            // returned None for Auto, causing inline "margin: auto" to be ignored.
365            if let Some(ref val) = author_css {
366                if val.is_auto() {
367                    return MultiValue::Auto;
368                }
369                if let Some(exact) = val.get_property().copied() {
370                    return MultiValue::Exact(exact.inner);
371                }
372                // For Initial, Inherit, None, Revert, Unset - fall through to UA CSS
373            }
374
375            // 2. Check User Agent CSS (only if author CSS didn't set a value)
376            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
377
378            if let Some(ua_prop) = ua_css {
379                if let Some(inner) = ua_prop.get_pixel_inner() {
380                    return MultiValue::Exact(inner);
381                }
382            }
383
384            // 3. Fallback to Initial (not set)
385            // IMPORTANT: Use Initial, not Auto! In CSS, the initial value for 
386            // margin is 0, not auto. Using Auto here caused margins to be treated
387            // as "margin: auto" which blocks align-self: stretch in flexbox.
388            MultiValue::Initial
389        }
390    };
391}
392
393/// Helper trait to extract PixelValue from any CssProperty variant
394trait CssPropertyPixelInner {
395    fn get_pixel_inner(&self) -> Option<PixelValue>;
396}
397
398impl CssPropertyPixelInner for azul_css::props::property::CssProperty {
399    fn get_pixel_inner(&self) -> Option<PixelValue> {
400        match self {
401            CssProperty::Left(CssPropertyValue::Exact(v)) => Some(v.inner),
402            CssProperty::Right(CssPropertyValue::Exact(v)) => Some(v.inner),
403            CssProperty::Top(CssPropertyValue::Exact(v)) => Some(v.inner),
404            CssProperty::Bottom(CssPropertyValue::Exact(v)) => Some(v.inner),
405            CssProperty::MarginLeft(CssPropertyValue::Exact(v)) => Some(v.inner),
406            CssProperty::MarginRight(CssPropertyValue::Exact(v)) => Some(v.inner),
407            CssProperty::MarginTop(CssPropertyValue::Exact(v)) => Some(v.inner),
408            CssProperty::MarginBottom(CssPropertyValue::Exact(v)) => Some(v.inner),
409            CssProperty::PaddingLeft(CssPropertyValue::Exact(v)) => Some(v.inner),
410            CssProperty::PaddingRight(CssPropertyValue::Exact(v)) => Some(v.inner),
411            CssProperty::PaddingTop(CssPropertyValue::Exact(v)) => Some(v.inner),
412            CssProperty::PaddingBottom(CssPropertyValue::Exact(v)) => Some(v.inner),
413            _ => None,
414        }
415    }
416}
417
418/// Generic macro for CSS properties with UA CSS fallback - returns MultiValue<T>
419macro_rules! get_css_property {
420    // Variant WITH compact cache fast path (for enum properties in Tier 1)
421    ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr, compact = $compact_method:ident) => {
422        pub fn $fn_name(
423            styled_dom: &StyledDom,
424            node_id: NodeId,
425            node_state: &StyledNodeState,
426        ) -> MultiValue<$return_type> {
427            // FAST PATH: compact cache for normal state (O(1) array + bitshift)
428            if node_state.is_normal() {
429                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
430                    return MultiValue::Exact(cc.$compact_method(node_id.index()));
431                }
432            }
433
434            // SLOW PATH: full cascade resolution
435            let node_data = &styled_dom.node_data.as_container()[node_id];
436
437            // 1. Check author CSS first
438            let author_css = styled_dom
439                .css_property_cache
440                .ptr
441                .$cache_method(node_data, &node_id, node_state);
442
443            if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
444                return MultiValue::Exact(val);
445            }
446
447            // 2. Check User Agent CSS
448            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
449
450            if let Some(ua_prop) = ua_css {
451                if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
452                    return MultiValue::Exact(val);
453                }
454            }
455
456            // 3. Fallback to Auto (not set)
457            MultiValue::Auto
458        }
459    };
460    // Variant WITH compact cache for u32-encoded dimension enums (LayoutWidth/LayoutHeight)
461    // These types have Auto, Px(PixelValue), MinContent, MaxContent, Calc variants
462    ($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) => {
463        pub fn $fn_name(
464            styled_dom: &StyledDom,
465            node_id: NodeId,
466            node_state: &StyledNodeState,
467        ) -> MultiValue<$return_type> {
468            // FAST PATH: compact cache for normal state
469            if node_state.is_normal() {
470                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
471                    let raw = cc.$compact_raw_method(node_id.index());
472                    match raw {
473                        azul_css::compact_cache::U32_AUTO => return MultiValue::Auto,
474                        azul_css::compact_cache::U32_INITIAL => return MultiValue::Initial,
475                        azul_css::compact_cache::U32_NONE => return MultiValue::Auto,
476                        azul_css::compact_cache::U32_MIN_CONTENT => return MultiValue::Exact($min_content_variant),
477                        azul_css::compact_cache::U32_MAX_CONTENT => return MultiValue::Exact($max_content_variant),
478                        azul_css::compact_cache::U32_SENTINEL | azul_css::compact_cache::U32_INHERIT => {
479                            // fall through to slow path
480                        }
481                        _ => {
482                            // Valid encoded pixel value
483                            if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
484                                return MultiValue::Exact($px_variant(pv));
485                            }
486                            // decode failed → slow path
487                        }
488                    }
489                }
490            }
491
492            // SLOW PATH: full cascade resolution
493            let node_data = &styled_dom.node_data.as_container()[node_id];
494
495            let author_css = styled_dom
496                .css_property_cache
497                .ptr
498                .$cache_method(node_data, &node_id, node_state);
499
500            if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
501                return MultiValue::Exact(val);
502            }
503
504            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
505
506            if let Some(ua_prop) = ua_css {
507                if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
508                    return MultiValue::Exact(val);
509                }
510            }
511
512            MultiValue::Auto
513        }
514    };
515    // Variant WITH compact cache for u32-encoded dimension structs (LayoutMinWidth etc.)
516    // These types are struct { inner: PixelValue }
517    ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr, compact_u32_struct = $compact_raw_method:ident) => {
518        pub fn $fn_name(
519            styled_dom: &StyledDom,
520            node_id: NodeId,
521            node_state: &StyledNodeState,
522        ) -> MultiValue<$return_type> {
523            // FAST PATH: compact cache for normal state
524            if node_state.is_normal() {
525                if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
526                    let raw = cc.$compact_raw_method(node_id.index());
527                    match raw {
528                        azul_css::compact_cache::U32_AUTO | azul_css::compact_cache::U32_NONE => return MultiValue::Auto,
529                        azul_css::compact_cache::U32_INITIAL => return MultiValue::Initial,
530                        azul_css::compact_cache::U32_SENTINEL | azul_css::compact_cache::U32_INHERIT => {
531                            // fall through to slow path
532                        }
533                        _ => {
534                            if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
535                                return MultiValue::Exact(
536                                    <$return_type as azul_css::props::PixelValueTaker>::from_pixel_value(pv)
537                                );
538                            }
539                        }
540                    }
541                }
542            }
543
544            // SLOW PATH
545            let node_data = &styled_dom.node_data.as_container()[node_id];
546
547            let author_css = styled_dom
548                .css_property_cache
549                .ptr
550                .$cache_method(node_data, &node_id, node_state);
551
552            if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
553                return MultiValue::Exact(val);
554            }
555
556            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
557
558            if let Some(ua_prop) = ua_css {
559                if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
560                    return MultiValue::Exact(val);
561                }
562            }
563
564            MultiValue::Auto
565        }
566    };
567    // Variant WITHOUT compact cache (original behavior)
568    ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr) => {
569        pub fn $fn_name(
570            styled_dom: &StyledDom,
571            node_id: NodeId,
572            node_state: &StyledNodeState,
573        ) -> MultiValue<$return_type> {
574            let node_data = &styled_dom.node_data.as_container()[node_id];
575
576            // 1. Check author CSS first
577            let author_css = styled_dom
578                .css_property_cache
579                .ptr
580                .$cache_method(node_data, &node_id, node_state);
581
582            if let Some(val) = author_css.and_then(|v| v.get_property().cloned()) {
583                return MultiValue::Exact(val);
584            }
585
586            // 2. Check User Agent CSS
587            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
588
589            if let Some(ua_prop) = ua_css {
590                if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
591                    return MultiValue::Exact(val);
592                }
593            }
594
595            // 3. Fallback to Auto (not set)
596            MultiValue::Auto
597        }
598    };
599}
600
601/// Helper trait to extract typed values from UA CSS properties
602trait ExtractPropertyValue<T> {
603    fn extract(&self) -> Option<T>;
604}
605
606fn extract_property_value<T>(prop: &azul_css::props::property::CssProperty) -> Option<T>
607where
608    azul_css::props::property::CssProperty: ExtractPropertyValue<T>,
609{
610    prop.extract()
611}
612
613// Implement extraction for all layout types
614
615impl ExtractPropertyValue<LayoutWidth> for azul_css::props::property::CssProperty {
616    fn extract(&self) -> Option<LayoutWidth> {
617        match self {
618            Self::Width(CssPropertyValue::Exact(v)) => Some(v.clone()),
619            _ => None,
620        }
621    }
622}
623
624impl ExtractPropertyValue<LayoutHeight> for azul_css::props::property::CssProperty {
625    fn extract(&self) -> Option<LayoutHeight> {
626        match self {
627            Self::Height(CssPropertyValue::Exact(v)) => Some(v.clone()),
628            _ => None,
629        }
630    }
631}
632
633impl ExtractPropertyValue<LayoutMinWidth> for azul_css::props::property::CssProperty {
634    fn extract(&self) -> Option<LayoutMinWidth> {
635        match self {
636            Self::MinWidth(CssPropertyValue::Exact(v)) => Some(*v),
637            _ => None,
638        }
639    }
640}
641
642impl ExtractPropertyValue<LayoutMinHeight> for azul_css::props::property::CssProperty {
643    fn extract(&self) -> Option<LayoutMinHeight> {
644        match self {
645            Self::MinHeight(CssPropertyValue::Exact(v)) => Some(*v),
646            _ => None,
647        }
648    }
649}
650
651impl ExtractPropertyValue<LayoutMaxWidth> for azul_css::props::property::CssProperty {
652    fn extract(&self) -> Option<LayoutMaxWidth> {
653        match self {
654            Self::MaxWidth(CssPropertyValue::Exact(v)) => Some(*v),
655            _ => None,
656        }
657    }
658}
659
660impl ExtractPropertyValue<LayoutMaxHeight> for azul_css::props::property::CssProperty {
661    fn extract(&self) -> Option<LayoutMaxHeight> {
662        match self {
663            Self::MaxHeight(CssPropertyValue::Exact(v)) => Some(*v),
664            _ => None,
665        }
666    }
667}
668
669impl ExtractPropertyValue<LayoutDisplay> for azul_css::props::property::CssProperty {
670    fn extract(&self) -> Option<LayoutDisplay> {
671        match self {
672            Self::Display(CssPropertyValue::Exact(v)) => Some(*v),
673            _ => None,
674        }
675    }
676}
677
678impl ExtractPropertyValue<LayoutWritingMode> for azul_css::props::property::CssProperty {
679    fn extract(&self) -> Option<LayoutWritingMode> {
680        match self {
681            Self::WritingMode(CssPropertyValue::Exact(v)) => Some(*v),
682            _ => None,
683        }
684    }
685}
686
687impl ExtractPropertyValue<LayoutFlexWrap> for azul_css::props::property::CssProperty {
688    fn extract(&self) -> Option<LayoutFlexWrap> {
689        match self {
690            Self::FlexWrap(CssPropertyValue::Exact(v)) => Some(*v),
691            _ => None,
692        }
693    }
694}
695
696impl ExtractPropertyValue<LayoutJustifyContent> for azul_css::props::property::CssProperty {
697    fn extract(&self) -> Option<LayoutJustifyContent> {
698        match self {
699            Self::JustifyContent(CssPropertyValue::Exact(v)) => Some(*v),
700            _ => None,
701        }
702    }
703}
704
705impl ExtractPropertyValue<StyleTextAlign> for azul_css::props::property::CssProperty {
706    fn extract(&self) -> Option<StyleTextAlign> {
707        match self {
708            Self::TextAlign(CssPropertyValue::Exact(v)) => Some(*v),
709            _ => None,
710        }
711    }
712}
713
714impl ExtractPropertyValue<LayoutFloat> for azul_css::props::property::CssProperty {
715    fn extract(&self) -> Option<LayoutFloat> {
716        match self {
717            Self::Float(CssPropertyValue::Exact(v)) => Some(*v),
718            _ => None,
719        }
720    }
721}
722
723impl ExtractPropertyValue<LayoutClear> for azul_css::props::property::CssProperty {
724    fn extract(&self) -> Option<LayoutClear> {
725        match self {
726            Self::Clear(CssPropertyValue::Exact(v)) => Some(*v),
727            _ => None,
728        }
729    }
730}
731
732impl ExtractPropertyValue<LayoutOverflow> for azul_css::props::property::CssProperty {
733    fn extract(&self) -> Option<LayoutOverflow> {
734        match self {
735            Self::OverflowX(CssPropertyValue::Exact(v)) => Some(*v),
736            Self::OverflowY(CssPropertyValue::Exact(v)) => Some(*v),
737            _ => None,
738        }
739    }
740}
741
742impl ExtractPropertyValue<LayoutPosition> for azul_css::props::property::CssProperty {
743    fn extract(&self) -> Option<LayoutPosition> {
744        match self {
745            Self::Position(CssPropertyValue::Exact(v)) => Some(*v),
746            _ => None,
747        }
748    }
749}
750
751impl ExtractPropertyValue<LayoutBoxSizing> for azul_css::props::property::CssProperty {
752    fn extract(&self) -> Option<LayoutBoxSizing> {
753        match self {
754            Self::BoxSizing(CssPropertyValue::Exact(v)) => Some(*v),
755            _ => None,
756        }
757    }
758}
759
760impl ExtractPropertyValue<PixelValue> for azul_css::props::property::CssProperty {
761    fn extract(&self) -> Option<PixelValue> {
762        self.get_pixel_inner()
763    }
764}
765
766impl ExtractPropertyValue<LayoutFlexDirection> for azul_css::props::property::CssProperty {
767    fn extract(&self) -> Option<LayoutFlexDirection> {
768        match self {
769            Self::FlexDirection(CssPropertyValue::Exact(v)) => Some(*v),
770            _ => None,
771        }
772    }
773}
774
775impl ExtractPropertyValue<LayoutAlignItems> for azul_css::props::property::CssProperty {
776    fn extract(&self) -> Option<LayoutAlignItems> {
777        match self {
778            Self::AlignItems(CssPropertyValue::Exact(v)) => Some(*v),
779            _ => None,
780        }
781    }
782}
783
784impl ExtractPropertyValue<LayoutAlignContent> for azul_css::props::property::CssProperty {
785    fn extract(&self) -> Option<LayoutAlignContent> {
786        match self {
787            Self::AlignContent(CssPropertyValue::Exact(v)) => Some(*v),
788            _ => None,
789        }
790    }
791}
792
793impl ExtractPropertyValue<StyleFontWeight> for azul_css::props::property::CssProperty {
794    fn extract(&self) -> Option<StyleFontWeight> {
795        match self {
796            Self::FontWeight(CssPropertyValue::Exact(v)) => Some(*v),
797            _ => None,
798        }
799    }
800}
801
802impl ExtractPropertyValue<StyleFontStyle> for azul_css::props::property::CssProperty {
803    fn extract(&self) -> Option<StyleFontStyle> {
804        match self {
805            Self::FontStyle(CssPropertyValue::Exact(v)) => Some(*v),
806            _ => None,
807        }
808    }
809}
810
811impl ExtractPropertyValue<StyleVisibility> for azul_css::props::property::CssProperty {
812    fn extract(&self) -> Option<StyleVisibility> {
813        match self {
814            Self::Visibility(CssPropertyValue::Exact(v)) => Some(*v),
815            _ => None,
816        }
817    }
818}
819
820impl ExtractPropertyValue<StyleWhiteSpace> for azul_css::props::property::CssProperty {
821    fn extract(&self) -> Option<StyleWhiteSpace> {
822        match self {
823            Self::WhiteSpace(CssPropertyValue::Exact(v)) => Some(*v),
824            _ => None,
825        }
826    }
827}
828
829impl ExtractPropertyValue<StyleDirection> for azul_css::props::property::CssProperty {
830    fn extract(&self) -> Option<StyleDirection> {
831        match self {
832            Self::Direction(CssPropertyValue::Exact(v)) => Some(*v),
833            _ => None,
834        }
835    }
836}
837
838impl ExtractPropertyValue<StyleVerticalAlign> for azul_css::props::property::CssProperty {
839    fn extract(&self) -> Option<StyleVerticalAlign> {
840        match self {
841            Self::VerticalAlign(CssPropertyValue::Exact(v)) => Some(*v),
842            _ => None,
843        }
844    }
845}
846
847get_css_property!(
848    get_writing_mode,
849    get_writing_mode,
850    LayoutWritingMode,
851    azul_css::props::property::CssPropertyType::WritingMode,
852    compact = get_writing_mode
853);
854
855get_css_property!(
856    get_css_width,
857    get_width,
858    LayoutWidth,
859    azul_css::props::property::CssPropertyType::Width,
860    compact_u32_dim = get_width_raw, LayoutWidth::Px, LayoutWidth::Auto, LayoutWidth::MinContent, LayoutWidth::MaxContent
861);
862
863get_css_property!(
864    get_css_height,
865    get_height,
866    LayoutHeight,
867    azul_css::props::property::CssPropertyType::Height,
868    compact_u32_dim = get_height_raw, LayoutHeight::Px, LayoutHeight::Auto, LayoutHeight::MinContent, LayoutHeight::MaxContent
869);
870
871get_css_property!(
872    get_wrap,
873    get_flex_wrap,
874    LayoutFlexWrap,
875    azul_css::props::property::CssPropertyType::FlexWrap,
876    compact = get_flex_wrap
877);
878
879get_css_property!(
880    get_justify_content,
881    get_justify_content,
882    LayoutJustifyContent,
883    azul_css::props::property::CssPropertyType::JustifyContent,
884    compact = get_justify_content
885);
886
887get_css_property!(
888    get_text_align,
889    get_text_align,
890    StyleTextAlign,
891    azul_css::props::property::CssPropertyType::TextAlign,
892    compact = get_text_align
893);
894
895get_css_property!(
896    get_float,
897    get_float,
898    LayoutFloat,
899    azul_css::props::property::CssPropertyType::Float,
900    compact = get_float
901);
902
903get_css_property!(
904    get_clear,
905    get_clear,
906    LayoutClear,
907    azul_css::props::property::CssPropertyType::Clear,
908    compact = get_clear
909);
910
911get_css_property!(
912    get_overflow_x,
913    get_overflow_x,
914    LayoutOverflow,
915    azul_css::props::property::CssPropertyType::OverflowX,
916    compact = get_overflow_x
917);
918
919get_css_property!(
920    get_overflow_y,
921    get_overflow_y,
922    LayoutOverflow,
923    azul_css::props::property::CssPropertyType::OverflowY,
924    compact = get_overflow_y
925);
926
927get_css_property!(
928    get_position,
929    get_position,
930    LayoutPosition,
931    azul_css::props::property::CssPropertyType::Position,
932    compact = get_position
933);
934
935get_css_property!(
936    get_css_box_sizing,
937    get_box_sizing,
938    LayoutBoxSizing,
939    azul_css::props::property::CssPropertyType::BoxSizing,
940    compact = get_box_sizing
941);
942
943get_css_property!(
944    get_flex_direction,
945    get_flex_direction,
946    LayoutFlexDirection,
947    azul_css::props::property::CssPropertyType::FlexDirection,
948    compact = get_flex_direction
949);
950
951get_css_property!(
952    get_align_items,
953    get_align_items,
954    LayoutAlignItems,
955    azul_css::props::property::CssPropertyType::AlignItems,
956    compact = get_align_items
957);
958
959get_css_property!(
960    get_align_content,
961    get_align_content,
962    LayoutAlignContent,
963    azul_css::props::property::CssPropertyType::AlignContent,
964    compact = get_align_content
965);
966
967get_css_property!(
968    get_font_weight_property,
969    get_font_weight,
970    StyleFontWeight,
971    azul_css::props::property::CssPropertyType::FontWeight,
972    compact = get_font_weight
973);
974
975get_css_property!(
976    get_font_style_property,
977    get_font_style,
978    StyleFontStyle,
979    azul_css::props::property::CssPropertyType::FontStyle,
980    compact = get_font_style
981);
982
983get_css_property!(
984    get_visibility,
985    get_visibility,
986    StyleVisibility,
987    azul_css::props::property::CssPropertyType::Visibility,
988    compact = get_visibility
989);
990
991get_css_property!(
992    get_white_space_property,
993    get_white_space,
994    StyleWhiteSpace,
995    azul_css::props::property::CssPropertyType::WhiteSpace,
996    compact = get_white_space
997);
998
999get_css_property!(
1000    get_direction_property,
1001    get_direction,
1002    StyleDirection,
1003    azul_css::props::property::CssPropertyType::Direction,
1004    compact = get_direction
1005);
1006
1007get_css_property!(
1008    get_vertical_align_property,
1009    get_vertical_align,
1010    StyleVerticalAlign,
1011    azul_css::props::property::CssPropertyType::VerticalAlign,
1012    compact = get_vertical_align
1013);
1014// Complex Property Getters
1015
1016/// Get border radius for all four corners (raw CSS property values)
1017pub fn get_style_border_radius(
1018    styled_dom: &StyledDom,
1019    node_id: NodeId,
1020    node_state: &StyledNodeState,
1021) -> azul_css::props::style::border_radius::StyleBorderRadius {
1022    let node_data = &styled_dom.node_data.as_container()[node_id];
1023
1024    let top_left = styled_dom
1025        .css_property_cache
1026        .ptr
1027        .get_border_top_left_radius(node_data, &node_id, node_state)
1028        .and_then(|br| br.get_property_or_default())
1029        .map(|v| v.inner)
1030        .unwrap_or_default();
1031
1032    let top_right = styled_dom
1033        .css_property_cache
1034        .ptr
1035        .get_border_top_right_radius(node_data, &node_id, node_state)
1036        .and_then(|br| br.get_property_or_default())
1037        .map(|v| v.inner)
1038        .unwrap_or_default();
1039
1040    let bottom_right = styled_dom
1041        .css_property_cache
1042        .ptr
1043        .get_border_bottom_right_radius(node_data, &node_id, node_state)
1044        .and_then(|br| br.get_property_or_default())
1045        .map(|v| v.inner)
1046        .unwrap_or_default();
1047
1048    let bottom_left = styled_dom
1049        .css_property_cache
1050        .ptr
1051        .get_border_bottom_left_radius(node_data, &node_id, node_state)
1052        .and_then(|br| br.get_property_or_default())
1053        .map(|v| v.inner)
1054        .unwrap_or_default();
1055
1056    StyleBorderRadius {
1057        top_left,
1058        top_right,
1059        bottom_right,
1060        bottom_left,
1061    }
1062}
1063
1064/// Get border radius for all four corners (resolved to pixels)
1065///
1066/// # Arguments
1067/// * `element_size` - The element's own size (width × height) for % resolution. According to CSS
1068///   spec, border-radius % uses element's own dimensions.
1069pub fn get_border_radius(
1070    styled_dom: &StyledDom,
1071    node_id: NodeId,
1072    node_state: &StyledNodeState,
1073    element_size: PhysicalSizeImport,
1074    viewport_size: LogicalSize,
1075) -> BorderRadius {
1076    use azul_css::props::basic::{PhysicalSize, PropertyContext, ResolutionContext};
1077
1078    let node_data = &styled_dom.node_data.as_container()[node_id];
1079
1080    // Get font sizes for em/rem resolution
1081    let element_font_size = get_element_font_size(styled_dom, node_id, node_state);
1082    let parent_font_size = styled_dom
1083        .node_hierarchy
1084        .as_container()
1085        .get(node_id)
1086        .and_then(|node| node.parent_id())
1087        .map(|p| get_element_font_size(styled_dom, p, node_state))
1088        .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE);
1089    let root_font_size = get_root_font_size(styled_dom, node_state);
1090
1091    // Create resolution context
1092    let context = ResolutionContext {
1093        element_font_size,
1094        parent_font_size,
1095        root_font_size,
1096        containing_block_size: PhysicalSize::new(0.0, 0.0), // Not used for border-radius
1097        element_size: Some(PhysicalSize::new(element_size.width, element_size.height)),
1098        viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
1099    };
1100
1101    let top_left = styled_dom
1102        .css_property_cache
1103        .ptr
1104        .get_border_top_left_radius(node_data, &node_id, node_state)
1105        .and_then(|br| br.get_property().cloned())
1106        .unwrap_or_default();
1107
1108    let top_right = styled_dom
1109        .css_property_cache
1110        .ptr
1111        .get_border_top_right_radius(node_data, &node_id, node_state)
1112        .and_then(|br| br.get_property().cloned())
1113        .unwrap_or_default();
1114
1115    let bottom_right = styled_dom
1116        .css_property_cache
1117        .ptr
1118        .get_border_bottom_right_radius(node_data, &node_id, node_state)
1119        .and_then(|br| br.get_property().cloned())
1120        .unwrap_or_default();
1121
1122    let bottom_left = styled_dom
1123        .css_property_cache
1124        .ptr
1125        .get_border_bottom_left_radius(node_data, &node_id, node_state)
1126        .and_then(|br| br.get_property().cloned())
1127        .unwrap_or_default();
1128
1129    BorderRadius {
1130        top_left: top_left
1131            .inner
1132            .resolve_with_context(&context, PropertyContext::BorderRadius),
1133        top_right: top_right
1134            .inner
1135            .resolve_with_context(&context, PropertyContext::BorderRadius),
1136        bottom_right: bottom_right
1137            .inner
1138            .resolve_with_context(&context, PropertyContext::BorderRadius),
1139        bottom_left: bottom_left
1140            .inner
1141            .resolve_with_context(&context, PropertyContext::BorderRadius),
1142    }
1143}
1144
1145/// Get z-index for stacking context ordering.
1146///
1147/// Returns the resolved integer z-index value:
1148/// - `z-index: auto` → 0 (participates in parent's stacking context)
1149/// - `z-index: <integer>` → that integer value
1150pub fn get_z_index(styled_dom: &StyledDom, node_id: Option<NodeId>) -> i32 {
1151    use azul_css::props::layout::position::LayoutZIndex;
1152
1153    let node_id = match node_id {
1154        Some(id) => id,
1155        None => return 0,
1156    };
1157
1158    let node_state = &styled_dom.styled_nodes.as_container()[node_id].styled_node_state;
1159
1160    // FAST PATH: compact cache for normal state
1161    if node_state.is_normal() {
1162        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1163            let raw = cc.get_z_index(node_id.index());
1164            if raw == azul_css::compact_cache::I16_AUTO {
1165                return 0;
1166            }
1167            if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
1168                return raw as i32;
1169            }
1170            // I16_SENTINEL → fall through to slow path
1171        }
1172    }
1173
1174    // SLOW PATH
1175    let node_data = &styled_dom.node_data.as_container()[node_id];
1176
1177    styled_dom
1178        .css_property_cache
1179        .ptr
1180        .get_z_index(node_data, &node_id, &node_state)
1181        .and_then(|v| v.get_property())
1182        .map(|z| match z {
1183            LayoutZIndex::Auto => 0,
1184            LayoutZIndex::Integer(i) => *i,
1185        })
1186        .unwrap_or(0)
1187}
1188
1189// Rendering Property Getters
1190
1191/// Information about background color for a node
1192///
1193/// # CSS Background Propagation (Special Case for HTML Root)
1194///
1195/// According to CSS Backgrounds and Borders Module Level 3, Section "The Canvas Background
1196/// and the HTML `<body>` Element":
1197///
1198/// For HTML documents where the root element is `<html>`, if the computed value of
1199/// `background-image` on the root element is `none` AND its `background-color` is `transparent`,
1200/// user agents **must propagate** the computed values of the background properties from the
1201/// first `<body>` child element to the root element.
1202///
1203/// This behavior exists for backwards compatibility with older HTML where backgrounds were
1204/// typically set on `<body>` using `bgcolor` attributes, and ensures that the `<body>`
1205/// background covers the entire viewport/canvas even when `<body>` itself has constrained
1206/// dimensions.
1207///
1208/// Implementation: When requesting the background of an `<html>` node, we first check if it
1209/// has a transparent background with no image. If so, we look for a `<body>` child and use
1210/// its background instead.
1211pub fn get_background_color(
1212    styled_dom: &StyledDom,
1213    node_id: NodeId,
1214    node_state: &StyledNodeState,
1215) -> ColorU {
1216    let node_data = &styled_dom.node_data.as_container()[node_id];
1217
1218    // Fast path: Get this node's background
1219    let get_node_bg = |node_id: NodeId, node_data: &azul_core::dom::NodeData| {
1220        styled_dom
1221            .css_property_cache
1222            .ptr
1223            .get_background_content(node_data, &node_id, node_state)
1224            .and_then(|bg| bg.get_property())
1225            .and_then(|bg_vec| bg_vec.get(0).cloned())
1226            .and_then(|first_bg| match &first_bg {
1227                azul_css::props::style::StyleBackgroundContent::Color(color) => Some(color.clone()),
1228                azul_css::props::style::StyleBackgroundContent::Image(_) => None, // Has image, not transparent
1229                _ => None,
1230            })
1231    };
1232
1233    let own_bg = get_node_bg(node_id, node_data);
1234
1235    // CSS Background Propagation: Special handling for <html> root element
1236    // Only check propagation if this is an Html node AND has transparent background (no
1237    // color/image)
1238    if !matches!(node_data.node_type, NodeType::Html) || own_bg.is_some() {
1239        // Not Html or has its own background - return own background or transparent
1240        return own_bg.unwrap_or(ColorU {
1241            r: 0,
1242            g: 0,
1243            b: 0,
1244            a: 0,
1245        });
1246    }
1247
1248    // Html node with transparent background - check if we should propagate from <body>
1249    let first_child = styled_dom
1250        .node_hierarchy
1251        .as_container()
1252        .get(node_id)
1253        .and_then(|node| node.first_child_id(node_id));
1254
1255    let Some(first_child) = first_child else {
1256        return ColorU {
1257            r: 0,
1258            g: 0,
1259            b: 0,
1260            a: 0,
1261        };
1262    };
1263
1264    let first_child_data = &styled_dom.node_data.as_container()[first_child];
1265
1266    // Check if first child is <body>
1267    if !matches!(first_child_data.node_type, NodeType::Body) {
1268        return ColorU {
1269            r: 0,
1270            g: 0,
1271            b: 0,
1272            a: 0,
1273        };
1274    }
1275
1276    // Propagate <body>'s background to <html> (canvas)
1277    get_node_bg(first_child, first_child_data).unwrap_or(ColorU {
1278        r: 0,
1279        g: 0,
1280        b: 0,
1281        a: 0,
1282    })
1283}
1284
1285/// Returns all background content layers for a node (colors, gradients, images).
1286/// This is used for rendering backgrounds that may include linear/radial/conic gradients.
1287///
1288/// CSS Background Propagation (CSS Backgrounds 3, Section 2.11.2):
1289/// For HTML documents, if the root `<html>` element has no background (transparent with no image),
1290/// propagate the background from the first `<body>` child element.
1291pub fn get_background_contents(
1292    styled_dom: &StyledDom,
1293    node_id: NodeId,
1294    node_state: &StyledNodeState,
1295) -> Vec<azul_css::props::style::StyleBackgroundContent> {
1296    use azul_core::dom::NodeType;
1297    use azul_css::props::style::StyleBackgroundContent;
1298
1299    let node_data = &styled_dom.node_data.as_container()[node_id];
1300
1301    // Helper to get backgrounds for a node
1302    let get_node_backgrounds =
1303        |nid: NodeId, ndata: &azul_core::dom::NodeData| -> Vec<StyleBackgroundContent> {
1304            styled_dom
1305                .css_property_cache
1306                .ptr
1307                .get_background_content(ndata, &nid, node_state)
1308                .and_then(|bg| bg.get_property())
1309                .map(|bg_vec| bg_vec.iter().cloned().collect())
1310                .unwrap_or_default()
1311        };
1312
1313    let own_backgrounds = get_node_backgrounds(node_id, node_data);
1314
1315    // CSS Background Propagation: Special handling for <html> root element
1316    // Only check propagation if this is an Html node AND has no backgrounds
1317    if !matches!(node_data.node_type, NodeType::Html) || !own_backgrounds.is_empty() {
1318        return own_backgrounds;
1319    }
1320
1321    // Html node with no backgrounds - check if we should propagate from <body>
1322    let first_child = styled_dom
1323        .node_hierarchy
1324        .as_container()
1325        .get(node_id)
1326        .and_then(|node| node.first_child_id(node_id));
1327
1328    let Some(first_child) = first_child else {
1329        return own_backgrounds;
1330    };
1331
1332    let first_child_data = &styled_dom.node_data.as_container()[first_child];
1333
1334    // Check if first child is <body>
1335    if !matches!(first_child_data.node_type, NodeType::Body) {
1336        return own_backgrounds;
1337    }
1338
1339    // Propagate <body>'s backgrounds to <html> (canvas)
1340    get_node_backgrounds(first_child, first_child_data)
1341}
1342
1343/// Information about border rendering
1344pub struct BorderInfo {
1345    pub widths: crate::solver3::display_list::StyleBorderWidths,
1346    pub colors: crate::solver3::display_list::StyleBorderColors,
1347    pub styles: crate::solver3::display_list::StyleBorderStyles,
1348}
1349
1350pub fn get_border_info(
1351    styled_dom: &StyledDom,
1352    node_id: NodeId,
1353    node_state: &StyledNodeState,
1354) -> BorderInfo {
1355    use crate::solver3::display_list::{StyleBorderColors, StyleBorderStyles, StyleBorderWidths};
1356    use azul_css::css::CssPropertyValue;
1357    use azul_css::props::basic::color::ColorU;
1358    use azul_css::props::style::border::{
1359        BorderStyle, StyleBorderTopColor, StyleBorderRightColor,
1360        StyleBorderBottomColor, StyleBorderLeftColor,
1361        StyleBorderTopStyle, StyleBorderRightStyle,
1362        StyleBorderBottomStyle, StyleBorderLeftStyle,
1363    };
1364
1365    // FAST PATH: compact cache for normal state
1366    if node_state.is_normal() {
1367        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
1368            let idx = node_id.index();
1369
1370            // Border widths (already have compact path via i16)
1371            let node_data = &styled_dom.node_data.as_container()[node_id];
1372            let widths = StyleBorderWidths {
1373                top: styled_dom.css_property_cache.ptr
1374                    .get_border_top_width(node_data, &node_id, node_state).cloned(),
1375                right: styled_dom.css_property_cache.ptr
1376                    .get_border_right_width(node_data, &node_id, node_state).cloned(),
1377                bottom: styled_dom.css_property_cache.ptr
1378                    .get_border_bottom_width(node_data, &node_id, node_state).cloned(),
1379                left: styled_dom.css_property_cache.ptr
1380                    .get_border_left_width(node_data, &node_id, node_state).cloned(),
1381            };
1382
1383            // Border colors from compact cache
1384            let make_color = |raw: u32| -> Option<ColorU> {
1385                if raw == 0 { None } else {
1386                    Some(ColorU {
1387                        r: ((raw >> 24) & 0xFF) as u8,
1388                        g: ((raw >> 16) & 0xFF) as u8,
1389                        b: ((raw >> 8) & 0xFF) as u8,
1390                        a: (raw & 0xFF) as u8,
1391                    })
1392                }
1393            };
1394
1395            let colors = StyleBorderColors {
1396                top: make_color(cc.get_border_top_color_raw(idx))
1397                    .map(|c| CssPropertyValue::Exact(StyleBorderTopColor { inner: c })),
1398                right: make_color(cc.get_border_right_color_raw(idx))
1399                    .map(|c| CssPropertyValue::Exact(StyleBorderRightColor { inner: c })),
1400                bottom: make_color(cc.get_border_bottom_color_raw(idx))
1401                    .map(|c| CssPropertyValue::Exact(StyleBorderBottomColor { inner: c })),
1402                left: make_color(cc.get_border_left_color_raw(idx))
1403                    .map(|c| CssPropertyValue::Exact(StyleBorderLeftColor { inner: c })),
1404            };
1405
1406            // Border styles from compact cache
1407            let styles = StyleBorderStyles {
1408                top: Some(CssPropertyValue::Exact(StyleBorderTopStyle {
1409                    inner: cc.get_border_top_style(idx),
1410                })),
1411                right: Some(CssPropertyValue::Exact(StyleBorderRightStyle {
1412                    inner: cc.get_border_right_style(idx),
1413                })),
1414                bottom: Some(CssPropertyValue::Exact(StyleBorderBottomStyle {
1415                    inner: cc.get_border_bottom_style(idx),
1416                })),
1417                left: Some(CssPropertyValue::Exact(StyleBorderLeftStyle {
1418                    inner: cc.get_border_left_style(idx),
1419                })),
1420            };
1421
1422            return BorderInfo { widths, colors, styles };
1423        }
1424    }
1425
1426    // SLOW PATH: full cascade
1427    let node_data = &styled_dom.node_data.as_container()[node_id];
1428
1429    // Get all border widths
1430    let widths = StyleBorderWidths {
1431        top: styled_dom
1432            .css_property_cache
1433            .ptr
1434            .get_border_top_width(node_data, &node_id, node_state)
1435            .cloned(),
1436        right: styled_dom
1437            .css_property_cache
1438            .ptr
1439            .get_border_right_width(node_data, &node_id, node_state)
1440            .cloned(),
1441        bottom: styled_dom
1442            .css_property_cache
1443            .ptr
1444            .get_border_bottom_width(node_data, &node_id, node_state)
1445            .cloned(),
1446        left: styled_dom
1447            .css_property_cache
1448            .ptr
1449            .get_border_left_width(node_data, &node_id, node_state)
1450            .cloned(),
1451    };
1452
1453    // Get all border colors
1454    let colors = StyleBorderColors {
1455        top: styled_dom
1456            .css_property_cache
1457            .ptr
1458            .get_border_top_color(node_data, &node_id, node_state)
1459            .cloned(),
1460        right: styled_dom
1461            .css_property_cache
1462            .ptr
1463            .get_border_right_color(node_data, &node_id, node_state)
1464            .cloned(),
1465        bottom: styled_dom
1466            .css_property_cache
1467            .ptr
1468            .get_border_bottom_color(node_data, &node_id, node_state)
1469            .cloned(),
1470        left: styled_dom
1471            .css_property_cache
1472            .ptr
1473            .get_border_left_color(node_data, &node_id, node_state)
1474            .cloned(),
1475    };
1476
1477    // Get all border styles
1478    let styles = StyleBorderStyles {
1479        top: styled_dom
1480            .css_property_cache
1481            .ptr
1482            .get_border_top_style(node_data, &node_id, node_state)
1483            .cloned(),
1484        right: styled_dom
1485            .css_property_cache
1486            .ptr
1487            .get_border_right_style(node_data, &node_id, node_state)
1488            .cloned(),
1489        bottom: styled_dom
1490            .css_property_cache
1491            .ptr
1492            .get_border_bottom_style(node_data, &node_id, node_state)
1493            .cloned(),
1494        left: styled_dom
1495            .css_property_cache
1496            .ptr
1497            .get_border_left_style(node_data, &node_id, node_state)
1498            .cloned(),
1499    };
1500
1501    BorderInfo {
1502        widths,
1503        colors,
1504        styles,
1505    }
1506}
1507
1508/// Convert BorderInfo to InlineBorderInfo for inline elements
1509///
1510/// This resolves the CSS property values to concrete pixel values and colors
1511/// that can be used during text rendering.
1512pub fn get_inline_border_info(
1513    styled_dom: &StyledDom,
1514    node_id: NodeId,
1515    node_state: &StyledNodeState,
1516    border_info: &BorderInfo,
1517) -> Option<crate::text3::cache::InlineBorderInfo> {
1518    use crate::text3::cache::InlineBorderInfo;
1519
1520    // Helper to extract pixel value from border width
1521    fn get_border_width_px(
1522        width: &Option<
1523            azul_css::css::CssPropertyValue<azul_css::props::style::border::LayoutBorderTopWidth>,
1524        >,
1525    ) -> f32 {
1526        width
1527            .as_ref()
1528            .and_then(|v| v.get_property())
1529            .map(|w| w.inner.number.get())
1530            .unwrap_or(0.0)
1531    }
1532
1533    fn get_border_width_px_right(
1534        width: &Option<
1535            azul_css::css::CssPropertyValue<azul_css::props::style::border::LayoutBorderRightWidth>,
1536        >,
1537    ) -> f32 {
1538        width
1539            .as_ref()
1540            .and_then(|v| v.get_property())
1541            .map(|w| w.inner.number.get())
1542            .unwrap_or(0.0)
1543    }
1544
1545    fn get_border_width_px_bottom(
1546        width: &Option<
1547            azul_css::css::CssPropertyValue<
1548                azul_css::props::style::border::LayoutBorderBottomWidth,
1549            >,
1550        >,
1551    ) -> f32 {
1552        width
1553            .as_ref()
1554            .and_then(|v| v.get_property())
1555            .map(|w| w.inner.number.get())
1556            .unwrap_or(0.0)
1557    }
1558
1559    fn get_border_width_px_left(
1560        width: &Option<
1561            azul_css::css::CssPropertyValue<azul_css::props::style::border::LayoutBorderLeftWidth>,
1562        >,
1563    ) -> f32 {
1564        width
1565            .as_ref()
1566            .and_then(|v| v.get_property())
1567            .map(|w| w.inner.number.get())
1568            .unwrap_or(0.0)
1569    }
1570
1571    // Helper to extract color from border color
1572    fn get_border_color_top(
1573        color: &Option<
1574            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderTopColor>,
1575        >,
1576    ) -> ColorU {
1577        color
1578            .as_ref()
1579            .and_then(|v| v.get_property())
1580            .map(|c| c.inner)
1581            .unwrap_or(ColorU::BLACK)
1582    }
1583
1584    fn get_border_color_right(
1585        color: &Option<
1586            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderRightColor>,
1587        >,
1588    ) -> ColorU {
1589        color
1590            .as_ref()
1591            .and_then(|v| v.get_property())
1592            .map(|c| c.inner)
1593            .unwrap_or(ColorU::BLACK)
1594    }
1595
1596    fn get_border_color_bottom(
1597        color: &Option<
1598            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderBottomColor>,
1599        >,
1600    ) -> ColorU {
1601        color
1602            .as_ref()
1603            .and_then(|v| v.get_property())
1604            .map(|c| c.inner)
1605            .unwrap_or(ColorU::BLACK)
1606    }
1607
1608    fn get_border_color_left(
1609        color: &Option<
1610            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderLeftColor>,
1611        >,
1612    ) -> ColorU {
1613        color
1614            .as_ref()
1615            .and_then(|v| v.get_property())
1616            .map(|c| c.inner)
1617            .unwrap_or(ColorU::BLACK)
1618    }
1619
1620    // Extract border-radius (simplified - uses the average of all corners if uniform)
1621    fn get_border_radius_px(
1622        styled_dom: &StyledDom,
1623        node_id: NodeId,
1624        node_state: &StyledNodeState,
1625    ) -> Option<f32> {
1626        let node_data = &styled_dom.node_data.as_container()[node_id];
1627
1628        let top_left = styled_dom
1629            .css_property_cache
1630            .ptr
1631            .get_border_top_left_radius(node_data, &node_id, node_state)
1632            .and_then(|br| br.get_property().cloned())
1633            .map(|v| v.inner.number.get());
1634
1635        let top_right = styled_dom
1636            .css_property_cache
1637            .ptr
1638            .get_border_top_right_radius(node_data, &node_id, node_state)
1639            .and_then(|br| br.get_property().cloned())
1640            .map(|v| v.inner.number.get());
1641
1642        let bottom_left = styled_dom
1643            .css_property_cache
1644            .ptr
1645            .get_border_bottom_left_radius(node_data, &node_id, node_state)
1646            .and_then(|br| br.get_property().cloned())
1647            .map(|v| v.inner.number.get());
1648
1649        let bottom_right = styled_dom
1650            .css_property_cache
1651            .ptr
1652            .get_border_bottom_right_radius(node_data, &node_id, node_state)
1653            .and_then(|br| br.get_property().cloned())
1654            .map(|v| v.inner.number.get());
1655
1656        // If any radius is defined, use the maximum (for inline, uniform radius is most common)
1657        let radii: Vec<f32> = [top_left, top_right, bottom_left, bottom_right]
1658            .into_iter()
1659            .filter_map(|r| r)
1660            .collect();
1661
1662        if radii.is_empty() {
1663            None
1664        } else {
1665            Some(radii.into_iter().fold(0.0f32, |a, b| a.max(b)))
1666        }
1667    }
1668
1669    let top = get_border_width_px(&border_info.widths.top);
1670    let right = get_border_width_px_right(&border_info.widths.right);
1671    let bottom = get_border_width_px_bottom(&border_info.widths.bottom);
1672    let left = get_border_width_px_left(&border_info.widths.left);
1673
1674    // Fetch padding values for inline elements
1675    fn resolve_padding(mv: MultiValue<PixelValue>) -> f32 {
1676        match mv {
1677            MultiValue::Exact(pv) => {
1678                use azul_css::props::basic::SizeMetric;
1679                match pv.metric {
1680                    SizeMetric::Px => pv.number.get(),
1681                    SizeMetric::Pt => pv.number.get() * 1.333333,
1682                    SizeMetric::Em | SizeMetric::Rem => pv.number.get() * 16.0,
1683                    _ => 0.0,
1684                }
1685            }
1686            _ => 0.0,
1687        }
1688    }
1689
1690    let p_top = resolve_padding(get_css_padding_top(styled_dom, node_id, node_state));
1691    let p_right = resolve_padding(get_css_padding_right(styled_dom, node_id, node_state));
1692    let p_bottom = resolve_padding(get_css_padding_bottom(styled_dom, node_id, node_state));
1693    let p_left = resolve_padding(get_css_padding_left(styled_dom, node_id, node_state));
1694
1695    // Only return Some if there's actually a border or padding
1696    let has_border = top > 0.0 || right > 0.0 || bottom > 0.0 || left > 0.0;
1697    let has_padding = p_top > 0.0 || p_right > 0.0 || p_bottom > 0.0 || p_left > 0.0;
1698    if !has_border && !has_padding {
1699        return None;
1700    }
1701
1702    Some(InlineBorderInfo {
1703        top,
1704        right,
1705        bottom,
1706        left,
1707        top_color: get_border_color_top(&border_info.colors.top),
1708        right_color: get_border_color_right(&border_info.colors.right),
1709        bottom_color: get_border_color_bottom(&border_info.colors.bottom),
1710        left_color: get_border_color_left(&border_info.colors.left),
1711        radius: get_border_radius_px(styled_dom, node_id, node_state),
1712        padding_top: p_top,
1713        padding_right: p_right,
1714        padding_bottom: p_bottom,
1715        padding_left: p_left,
1716    })
1717}
1718
1719// Selection and Caret Styling
1720
1721/// Style information for text selection rendering
1722#[derive(Debug, Clone, Copy, Default)]
1723pub struct SelectionStyle {
1724    /// Background color of the selection highlight
1725    pub bg_color: ColorU,
1726    /// Text color when selected (overrides normal text color)
1727    pub text_color: Option<ColorU>,
1728    /// Border radius for selection rectangles
1729    pub radius: f32,
1730}
1731
1732/// Get selection style for a node
1733pub fn get_selection_style(
1734    styled_dom: &StyledDom, 
1735    node_id: Option<NodeId>,
1736    system_style: Option<&std::sync::Arc<azul_css::system::SystemStyle>>,
1737) -> SelectionStyle {
1738    let Some(node_id) = node_id else {
1739        return SelectionStyle::default();
1740    };
1741
1742    let node_data = &styled_dom.node_data.as_container()[node_id];
1743    let node_state = &StyledNodeState::default();
1744
1745    // Try to get selection background from CSS, otherwise use system color, otherwise hard-coded default
1746    let default_bg = system_style
1747        .and_then(|ss| ss.colors.selection_background.as_option().copied())
1748        .unwrap_or(ColorU {
1749            r: 51,
1750            g: 153,
1751            b: 255, // Standard blue selection color
1752            a: 128, // Semi-transparent
1753        });
1754
1755    let bg_color = styled_dom
1756        .css_property_cache
1757        .ptr
1758        .get_selection_background_color(node_data, &node_id, node_state)
1759        .and_then(|c| c.get_property().cloned())
1760        .map(|c| c.inner)
1761        .unwrap_or(default_bg);
1762
1763    // Try to get selection text color from CSS, otherwise use system color
1764    let default_text = system_style
1765        .and_then(|ss| ss.colors.selection_text.as_option().copied());
1766
1767    let text_color = styled_dom
1768        .css_property_cache
1769        .ptr
1770        .get_selection_color(node_data, &node_id, node_state)
1771        .and_then(|c| c.get_property().cloned())
1772        .map(|c| c.inner)
1773        .or(default_text);
1774
1775    let radius = styled_dom
1776        .css_property_cache
1777        .ptr
1778        .get_selection_radius(node_data, &node_id, node_state)
1779        .and_then(|r| r.get_property().cloned())
1780        .map(|r| r.inner.to_pixels_internal(0.0, 16.0)) // percent=0, em=16px default font size
1781        .unwrap_or(0.0);
1782
1783    SelectionStyle {
1784        bg_color,
1785        text_color,
1786        radius,
1787    }
1788}
1789
1790/// Style information for caret rendering
1791#[derive(Debug, Clone, Copy, Default)]
1792pub struct CaretStyle {
1793    pub color: ColorU,
1794    pub width: f32,
1795    pub animation_duration: u32,
1796}
1797
1798/// Get caret style for a node
1799pub fn get_caret_style(styled_dom: &StyledDom, node_id: Option<NodeId>) -> CaretStyle {
1800    let Some(node_id) = node_id else {
1801        return CaretStyle::default();
1802    };
1803
1804    let node_data = &styled_dom.node_data.as_container()[node_id];
1805    let node_state = &StyledNodeState::default();
1806
1807    let color = styled_dom
1808        .css_property_cache
1809        .ptr
1810        .get_caret_color(node_data, &node_id, node_state)
1811        .and_then(|c| c.get_property().cloned())
1812        .map(|c| c.inner)
1813        .unwrap_or(ColorU {
1814            r: 255,
1815            g: 255,
1816            b: 255,
1817            a: 255, // White caret by default
1818        });
1819
1820    let width = styled_dom
1821        .css_property_cache
1822        .ptr
1823        .get_caret_width(node_data, &node_id, node_state)
1824        .and_then(|w| w.get_property().cloned())
1825        .map(|w| w.inner.to_pixels_internal(0.0, 16.0)) // 16.0 as default em size
1826        .unwrap_or(2.0); // 2px width by default
1827
1828    let animation_duration = styled_dom
1829        .css_property_cache
1830        .ptr
1831        .get_caret_animation_duration(node_data, &node_id, node_state)
1832        .and_then(|d| d.get_property().cloned())
1833        .map(|d| d.inner.inner) // Duration.inner is the u32 milliseconds value
1834        .unwrap_or(500); // 500ms blink by default
1835
1836    CaretStyle {
1837        color,
1838        width,
1839        animation_duration,
1840    }
1841}
1842
1843// Scrollbar Information
1844
1845/// Get scrollbar information from a layout node.
1846///
1847/// Scrollbar requirements are computed during the layout phase in two paths:
1848/// - BFC layout: `compute_scrollbar_info()` + `merge_scrollbar_info()` in cache.rs
1849/// - Taffy layout: set in the measure callback in taffy_bridge.rs
1850///
1851/// If neither path set `scrollbar_info`, the node genuinely does not need
1852/// scrollbars. The previous heuristic (>3 children = force overflow) caused
1853/// false-positive scrollbars on normal containers.
1854pub fn get_scrollbar_info_from_layout(node: &LayoutNode) -> ScrollbarRequirements {
1855    node.scrollbar_info
1856        .clone()
1857        .unwrap_or_default()
1858}
1859
1860/// Resolve the **layout-effective** scrollbar width for a node, in pixels.
1861///
1862/// This combines three inputs:
1863/// 1. CSS `scrollbar-width` property on the node (`auto` → 16, `thin` → 8, `none` → 0)
1864/// 2. OS-level `ScrollbarPreferences.visibility` (overlay scrollbars → 0 layout reservation)
1865/// 3. Custom `-azul-scrollbar-style` width override
1866///
1867/// For **overlay** scrollbars (macOS `WhenScrolling`, or equivalent), this returns `0.0`
1868/// because overlay scrollbars are painted on top of content and do not consume layout space.
1869/// The scrollbar is still *rendered*, but no space is reserved during layout.
1870///
1871/// During display-list generation, use `get_scrollbar_style()` instead — that returns
1872/// the full visual style including the *paint* width (which may be non-zero for overlay).
1873pub fn get_layout_scrollbar_width_px<T: crate::font_traits::ParsedFontTrait>(
1874    ctx: &crate::solver3::LayoutContext<'_, T>,
1875    dom_id: NodeId,
1876    styled_node_state: &StyledNodeState,
1877) -> f32 {
1878    use azul_css::props::style::scrollbar::LayoutScrollbarWidth;
1879
1880    // Check OS-level preference: overlay scrollbars reserve no layout space.
1881    if let Some(ref sys) = ctx.system_style {
1882        use azul_css::system::ScrollbarVisibility;
1883        match sys.scrollbar_preferences.visibility {
1884            ScrollbarVisibility::WhenScrolling => return 0.0, // overlay
1885            ScrollbarVisibility::Always | ScrollbarVisibility::Automatic => {}
1886        }
1887    }
1888
1889    // Per-node CSS resolution
1890    get_scrollbar_width_px(ctx.styled_dom, dom_id, styled_node_state)
1891}
1892
1893get_css_property!(
1894    get_display_property_internal,
1895    get_display,
1896    LayoutDisplay,
1897    azul_css::props::property::CssPropertyType::Display,
1898    compact = get_display
1899);
1900
1901pub fn get_display_property(
1902    styled_dom: &StyledDom,
1903    dom_id: Option<NodeId>,
1904) -> MultiValue<LayoutDisplay> {
1905    let Some(id) = dom_id else {
1906        return MultiValue::Exact(LayoutDisplay::Inline);
1907    };
1908    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
1909    get_display_property_internal(styled_dom, id, node_state)
1910}
1911
1912/// Reads the CSS `vertical-align` property for a DOM node and converts it to
1913/// the text3 `VerticalAlign` enum used during inline layout.
1914pub fn get_vertical_align_for_node(
1915    styled_dom: &StyledDom,
1916    dom_id: NodeId,
1917) -> crate::text3::cache::VerticalAlign {
1918    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
1919    let va = match get_vertical_align_property(styled_dom, dom_id, node_state) {
1920        MultiValue::Exact(v) => v,
1921        _ => StyleVerticalAlign::default(),
1922    };
1923    match va {
1924        StyleVerticalAlign::Baseline => crate::text3::cache::VerticalAlign::Baseline,
1925        StyleVerticalAlign::Top => crate::text3::cache::VerticalAlign::Top,
1926        StyleVerticalAlign::Middle => crate::text3::cache::VerticalAlign::Middle,
1927        StyleVerticalAlign::Bottom => crate::text3::cache::VerticalAlign::Bottom,
1928        StyleVerticalAlign::Sub => crate::text3::cache::VerticalAlign::Sub,
1929        StyleVerticalAlign::Superscript => crate::text3::cache::VerticalAlign::Super,
1930        StyleVerticalAlign::TextTop => crate::text3::cache::VerticalAlign::TextTop,
1931        StyleVerticalAlign::TextBottom => crate::text3::cache::VerticalAlign::TextBottom,
1932    }
1933}
1934
1935pub fn get_style_properties(
1936    styled_dom: &StyledDom,
1937    dom_id: NodeId,
1938    system_style: Option<&std::sync::Arc<azul_css::system::SystemStyle>>,
1939) -> StyleProperties {
1940    use azul_css::props::basic::{PhysicalSize, PropertyContext, ResolutionContext};
1941
1942    let node_data = &styled_dom.node_data.as_container()[dom_id];
1943    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
1944    let cache = &styled_dom.css_property_cache.ptr;
1945
1946    // NEW: Get ALL fonts from CSS font-family, not just first
1947    use azul_css::props::basic::font::{StyleFontFamily, StyleFontFamilyVec};
1948
1949    let font_families = cache
1950        .get_font_family(node_data, &dom_id, node_state)
1951        .and_then(|v| v.get_property().cloned())
1952        .unwrap_or_else(|| {
1953            // Default to serif (same as browser default)
1954            StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
1955        });
1956
1957    // Get parent's font-size for proper em resolution in font-size property
1958    let parent_font_size = styled_dom
1959        .node_hierarchy
1960        .as_container()
1961        .get(dom_id)
1962        .and_then(|node| {
1963            let parent_id = CoreNodeId::from_usize(node.parent)?;
1964            // Recursively get parent's font-size
1965            cache
1966                .get_font_size(
1967                    &styled_dom.node_data.as_container()[parent_id],
1968                    &parent_id,
1969                    &styled_dom.styled_nodes.as_container()[parent_id].styled_node_state,
1970                )
1971                .and_then(|v| v.get_property().cloned())
1972                .map(|v| {
1973                    // If parent also has em/rem, we'd need to recurse, but for now use fallback
1974                    use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
1975                    v.inner.to_pixels_internal(0.0, DEFAULT_FONT_SIZE)
1976                })
1977        })
1978        .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE);
1979
1980    let root_font_size = get_root_font_size(styled_dom, node_state);
1981
1982    // Create resolution context for font-size (em refers to parent)
1983    let font_size_context = ResolutionContext {
1984        element_font_size: azul_css::props::basic::pixel::DEFAULT_FONT_SIZE, /* Not used for font-size property */
1985        parent_font_size,
1986        root_font_size,
1987        containing_block_size: PhysicalSize::new(0.0, 0.0),
1988        element_size: None,
1989        viewport_size: PhysicalSize::new(0.0, 0.0), // TODO: Pass viewport from LayoutContext
1990    };
1991
1992    // Get font-size: either from this node's CSS, or inherit from parent
1993    // font-size is an inheritable property, so if the node doesn't have
1994    // an explicit font-size, it should inherit from the parent (not default to 16px)
1995    let font_size = {
1996        // FAST PATH: compact cache for normal state
1997        let mut fast_font_size = None;
1998        if node_state.is_normal() {
1999            if let Some(ref cc) = cache.compact_cache {
2000                let raw = cc.get_font_size_raw(dom_id.index());
2001                if raw != azul_css::compact_cache::U32_SENTINEL
2002                    && raw != azul_css::compact_cache::U32_INHERIT
2003                    && raw != azul_css::compact_cache::U32_INITIAL
2004                {
2005                    if let Some(pv) = azul_css::compact_cache::decode_pixel_value_u32(raw) {
2006                        fast_font_size = Some(pv.resolve_with_context(
2007                            &font_size_context,
2008                            PropertyContext::FontSize,
2009                        ));
2010                    }
2011                }
2012            }
2013        }
2014        fast_font_size.unwrap_or_else(|| {
2015            cache
2016                .get_font_size(node_data, &dom_id, node_state)
2017                .and_then(|v| v.get_property().cloned())
2018                .map(|v| {
2019                    v.inner
2020                        .resolve_with_context(&font_size_context, PropertyContext::FontSize)
2021                })
2022                .unwrap_or(parent_font_size)
2023        })
2024    };
2025
2026    let color_from_cache = {
2027        // FAST PATH: compact cache for text color
2028        let mut fast_color = None;
2029        if node_state.is_normal() {
2030            if let Some(ref cc) = cache.compact_cache {
2031                let raw = cc.get_text_color_raw(dom_id.index());
2032                if raw != 0 {
2033                    // Decode 0xRRGGBBAA → ColorU
2034                    fast_color = Some(ColorU {
2035                        r: (raw >> 24) as u8,
2036                        g: (raw >> 16) as u8,
2037                        b: (raw >> 8) as u8,
2038                        a: raw as u8,
2039                    });
2040                }
2041            }
2042        }
2043        fast_color.or_else(|| {
2044            cache
2045                .get_text_color(node_data, &dom_id, node_state)
2046                .and_then(|v| v.get_property().cloned())
2047                .map(|v| v.inner)
2048        })
2049    };
2050
2051    // Use system text color as fallback (respects dark/light mode)
2052    let system_text_color = system_style
2053        .and_then(|ss| ss.colors.text.as_option().copied())
2054        .unwrap_or(ColorU::BLACK); // Ultimate fallback if no system style
2055    
2056    let color = color_from_cache.unwrap_or(system_text_color);
2057
2058    let line_height = {
2059        // FAST PATH: compact cache for line-height (stored as normalized × 1000 i16)
2060        let mut fast_lh = None;
2061        if node_state.is_normal() {
2062            if let Some(ref cc) = cache.compact_cache {
2063                if let Some(normalized) = cc.get_line_height(dom_id.index()) {
2064                    // normalized is the raw i16 / 1000.0 value from decode_resolved_px_i16
2065                    // But line_height encoding is special: percentage × 10 as i16
2066                    // decode: i16 / 10.0 → raw percentage value (not /100!)
2067                    // Wait - get_line_height uses decode_resolved_px_i16 which does val / 10.0
2068                    // Builder stores: normalized() * 1000.0 as i16
2069                    // So decoded = i16 / 10.0 = normalized() * 100.0
2070                    // We need normalized() * font_size, so: decoded / 100.0 * font_size
2071                    fast_lh = Some(normalized / 100.0 * font_size);
2072                }
2073            }
2074        }
2075        fast_lh.unwrap_or_else(|| {
2076            cache
2077                .get_line_height(node_data, &dom_id, node_state)
2078                .and_then(|v| v.get_property().cloned())
2079                .map(|v| v.inner.normalized() * font_size)
2080                .unwrap_or(font_size * 1.2)
2081        })
2082    };
2083
2084    // Get background color for INLINE elements only
2085    // CSS background-color is NOT inherited. For block-level elements (th, td, div, etc.),
2086    // the background is painted separately by paint_element_background() in display_list.rs.
2087    // Only inline elements (span, em, strong, a, etc.) should have their background color
2088    // propagated through StyleProperties for the text rendering pipeline.
2089    use azul_css::props::layout::LayoutDisplay;
2090    let display = cache
2091        .get_display(node_data, &dom_id, node_state)
2092        .and_then(|v| v.get_property().cloned())
2093        .unwrap_or(LayoutDisplay::Inline);
2094
2095    // For inline and inline-block elements, get background content and border info
2096    // Block elements have their backgrounds/borders painted by display_list.rs
2097    let (background_color, background_content, border) =
2098        if matches!(display, LayoutDisplay::Inline | LayoutDisplay::InlineBlock) {
2099            let bg = get_background_color(styled_dom, dom_id, node_state);
2100            let bg_color = if bg.a > 0 { Some(bg) } else { None };
2101
2102            // Get full background contents (including gradients)
2103            let bg_contents = get_background_contents(styled_dom, dom_id, node_state);
2104
2105            // Get border info for inline elements
2106            let border_info = get_border_info(styled_dom, dom_id, node_state);
2107            let inline_border =
2108                get_inline_border_info(styled_dom, dom_id, node_state, &border_info);
2109
2110            (bg_color, bg_contents, inline_border)
2111        } else {
2112            // Block-level elements: background/border is painted by display_list.rs
2113            // via push_backgrounds_and_border() in DisplayListBuilder
2114            (None, Vec::new(), None)
2115        };
2116
2117    // Query font-weight from CSS cache
2118    let font_weight = match get_font_weight_property(styled_dom, dom_id, node_state) {
2119        MultiValue::Exact(v) => v,
2120        _ => StyleFontWeight::Normal,
2121    };
2122
2123    // Query font-style from CSS cache
2124    let font_style = match get_font_style_property(styled_dom, dom_id, node_state) {
2125        MultiValue::Exact(v) => v,
2126        _ => StyleFontStyle::Normal,
2127    };
2128
2129    // Convert StyleFontWeight/StyleFontStyle to fontconfig types
2130    let fc_weight = super::fc::convert_font_weight(font_weight);
2131    let fc_style = super::fc::convert_font_style(font_style);
2132
2133    // Check if any font family is a FontRef - if so, use FontStack::Ref
2134    // This allows embedded fonts (like Material Icons) to bypass fontconfig
2135    let font_stack = {
2136        // Look for a Ref in the font families
2137        let font_ref = (0..font_families.len())
2138            .find_map(|i| {
2139                match font_families.get(i).unwrap() {
2140                    azul_css::props::basic::font::StyleFontFamily::Ref(r) => Some(r.clone()),
2141                    _ => None,
2142                }
2143            });
2144        
2145        // Get platform for resolving system font types
2146        let platform = system_style.map(|ss| &ss.platform);
2147
2148        if let Some(font_ref) = font_ref {
2149            // Use FontStack::Ref for embedded fonts
2150            FontStack::Ref(font_ref)
2151        } else {
2152            // Build regular font stack from all font families
2153            let mut stack = Vec::with_capacity(font_families.len() + 3);
2154
2155            for i in 0..font_families.len() {
2156                let family = font_families.get(i).unwrap();
2157
2158                // Handle SystemFontType specially - resolve to actual OS font names
2159                // (e.g., "system:ui" → ["System Font", "Helvetica Neue", "Lucida Grande"] on macOS)
2160                if let azul_css::props::basic::font::StyleFontFamily::SystemType(system_type) = family {
2161                    if let Some(platform) = platform {
2162                        let font_names = system_type.get_fallback_chain(platform);
2163                        let system_weight = if system_type.is_bold() {
2164                            rust_fontconfig::FcWeight::Bold
2165                        } else {
2166                            fc_weight
2167                        };
2168                        let system_style_val = if system_type.is_italic() {
2169                            crate::text3::cache::FontStyle::Italic
2170                        } else {
2171                            fc_style
2172                        };
2173                        for font_name in font_names {
2174                            stack.push(crate::text3::cache::FontSelector {
2175                                family: font_name.to_string(),
2176                                weight: system_weight,
2177                                style: system_style_val,
2178                                unicode_ranges: Vec::new(),
2179                            });
2180                        }
2181                    } else {
2182                        // No platform info - fall back to generic sans-serif
2183                        stack.push(crate::text3::cache::FontSelector {
2184                            family: "sans-serif".to_string(),
2185                            weight: fc_weight,
2186                            style: fc_style,
2187                            unicode_ranges: Vec::new(),
2188                        });
2189                    }
2190                } else {
2191                    stack.push(crate::text3::cache::FontSelector {
2192                        family: family.as_string(),
2193                        weight: fc_weight,
2194                        style: fc_style,
2195                        unicode_ranges: Vec::new(),
2196                    });
2197                }
2198            }
2199
2200            // Add generic fallbacks (serif/sans-serif will be resolved based on Unicode ranges later)
2201            let generic_fallbacks = ["sans-serif", "serif", "monospace"];
2202            for fallback in &generic_fallbacks {
2203                if !stack
2204                    .iter()
2205                    .any(|f| f.family.to_lowercase() == fallback.to_lowercase())
2206                {
2207                    stack.push(crate::text3::cache::FontSelector {
2208                        family: fallback.to_string(),
2209                        weight: rust_fontconfig::FcWeight::Normal,
2210                        style: crate::text3::cache::FontStyle::Normal,
2211                        unicode_ranges: Vec::new(),
2212                    });
2213                }
2214            }
2215
2216            FontStack::Stack(stack)
2217        }
2218    };
2219
2220    // Get letter-spacing from CSS
2221    let letter_spacing = {
2222        // FAST PATH: compact cache for letter-spacing (i16 resolved px × 10)
2223        let mut fast_ls = None;
2224        if node_state.is_normal() {
2225            if let Some(ref cc) = cache.compact_cache {
2226                if let Some(px_val) = cc.get_letter_spacing(dom_id.index()) {
2227                    fast_ls = Some(crate::text3::cache::Spacing::Px(px_val.round() as i32));
2228                }
2229            }
2230        }
2231        fast_ls.unwrap_or_else(|| {
2232            cache
2233                .get_letter_spacing(node_data, &dom_id, node_state)
2234                .and_then(|v| v.get_property().cloned())
2235                .map(|v| {
2236                    let px_value = v.inner.resolve_with_context(&font_size_context, PropertyContext::FontSize);
2237                    crate::text3::cache::Spacing::Px(px_value.round() as i32)
2238                })
2239                .unwrap_or_default()
2240        })
2241    };
2242
2243    // Get word-spacing from CSS
2244    let word_spacing = {
2245        // FAST PATH: compact cache for word-spacing (i16 resolved px × 10)
2246        let mut fast_ws = None;
2247        if node_state.is_normal() {
2248            if let Some(ref cc) = cache.compact_cache {
2249                if let Some(px_val) = cc.get_word_spacing(dom_id.index()) {
2250                    fast_ws = Some(crate::text3::cache::Spacing::Px(px_val.round() as i32));
2251                }
2252            }
2253        }
2254        fast_ws.unwrap_or_else(|| {
2255            cache
2256                .get_word_spacing(node_data, &dom_id, node_state)
2257                .and_then(|v| v.get_property().cloned())
2258                .map(|v| {
2259                    let px_value = v.inner.resolve_with_context(&font_size_context, PropertyContext::FontSize);
2260                    crate::text3::cache::Spacing::Px(px_value.round() as i32)
2261                })
2262                .unwrap_or_default()
2263        })
2264    };
2265
2266    // Get text-decoration from CSS
2267    let text_decoration = cache
2268        .get_text_decoration(node_data, &dom_id, node_state)
2269        .and_then(|v| v.get_property().cloned())
2270        .map(|v| crate::text3::cache::TextDecoration::from_css(v))
2271        .unwrap_or_default();
2272
2273    // Get tab-size (tab-size) from CSS
2274    let tab_size = {
2275        // FAST PATH: compact cache for tab-size (i16 resolved px × 10)
2276        let mut fast_tab = None;
2277        if node_state.is_normal() {
2278            if let Some(ref cc) = cache.compact_cache {
2279                let raw = cc.get_tab_size_raw(dom_id.index());
2280                if raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD {
2281                    fast_tab = Some(raw as f32 / 10.0);
2282                }
2283            }
2284        }
2285        fast_tab.unwrap_or_else(|| {
2286            cache
2287                .get_tab_size(node_data, &dom_id, node_state)
2288                .and_then(|v| v.get_property().cloned())
2289                .map(|v| v.inner.number.get())
2290                .unwrap_or(8.0)
2291        })
2292    };
2293
2294    let properties = StyleProperties {
2295        font_stack,
2296        font_size_px: font_size,
2297        color,
2298        background_color,
2299        background_content,
2300        border,
2301        line_height,
2302        letter_spacing,
2303        word_spacing,
2304        text_decoration,
2305        tab_size,
2306        // These still use defaults - could be extended in future:
2307        // font_features, font_variations, text_transform, writing_mode, 
2308        // text_orientation, text_combine_upright, font_variant_*
2309        ..Default::default()
2310    };
2311
2312    properties
2313}
2314
2315pub fn get_list_style_type(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> StyleListStyleType {
2316    let Some(id) = dom_id else {
2317        return StyleListStyleType::default();
2318    };
2319    let node_data = &styled_dom.node_data.as_container()[id];
2320    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2321    styled_dom
2322        .css_property_cache
2323        .ptr
2324        .get_list_style_type(node_data, &id, node_state)
2325        .and_then(|v| v.get_property().copied())
2326        .unwrap_or_default()
2327}
2328
2329pub fn get_list_style_position(
2330    styled_dom: &StyledDom,
2331    dom_id: Option<NodeId>,
2332) -> StyleListStylePosition {
2333    let Some(id) = dom_id else {
2334        return StyleListStylePosition::default();
2335    };
2336    let node_data = &styled_dom.node_data.as_container()[id];
2337    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2338    styled_dom
2339        .css_property_cache
2340        .ptr
2341        .get_list_style_position(node_data, &id, node_state)
2342        .and_then(|v| v.get_property().copied())
2343        .unwrap_or_default()
2344}
2345
2346// New: Taffy Bridge Getters - Box Model Properties with Ua Css Fallback
2347
2348use azul_css::props::layout::{
2349    LayoutInsetBottom, LayoutLeft, LayoutMarginBottom, LayoutMarginLeft, LayoutMarginRight,
2350    LayoutMarginTop, LayoutMaxHeight, LayoutMaxWidth, LayoutMinHeight, LayoutMinWidth,
2351    LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutPaddingTop, LayoutRight,
2352    LayoutTop,
2353};
2354
2355/// Get inset (position) properties - returns MultiValue<PixelValue>
2356get_css_property_pixel!(
2357    get_css_left,
2358    get_left,
2359    azul_css::props::property::CssPropertyType::Left,
2360    compact_i16 = get_left
2361);
2362get_css_property_pixel!(
2363    get_css_right,
2364    get_right,
2365    azul_css::props::property::CssPropertyType::Right,
2366    compact_i16 = get_right
2367);
2368get_css_property_pixel!(
2369    get_css_top,
2370    get_top,
2371    azul_css::props::property::CssPropertyType::Top,
2372    compact_i16 = get_top
2373);
2374get_css_property_pixel!(
2375    get_css_bottom,
2376    get_bottom,
2377    azul_css::props::property::CssPropertyType::Bottom,
2378    compact_i16 = get_bottom
2379);
2380
2381/// Get margin properties - returns MultiValue<PixelValue>
2382get_css_property_pixel!(
2383    get_css_margin_left,
2384    get_margin_left,
2385    azul_css::props::property::CssPropertyType::MarginLeft,
2386    compact_i16 = get_margin_left_raw
2387);
2388get_css_property_pixel!(
2389    get_css_margin_right,
2390    get_margin_right,
2391    azul_css::props::property::CssPropertyType::MarginRight,
2392    compact_i16 = get_margin_right_raw
2393);
2394get_css_property_pixel!(
2395    get_css_margin_top,
2396    get_margin_top,
2397    azul_css::props::property::CssPropertyType::MarginTop,
2398    compact_i16 = get_margin_top_raw
2399);
2400get_css_property_pixel!(
2401    get_css_margin_bottom,
2402    get_margin_bottom,
2403    azul_css::props::property::CssPropertyType::MarginBottom,
2404    compact_i16 = get_margin_bottom_raw
2405);
2406
2407/// Get padding properties - returns MultiValue<PixelValue>
2408get_css_property_pixel!(
2409    get_css_padding_left,
2410    get_padding_left,
2411    azul_css::props::property::CssPropertyType::PaddingLeft,
2412    compact_i16 = get_padding_left_raw
2413);
2414get_css_property_pixel!(
2415    get_css_padding_right,
2416    get_padding_right,
2417    azul_css::props::property::CssPropertyType::PaddingRight,
2418    compact_i16 = get_padding_right_raw
2419);
2420get_css_property_pixel!(
2421    get_css_padding_top,
2422    get_padding_top,
2423    azul_css::props::property::CssPropertyType::PaddingTop,
2424    compact_i16 = get_padding_top_raw
2425);
2426get_css_property_pixel!(
2427    get_css_padding_bottom,
2428    get_padding_bottom,
2429    azul_css::props::property::CssPropertyType::PaddingBottom,
2430    compact_i16 = get_padding_bottom_raw
2431);
2432
2433/// Get min/max size properties
2434get_css_property!(
2435    get_css_min_width,
2436    get_min_width,
2437    LayoutMinWidth,
2438    azul_css::props::property::CssPropertyType::MinWidth,
2439    compact_u32_struct = get_min_width_raw
2440);
2441
2442get_css_property!(
2443    get_css_min_height,
2444    get_min_height,
2445    LayoutMinHeight,
2446    azul_css::props::property::CssPropertyType::MinHeight,
2447    compact_u32_struct = get_min_height_raw
2448);
2449
2450get_css_property!(
2451    get_css_max_width,
2452    get_max_width,
2453    LayoutMaxWidth,
2454    azul_css::props::property::CssPropertyType::MaxWidth,
2455    compact_u32_struct = get_max_width_raw
2456);
2457
2458get_css_property!(
2459    get_css_max_height,
2460    get_max_height,
2461    LayoutMaxHeight,
2462    azul_css::props::property::CssPropertyType::MaxHeight,
2463    compact_u32_struct = get_max_height_raw
2464);
2465
2466/// Get border width properties (no UA CSS fallback needed, defaults to 0)
2467get_css_property_pixel!(
2468    get_css_border_left_width,
2469    get_border_left_width,
2470    azul_css::props::property::CssPropertyType::BorderLeftWidth,
2471    compact_i16 = get_border_left_width_raw
2472);
2473get_css_property_pixel!(
2474    get_css_border_right_width,
2475    get_border_right_width,
2476    azul_css::props::property::CssPropertyType::BorderRightWidth,
2477    compact_i16 = get_border_right_width_raw
2478);
2479get_css_property_pixel!(
2480    get_css_border_top_width,
2481    get_border_top_width,
2482    azul_css::props::property::CssPropertyType::BorderTopWidth,
2483    compact_i16 = get_border_top_width_raw
2484);
2485get_css_property_pixel!(
2486    get_css_border_bottom_width,
2487    get_border_bottom_width,
2488    azul_css::props::property::CssPropertyType::BorderBottomWidth,
2489    compact_i16 = get_border_bottom_width_raw
2490);
2491
2492// Fragmentation (page breaking) properties
2493
2494/// Get break-before property for paged media
2495pub fn get_break_before(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> PageBreak {
2496    let Some(id) = dom_id else {
2497        return PageBreak::Auto;
2498    };
2499    let node_data = &styled_dom.node_data.as_container()[id];
2500    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2501    styled_dom
2502        .css_property_cache
2503        .ptr
2504        .get_break_before(node_data, &id, node_state)
2505        .and_then(|v| v.get_property().cloned())
2506        .unwrap_or(PageBreak::Auto)
2507}
2508
2509/// Get break-after property for paged media
2510pub fn get_break_after(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> PageBreak {
2511    let Some(id) = dom_id else {
2512        return PageBreak::Auto;
2513    };
2514    let node_data = &styled_dom.node_data.as_container()[id];
2515    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2516    styled_dom
2517        .css_property_cache
2518        .ptr
2519        .get_break_after(node_data, &id, node_state)
2520        .and_then(|v| v.get_property().cloned())
2521        .unwrap_or(PageBreak::Auto)
2522}
2523
2524/// Check if a PageBreak value forces a page break (always, page, left, right, etc.)
2525pub fn is_forced_page_break(page_break: PageBreak) -> bool {
2526    matches!(
2527        page_break,
2528        PageBreak::Always
2529            | PageBreak::Page
2530            | PageBreak::Left
2531            | PageBreak::Right
2532            | PageBreak::Recto
2533            | PageBreak::Verso
2534            | PageBreak::All
2535    )
2536}
2537
2538/// Get break-inside property for paged media
2539pub fn get_break_inside(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> BreakInside {
2540    let Some(id) = dom_id else {
2541        return BreakInside::Auto;
2542    };
2543    let node_data = &styled_dom.node_data.as_container()[id];
2544    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2545    styled_dom
2546        .css_property_cache
2547        .ptr
2548        .get_break_inside(node_data, &id, node_state)
2549        .and_then(|v| v.get_property().cloned())
2550        .unwrap_or(BreakInside::Auto)
2551}
2552
2553/// Get orphans property (minimum lines at bottom of page)
2554pub fn get_orphans(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> u32 {
2555    let Some(id) = dom_id else {
2556        return 2; // Default value
2557    };
2558    let node_data = &styled_dom.node_data.as_container()[id];
2559    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2560    styled_dom
2561        .css_property_cache
2562        .ptr
2563        .get_orphans(node_data, &id, node_state)
2564        .and_then(|v| v.get_property().cloned())
2565        .map(|o| o.inner)
2566        .unwrap_or(2)
2567}
2568
2569/// Get widows property (minimum lines at top of page)
2570pub fn get_widows(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> u32 {
2571    let Some(id) = dom_id else {
2572        return 2; // Default value
2573    };
2574    let node_data = &styled_dom.node_data.as_container()[id];
2575    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2576    styled_dom
2577        .css_property_cache
2578        .ptr
2579        .get_widows(node_data, &id, node_state)
2580        .and_then(|v| v.get_property().cloned())
2581        .map(|w| w.inner)
2582        .unwrap_or(2)
2583}
2584
2585/// Get box-decoration-break property
2586pub fn get_box_decoration_break(
2587    styled_dom: &StyledDom,
2588    dom_id: Option<NodeId>,
2589) -> BoxDecorationBreak {
2590    let Some(id) = dom_id else {
2591        return BoxDecorationBreak::Slice;
2592    };
2593    let node_data = &styled_dom.node_data.as_container()[id];
2594    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
2595    styled_dom
2596        .css_property_cache
2597        .ptr
2598        .get_box_decoration_break(node_data, &id, node_state)
2599        .and_then(|v| v.get_property().cloned())
2600        .unwrap_or(BoxDecorationBreak::Slice)
2601}
2602
2603// Helper functions for break properties
2604
2605/// Check if a PageBreak value is avoid
2606pub fn is_avoid_page_break(page_break: &PageBreak) -> bool {
2607    matches!(page_break, PageBreak::Avoid | PageBreak::AvoidPage)
2608}
2609
2610/// Check if a BreakInside value prevents breaks
2611pub fn is_avoid_break_inside(break_inside: &BreakInside) -> bool {
2612    matches!(
2613        break_inside,
2614        BreakInside::Avoid | BreakInside::AvoidPage | BreakInside::AvoidColumn
2615    )
2616}
2617
2618// Font Chain Resolution - Pre-Layout Font Loading
2619
2620use std::collections::HashMap;
2621
2622use rust_fontconfig::{FcFontCache, FcWeight, FontFallbackChain, PatternMatch};
2623
2624use crate::text3::cache::{FontChainKey, FontChainKeyOrRef, FontSelector, FontStack, FontStyle};
2625
2626/// Result of collecting font stacks from a StyledDom
2627/// Contains all unique font stacks and the mapping from StyleFontFamiliesHash to FontChainKey
2628#[derive(Debug, Clone)]
2629pub struct CollectedFontStacks {
2630    /// All unique font stacks found in the document (system/file fonts via fontconfig)
2631    pub font_stacks: Vec<Vec<FontSelector>>,
2632    /// Map from the font stack hash to the index in font_stacks
2633    pub hash_to_index: HashMap<u64, usize>,
2634    /// Direct FontRefs that bypass fontconfig (e.g., embedded icon fonts)
2635    /// These are keyed by their pointer address for uniqueness
2636    pub font_refs: HashMap<usize, azul_css::props::basic::font::FontRef>,
2637}
2638
2639/// Resolved font chains ready for use in layout
2640/// This is the result of resolving font stacks against FcFontCache
2641#[derive(Debug, Clone)]
2642pub struct ResolvedFontChains {
2643    /// Map from FontChainKeyOrRef to the resolved FontFallbackChain
2644    /// For FontChainKeyOrRef::Ref variants, the FontFallbackChain contains
2645    /// a single-font chain that covers the entire Unicode range.
2646    pub chains: HashMap<FontChainKeyOrRef, FontFallbackChain>,
2647}
2648
2649impl ResolvedFontChains {
2650    /// Get a font chain by its key
2651    pub fn get(&self, key: &FontChainKeyOrRef) -> Option<&FontFallbackChain> {
2652        self.chains.get(key)
2653    }
2654    
2655    /// Get a font chain by FontChainKey (for system fonts)
2656    pub fn get_by_chain_key(&self, key: &FontChainKey) -> Option<&FontFallbackChain> {
2657        self.chains.get(&FontChainKeyOrRef::Chain(key.clone()))
2658    }
2659
2660    /// Get a font chain for a font stack (via fontconfig)
2661    pub fn get_for_font_stack(&self, font_stack: &[FontSelector]) -> Option<&FontFallbackChain> {
2662        let key = FontChainKeyOrRef::Chain(FontChainKey::from_selectors(font_stack));
2663        self.chains.get(&key)
2664    }
2665    
2666    /// Get a font chain for a FontRef pointer
2667    pub fn get_for_font_ref(&self, ptr: usize) -> Option<&FontFallbackChain> {
2668        self.chains.get(&FontChainKeyOrRef::Ref(ptr))
2669    }
2670
2671    /// Consume self and return the inner HashMap with FontChainKeyOrRef keys
2672    ///
2673    /// This is useful when you need access to both Chain and Ref variants.
2674    pub fn into_inner(self) -> HashMap<FontChainKeyOrRef, FontFallbackChain> {
2675        self.chains
2676    }
2677
2678    /// Consume self and return only the fontconfig-resolved chains
2679    /// 
2680    /// This filters out FontRef entries and returns only the chains
2681    /// resolved via fontconfig. This is what FontManager expects.
2682    pub fn into_fontconfig_chains(self) -> HashMap<FontChainKey, FontFallbackChain> {
2683        self.chains
2684            .into_iter()
2685            .filter_map(|(key, chain)| {
2686                match key {
2687                    FontChainKeyOrRef::Chain(chain_key) => Some((chain_key, chain)),
2688                    FontChainKeyOrRef::Ref(_) => None,
2689                }
2690            })
2691            .collect()
2692    }
2693
2694    /// Get the number of resolved chains
2695    pub fn len(&self) -> usize {
2696        self.chains.len()
2697    }
2698
2699    /// Check if there are no resolved chains
2700    pub fn is_empty(&self) -> bool {
2701        self.chains.is_empty()
2702    }
2703    
2704    /// Get the number of direct FontRefs
2705    pub fn font_refs_len(&self) -> usize {
2706        self.chains.keys().filter(|k| k.is_ref()).count()
2707    }
2708}
2709
2710/// Collect all unique font stacks from a StyledDom
2711///
2712/// This is a pure function that iterates over all nodes in the DOM and
2713/// extracts the font-family property from each node that has text content.
2714///
2715/// # Arguments
2716/// * `styled_dom` - The styled DOM to extract font stacks from
2717/// * `platform` - The current platform for resolving system font types
2718///
2719/// # Returns
2720/// A `CollectedFontStacks` containing all unique font stacks and a hash-to-index mapping
2721pub fn collect_font_stacks_from_styled_dom(
2722    styled_dom: &StyledDom,
2723    platform: &azul_css::system::Platform,
2724) -> CollectedFontStacks {
2725    let mut font_stacks = Vec::new();
2726    let mut hash_to_index: HashMap<u64, usize> = HashMap::new();
2727    let mut seen_hashes = std::collections::HashSet::new();
2728    let mut font_refs: HashMap<usize, azul_css::props::basic::font::FontRef> = HashMap::new();
2729
2730    let node_data_container = styled_dom.node_data.as_container();
2731    let styled_nodes_container = styled_dom.styled_nodes.as_container();
2732    let cache = &styled_dom.css_property_cache.ptr;
2733
2734    // Iterate over all nodes
2735    for (node_idx, node_data) in node_data_container.internal.iter().enumerate() {
2736        // Only process text nodes (they are the ones that need fonts)
2737        if !matches!(node_data.node_type, NodeType::Text(_)) {
2738            continue;
2739        }
2740
2741        let dom_id = match NodeId::from_usize(node_idx) {
2742            Some(id) => id,
2743            None => continue,
2744        };
2745
2746        let node_state = &styled_nodes_container[dom_id].styled_node_state;
2747
2748        // Get font families from CSS
2749        let font_families = cache
2750            .get_font_family(node_data, &dom_id, node_state)
2751            .and_then(|v| v.get_property().cloned())
2752            .unwrap_or_else(|| {
2753                StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
2754            });
2755
2756        // Check if the first font family is a FontRef (direct embedded font)
2757        // If so, we don't need to go through fontconfig - just collect the FontRef
2758        if let Some(first_family) = font_families.get(0) {
2759            if let StyleFontFamily::Ref(font_ref) = first_family {
2760                let ptr = font_ref.parsed as usize;
2761                if !font_refs.contains_key(&ptr) {
2762                    font_refs.insert(ptr, font_ref.clone());
2763                }
2764                // Skip the normal font stack processing for FontRef
2765                continue;
2766            }
2767        }
2768
2769        // Get font weight and style
2770        let font_weight = match get_font_weight_property(styled_dom, dom_id, node_state) {
2771            MultiValue::Exact(v) => v,
2772            _ => StyleFontWeight::Normal,
2773        };
2774
2775        let font_style = match get_font_style_property(styled_dom, dom_id, node_state) {
2776            MultiValue::Exact(v) => v,
2777            _ => StyleFontStyle::Normal,
2778        };
2779
2780        // Convert to fontconfig types
2781        let mut fc_weight = super::fc::convert_font_weight(font_weight);
2782        let mut fc_style = super::fc::convert_font_style(font_style);
2783
2784        // Build font stack (only for non-Ref font families)
2785        let mut font_stack = Vec::with_capacity(font_families.len() + 3);
2786
2787        for i in 0..font_families.len() {
2788            let family = font_families.get(i).unwrap();
2789            // Skip FontRef entries in the stack - they're handled separately
2790            if matches!(family, StyleFontFamily::Ref(_)) {
2791                continue;
2792            }
2793            
2794            // Handle SystemFontType specially - resolve to actual font names
2795            // and apply the font weight/style from the system font type
2796            if let StyleFontFamily::SystemType(system_type) = family {
2797                // Get platform-specific font names using the provided platform
2798                let font_names = system_type.get_fallback_chain(platform);
2799                
2800                // Override weight/style based on system font type
2801                let system_weight = if system_type.is_bold() {
2802                    FcWeight::Bold
2803                } else {
2804                    fc_weight
2805                };
2806                let system_style = if system_type.is_italic() {
2807                    FontStyle::Italic
2808                } else {
2809                    fc_style
2810                };
2811                
2812                // Add each font name from the fallback chain
2813                for font_name in font_names {
2814                    font_stack.push(FontSelector {
2815                        family: font_name.to_string(),
2816                        weight: system_weight,
2817                        style: system_style,
2818                        unicode_ranges: Vec::new(),
2819                    });
2820                }
2821            } else {
2822                font_stack.push(FontSelector {
2823                    family: family.as_string(),
2824                    weight: fc_weight,
2825                    style: fc_style,
2826                    unicode_ranges: Vec::new(),
2827                });
2828            }
2829        }
2830
2831        // Add generic fallbacks
2832        let generic_fallbacks = ["sans-serif", "serif", "monospace"];
2833        for fallback in &generic_fallbacks {
2834            if !font_stack
2835                .iter()
2836                .any(|f| f.family.to_lowercase() == fallback.to_lowercase())
2837            {
2838                font_stack.push(FontSelector {
2839                    family: fallback.to_string(),
2840                    weight: FcWeight::Normal,
2841                    style: FontStyle::Normal,
2842                    unicode_ranges: Vec::new(),
2843                });
2844            }
2845        }
2846
2847        // Skip empty font stacks (can happen if all families were FontRefs)
2848        if font_stack.is_empty() {
2849            continue;
2850        }
2851
2852        // Compute hash for deduplication
2853        let key = FontChainKey::from_selectors(&font_stack);
2854        let hash = {
2855            use std::hash::{Hash, Hasher};
2856            let mut hasher = std::collections::hash_map::DefaultHasher::new();
2857            key.hash(&mut hasher);
2858            hasher.finish()
2859        };
2860
2861        // Only add if not seen before
2862        if !seen_hashes.contains(&hash) {
2863            seen_hashes.insert(hash);
2864            let idx = font_stacks.len();
2865            font_stacks.push(font_stack);
2866            hash_to_index.insert(hash, idx);
2867        }
2868    }
2869
2870    CollectedFontStacks {
2871        font_stacks,
2872        hash_to_index,
2873        font_refs,
2874    }
2875}
2876
2877/// Resolve all font chains for the collected font stacks
2878///
2879/// This is a pure function that takes the collected font stacks and resolves
2880/// them against the FcFontCache to produce FontFallbackChains.
2881///
2882/// # Arguments
2883/// * `collected` - The collected font stacks from `collect_font_stacks_from_styled_dom`
2884/// * `fc_cache` - The fontconfig cache to resolve fonts against
2885///
2886/// # Returns
2887/// A `ResolvedFontChains` containing all resolved font chains
2888pub fn resolve_font_chains(
2889    collected: &CollectedFontStacks,
2890    fc_cache: &FcFontCache,
2891) -> ResolvedFontChains {
2892    let mut chains = HashMap::new();
2893
2894    // Resolve system/file font stacks via fontconfig
2895    for font_stack in &collected.font_stacks {
2896        if font_stack.is_empty() {
2897            continue;
2898        }
2899
2900        // Build font families list
2901        let font_families: Vec<String> = font_stack
2902            .iter()
2903            .map(|s| s.family.clone())
2904            .filter(|f| !f.is_empty())
2905            .collect();
2906
2907        let font_families = if font_families.is_empty() {
2908            vec!["sans-serif".to_string()]
2909        } else {
2910            font_families
2911        };
2912
2913        let weight = font_stack[0].weight;
2914        let is_italic = font_stack[0].style == FontStyle::Italic;
2915        let is_oblique = font_stack[0].style == FontStyle::Oblique;
2916
2917        let cache_key = FontChainKeyOrRef::Chain(FontChainKey {
2918            font_families: font_families.clone(),
2919            weight,
2920            italic: is_italic,
2921            oblique: is_oblique,
2922        });
2923
2924        // Skip if already resolved
2925        if chains.contains_key(&cache_key) {
2926            continue;
2927        }
2928
2929        // Resolve the font chain
2930        // IMPORTANT: Use False (not DontCare) when style is Normal.
2931        // DontCare means "accept italic too" which can match italic fonts.
2932        // False means "must NOT be italic" which correctly prefers Normal.
2933        let italic = if is_italic {
2934            PatternMatch::True
2935        } else {
2936            PatternMatch::False
2937        };
2938        let oblique = if is_oblique {
2939            PatternMatch::True
2940        } else {
2941            PatternMatch::False
2942        };
2943
2944        let mut trace = Vec::new();
2945        let chain =
2946            fc_cache.resolve_font_chain(&font_families, weight, italic, oblique, &mut trace);
2947
2948        chains.insert(cache_key, chain);
2949    }
2950
2951    // Create single-font chains for direct FontRefs
2952    // These bypass fontconfig and cover the entire Unicode range
2953    // NOTE: FontRefs are handled differently - they don't go through fontconfig at all.
2954    // The shaping code checks style.font_stack for FontStack::Ref and uses the font directly.
2955    // We just need to record that we have these font refs for font loading purposes.
2956    for (ptr, _font_ref) in &collected.font_refs {
2957        let cache_key = FontChainKeyOrRef::Ref(*ptr);
2958        
2959        // For FontRef, we create an empty pattern that will be handled specially
2960        // during shaping. The font data is already available via the FontRef pointer.
2961        // We don't insert anything - the shaping code handles FontStack::Ref directly.
2962        let _ = cache_key; // Mark as used
2963    }
2964
2965    ResolvedFontChains { chains }
2966}
2967
2968/// Convenience function that collects and resolves font chains in one call
2969///
2970/// # Arguments
2971/// * `styled_dom` - The styled DOM to extract font stacks from
2972/// * `fc_cache` - The fontconfig cache to resolve fonts against
2973/// * `platform` - The current platform for resolving system font types
2974///
2975/// # Returns
2976/// A `ResolvedFontChains` containing all resolved font chains
2977pub fn collect_and_resolve_font_chains(
2978    styled_dom: &StyledDom,
2979    fc_cache: &FcFontCache,
2980    platform: &azul_css::system::Platform,
2981) -> ResolvedFontChains {
2982    let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
2983    resolve_font_chains(&collected, fc_cache)
2984}
2985
2986/// Register all embedded FontRefs from the styled DOM in the FontManager
2987/// 
2988/// This must be called BEFORE layout so that the fonts are available
2989/// for WebRender resource registration after layout.
2990pub fn register_embedded_fonts_from_styled_dom<T: crate::font_traits::ParsedFontTrait>(
2991    styled_dom: &StyledDom,
2992    font_manager: &crate::text3::cache::FontManager<T>,
2993    platform: &azul_css::system::Platform,
2994) {
2995    let collected = collect_font_stacks_from_styled_dom(styled_dom, platform);
2996    for (_ptr, font_ref) in &collected.font_refs {
2997        font_manager.register_embedded_font(font_ref);
2998    }
2999}
3000
3001// Font Loading Functions
3002
3003use std::collections::HashSet;
3004
3005use rust_fontconfig::FontId;
3006
3007/// Extract all unique FontIds from resolved font chains
3008///
3009/// This function collects all FontIds that are referenced in the font chains,
3010/// which represents the complete set of fonts that may be needed for rendering.
3011pub fn collect_font_ids_from_chains(chains: &ResolvedFontChains) -> HashSet<FontId> {
3012    let mut font_ids = HashSet::new();
3013
3014    for chain in chains.chains.values() {
3015        // Collect from CSS fallbacks
3016        for group in &chain.css_fallbacks {
3017            for font in &group.fonts {
3018                font_ids.insert(font.id);
3019            }
3020        }
3021
3022        // Collect from Unicode fallbacks
3023        for font in &chain.unicode_fallbacks {
3024            font_ids.insert(font.id);
3025        }
3026    }
3027
3028    font_ids
3029}
3030
3031/// Compute which fonts need to be loaded (diff with already loaded fonts)
3032///
3033/// # Arguments
3034/// * `required_fonts` - Set of FontIds that are needed
3035/// * `already_loaded` - Set of FontIds that are already loaded
3036///
3037/// # Returns
3038/// Set of FontIds that need to be loaded
3039pub fn compute_fonts_to_load(
3040    required_fonts: &HashSet<FontId>,
3041    already_loaded: &HashSet<FontId>,
3042) -> HashSet<FontId> {
3043    required_fonts.difference(already_loaded).cloned().collect()
3044}
3045
3046/// Result of loading fonts
3047#[derive(Debug)]
3048pub struct FontLoadResult<T> {
3049    /// Successfully loaded fonts
3050    pub loaded: HashMap<FontId, T>,
3051    /// FontIds that failed to load, with error messages
3052    pub failed: Vec<(FontId, String)>,
3053}
3054
3055/// Load fonts from disk using the provided loader function
3056///
3057/// This is a generic function that works with any font loading implementation.
3058/// The `load_fn` parameter should be a function that takes font bytes and an index,
3059/// and returns a parsed font or an error.
3060///
3061/// # Arguments
3062/// * `font_ids` - Set of FontIds to load
3063/// * `fc_cache` - The fontconfig cache to get font paths from
3064/// * `load_fn` - Function to load and parse font bytes
3065///
3066/// # Returns
3067/// A `FontLoadResult` containing successfully loaded fonts and any failures
3068pub fn load_fonts_from_disk<T, F>(
3069    font_ids: &HashSet<FontId>,
3070    fc_cache: &FcFontCache,
3071    load_fn: F,
3072) -> FontLoadResult<T>
3073where
3074    F: Fn(&[u8], usize) -> Result<T, crate::text3::cache::LayoutError>,
3075{
3076    let mut loaded = HashMap::new();
3077    let mut failed = Vec::new();
3078
3079    for font_id in font_ids {
3080        // Get font bytes from fc_cache
3081        let font_bytes = match fc_cache.get_font_bytes(font_id) {
3082            Some(bytes) => bytes,
3083            None => {
3084                failed.push((
3085                    *font_id,
3086                    format!("Could not get font bytes for {:?}", font_id),
3087                ));
3088                continue;
3089            }
3090        };
3091
3092        // Get font index (for font collections like .ttc files)
3093        let font_index = fc_cache
3094            .get_font_by_id(font_id)
3095            .and_then(|source| match source {
3096                rust_fontconfig::FontSource::Disk(path) => Some(path.font_index),
3097                rust_fontconfig::FontSource::Memory(font) => Some(font.font_index),
3098            })
3099            .unwrap_or(0) as usize;
3100
3101        // Load the font using the provided function
3102        match load_fn(&font_bytes, font_index) {
3103            Ok(font) => {
3104                loaded.insert(*font_id, font);
3105            }
3106            Err(e) => {
3107                failed.push((
3108                    *font_id,
3109                    format!("Failed to parse font {:?}: {:?}", font_id, e),
3110                ));
3111            }
3112        }
3113    }
3114
3115    FontLoadResult { loaded, failed }
3116}
3117
3118/// Convenience function to load all required fonts for a styled DOM
3119///
3120/// This function:
3121/// 1. Collects all font stacks from the DOM
3122/// 2. Resolves them to font chains
3123/// 3. Extracts all required FontIds
3124/// 4. Computes which fonts need to be loaded (diff with already loaded)
3125/// 5. Loads the missing fonts
3126///
3127/// # Arguments
3128/// * `styled_dom` - The styled DOM to extract font requirements from
3129/// * `fc_cache` - The fontconfig cache
3130/// * `already_loaded` - Set of FontIds that are already loaded
3131/// * `load_fn` - Function to load and parse font bytes
3132/// * `platform` - The current platform for resolving system font types
3133///
3134/// # Returns
3135/// A tuple of (ResolvedFontChains, FontLoadResult)
3136pub fn resolve_and_load_fonts<T, F>(
3137    styled_dom: &StyledDom,
3138    fc_cache: &FcFontCache,
3139    already_loaded: &HashSet<FontId>,
3140    load_fn: F,
3141    platform: &azul_css::system::Platform,
3142) -> (ResolvedFontChains, FontLoadResult<T>)
3143where
3144    F: Fn(&[u8], usize) -> Result<T, crate::text3::cache::LayoutError>,
3145{
3146    // Step 1-2: Collect and resolve font chains
3147    let chains = collect_and_resolve_font_chains(styled_dom, fc_cache, platform);
3148
3149    // Step 3: Extract all required FontIds
3150    let required_fonts = collect_font_ids_from_chains(&chains);
3151
3152    // Step 4: Compute diff
3153    let fonts_to_load = compute_fonts_to_load(&required_fonts, already_loaded);
3154
3155    // Step 5: Load missing fonts
3156    let load_result = load_fonts_from_disk(&fonts_to_load, fc_cache, load_fn);
3157
3158    (chains, load_result)
3159}
3160
3161// ============================================================================
3162// Scrollbar Style Getters
3163// ============================================================================
3164
3165use azul_css::props::style::scrollbar::{
3166    LayoutScrollbarWidth, ScrollbarColorCustom, ScrollbarInfo, StyleScrollbarColor,
3167    SCROLLBAR_CLASSIC_LIGHT,
3168};
3169
3170/// Computed scrollbar style for a node, combining CSS properties
3171#[derive(Debug, Clone)]
3172pub struct ComputedScrollbarStyle {
3173    /// The scrollbar width mode (auto/thin/none)
3174    pub width_mode: LayoutScrollbarWidth,
3175    /// Actual width in pixels (resolved from width_mode or scrollbar-style)
3176    pub width_px: f32,
3177    /// Thumb color
3178    pub thumb_color: ColorU,
3179    /// Track color
3180    pub track_color: ColorU,
3181    /// Button color (for scroll arrows)
3182    pub button_color: ColorU,
3183    /// Corner color (where scrollbars meet)
3184    pub corner_color: ColorU,
3185    /// Whether to clip the scrollbar to the container's border-radius
3186    pub clip_to_container_border: bool,
3187}
3188
3189impl Default for ComputedScrollbarStyle {
3190    fn default() -> Self {
3191        Self {
3192            width_mode: LayoutScrollbarWidth::Auto,
3193            width_px: 16.0, // Standard scrollbar width
3194            // Debug colors - bright magenta thumb, orange track
3195            thumb_color: ColorU::new(255, 0, 255, 255), // Magenta
3196            track_color: ColorU::new(255, 165, 0, 255), // Orange
3197            button_color: ColorU::new(0, 255, 0, 255),  // Green
3198            corner_color: ColorU::new(0, 0, 255, 255),  // Blue
3199            clip_to_container_border: false,
3200        }
3201    }
3202}
3203
3204/// Get the computed scrollbar style for a node
3205///
3206/// This combines:
3207/// - `scrollbar-width` property (auto/thin/none)
3208/// - `scrollbar-color` property (thumb and track colors)
3209/// - `-azul-scrollbar-style` property (full scrollbar customization)
3210pub fn get_scrollbar_style(
3211    styled_dom: &StyledDom,
3212    node_id: NodeId,
3213    node_state: &StyledNodeState,
3214) -> ComputedScrollbarStyle {
3215    let node_data = &styled_dom.node_data.as_container()[node_id];
3216
3217    // Start with defaults
3218    let mut result = ComputedScrollbarStyle::default();
3219
3220    // Check for -azul-scrollbar-style (full customization)
3221    if let Some(scrollbar_style) = styled_dom
3222        .css_property_cache
3223        .ptr
3224        .get_scrollbar_style(node_data, &node_id, node_state)
3225        .and_then(|v| v.get_property())
3226    {
3227        // Use the detailed scrollbar info
3228        result.width_px = match scrollbar_style.horizontal.width {
3229            azul_css::props::layout::dimensions::LayoutWidth::Px(px) => {
3230                // Use to_pixels_internal with 100% = 16px and 1em = 16px as reasonable defaults
3231                px.to_pixels_internal(16.0, 16.0)
3232            }
3233            _ => 16.0,
3234        };
3235        result.thumb_color = extract_color_from_background(&scrollbar_style.horizontal.thumb);
3236        result.track_color = extract_color_from_background(&scrollbar_style.horizontal.track);
3237        result.button_color = extract_color_from_background(&scrollbar_style.horizontal.button);
3238        result.corner_color = extract_color_from_background(&scrollbar_style.horizontal.corner);
3239        result.clip_to_container_border = scrollbar_style.horizontal.clip_to_container_border;
3240    }
3241
3242    // Check for scrollbar-width (overrides width)
3243    if let Some(scrollbar_width) = styled_dom
3244        .css_property_cache
3245        .ptr
3246        .get_scrollbar_width(node_data, &node_id, node_state)
3247        .and_then(|v| v.get_property())
3248    {
3249        result.width_mode = *scrollbar_width;
3250        result.width_px = match scrollbar_width {
3251            LayoutScrollbarWidth::Auto => 16.0,
3252            LayoutScrollbarWidth::Thin => 8.0,
3253            LayoutScrollbarWidth::None => 0.0,
3254        };
3255    }
3256
3257    // Check for scrollbar-color (overrides thumb/track colors)
3258    if let Some(scrollbar_color) = styled_dom
3259        .css_property_cache
3260        .ptr
3261        .get_scrollbar_color(node_data, &node_id, node_state)
3262        .and_then(|v| v.get_property())
3263    {
3264        match scrollbar_color {
3265            StyleScrollbarColor::Auto => {
3266                // Keep default colors
3267            }
3268            StyleScrollbarColor::Custom(custom) => {
3269                result.thumb_color = custom.thumb;
3270                result.track_color = custom.track;
3271            }
3272        }
3273    }
3274
3275    result
3276}
3277
3278/// Helper to extract a solid color from a StyleBackgroundContent
3279fn extract_color_from_background(
3280    bg: &azul_css::props::style::background::StyleBackgroundContent,
3281) -> ColorU {
3282    use azul_css::props::style::background::StyleBackgroundContent;
3283    match bg {
3284        StyleBackgroundContent::Color(c) => *c,
3285        _ => ColorU::TRANSPARENT,
3286    }
3287}
3288
3289/// Check if a node should clip its scrollbar to the container's border-radius
3290pub fn should_clip_scrollbar_to_border(
3291    styled_dom: &StyledDom,
3292    node_id: NodeId,
3293    node_state: &StyledNodeState,
3294) -> bool {
3295    let style = get_scrollbar_style(styled_dom, node_id, node_state);
3296    style.clip_to_container_border
3297}
3298
3299/// Get the scrollbar width in pixels for a node
3300pub fn get_scrollbar_width_px(
3301    styled_dom: &StyledDom,
3302    node_id: NodeId,
3303    node_state: &StyledNodeState,
3304) -> f32 {
3305    let style = get_scrollbar_style(styled_dom, node_id, node_state);
3306    style.width_px
3307}
3308
3309/// Checks if text in a node is selectable based on CSS `user-select` property.
3310///
3311/// Returns `true` if the text can be selected (default behavior),
3312/// `false` if `user-select: none` is set.
3313pub fn is_text_selectable(
3314    styled_dom: &StyledDom,
3315    node_id: NodeId,
3316    node_state: &StyledNodeState,
3317) -> bool {
3318    let node_data = &styled_dom.node_data.as_container()[node_id];
3319    
3320    styled_dom
3321        .css_property_cache
3322        .ptr
3323        .get_user_select(node_data, &node_id, node_state)
3324        .and_then(|v| v.get_property())
3325        .map(|us| *us != StyleUserSelect::None)
3326        .unwrap_or(true) // Default: text is selectable
3327}
3328
3329/// Checks if a node has the `contenteditable` attribute set directly.
3330///
3331/// Returns `true` if:
3332/// - The node has `contenteditable: true` set via `.set_contenteditable(true)`
3333/// - OR the node has `contenteditable` attribute set to `true`
3334///
3335/// This does NOT check inheritance - use `is_node_contenteditable_inherited` for that.
3336pub fn is_node_contenteditable(styled_dom: &StyledDom, node_id: NodeId) -> bool {
3337    use azul_core::dom::AttributeType;
3338    
3339    let node_data = &styled_dom.node_data.as_container()[node_id];
3340    
3341    // First check the direct contenteditable field (primary method)
3342    if node_data.is_contenteditable() {
3343        return true;
3344    }
3345    
3346    // Also check the attribute for backwards compatibility
3347    // Only return true if the attribute value is explicitly true
3348    node_data.attributes.as_ref().iter().any(|attr| {
3349        matches!(attr, AttributeType::ContentEditable(true))
3350    })
3351}
3352// =============================================================================
3353// Additional ExtractPropertyValue impls (not in compact cache tier 1/2)
3354// =============================================================================
3355
3356use azul_css::props::layout::text::LayoutTextJustify;
3357use azul_css::props::layout::table::{LayoutTableLayout, StyleBorderCollapse, StyleCaptionSide};
3358use azul_css::props::style::text::StyleHyphens;
3359use azul_css::props::style::effects::StyleCursor;
3360
3361impl ExtractPropertyValue<LayoutTextJustify> for CssProperty {
3362    fn extract(&self) -> Option<LayoutTextJustify> {
3363        match self {
3364            Self::TextJustify(CssPropertyValue::Exact(v)) => Some(*v),
3365            _ => None,
3366        }
3367    }
3368}
3369
3370impl ExtractPropertyValue<StyleHyphens> for CssProperty {
3371    fn extract(&self) -> Option<StyleHyphens> {
3372        match self {
3373            Self::Hyphens(CssPropertyValue::Exact(v)) => Some(*v),
3374            _ => None,
3375        }
3376    }
3377}
3378
3379impl ExtractPropertyValue<LayoutTableLayout> for CssProperty {
3380    fn extract(&self) -> Option<LayoutTableLayout> {
3381        match self {
3382            Self::TableLayout(CssPropertyValue::Exact(v)) => Some(*v),
3383            _ => None,
3384        }
3385    }
3386}
3387
3388impl ExtractPropertyValue<StyleBorderCollapse> for CssProperty {
3389    fn extract(&self) -> Option<StyleBorderCollapse> {
3390        match self {
3391            Self::BorderCollapse(CssPropertyValue::Exact(v)) => Some(*v),
3392            _ => None,
3393        }
3394    }
3395}
3396
3397impl ExtractPropertyValue<StyleCaptionSide> for CssProperty {
3398    fn extract(&self) -> Option<StyleCaptionSide> {
3399        match self {
3400            Self::CaptionSide(CssPropertyValue::Exact(v)) => Some(*v),
3401            _ => None,
3402        }
3403    }
3404}
3405
3406impl ExtractPropertyValue<StyleCursor> for CssProperty {
3407    fn extract(&self) -> Option<StyleCursor> {
3408        match self {
3409            Self::Cursor(CssPropertyValue::Exact(v)) => Some(v.clone()),
3410            _ => None,
3411        }
3412    }
3413}
3414
3415// =============================================================================
3416// Additional macro-based getters (not covered by compact cache fast-path getters)
3417// =============================================================================
3418
3419get_css_property!(
3420    get_text_justify,
3421    get_text_justify,
3422    LayoutTextJustify,
3423    CssPropertyType::TextJustify
3424);
3425
3426get_css_property!(
3427    get_hyphens,
3428    get_hyphens,
3429    StyleHyphens,
3430    CssPropertyType::Hyphens
3431);
3432
3433get_css_property!(
3434    get_table_layout,
3435    get_table_layout,
3436    LayoutTableLayout,
3437    CssPropertyType::TableLayout
3438);
3439
3440get_css_property!(
3441    get_border_collapse,
3442    get_border_collapse,
3443    StyleBorderCollapse,
3444    CssPropertyType::BorderCollapse,
3445    compact = get_border_collapse
3446);
3447
3448get_css_property!(
3449    get_caption_side,
3450    get_caption_side,
3451    StyleCaptionSide,
3452    CssPropertyType::CaptionSide
3453);
3454
3455get_css_property!(
3456    get_cursor_property,
3457    get_cursor,
3458    StyleCursor,
3459    CssPropertyType::Cursor
3460);
3461
3462// =============================================================================
3463// Handwritten getters (Option<T>, special logic, or non-standard returns)
3464// =============================================================================
3465
3466/// Get height property value for IFC text layout height reference.
3467pub fn get_height_value(
3468    styled_dom: &StyledDom,
3469    node_id: NodeId,
3470    node_state: &StyledNodeState,
3471) -> Option<LayoutHeight> {
3472    let node_data = &styled_dom.node_data.as_container()[node_id];
3473    styled_dom.css_property_cache.ptr
3474        .get_height(node_data, &node_id, node_state)
3475        .and_then(|v| v.get_property())
3476        .cloned()
3477}
3478
3479/// Get shape-inside property. Returns Option<ShapeInside> (cloned).
3480pub fn get_shape_inside(
3481    styled_dom: &StyledDom,
3482    node_id: NodeId,
3483    node_state: &StyledNodeState,
3484) -> Option<azul_css::props::layout::shape::ShapeInside> {
3485    let node_data = &styled_dom.node_data.as_container()[node_id];
3486    styled_dom.css_property_cache.ptr
3487        .get_shape_inside(node_data, &node_id, node_state)
3488        .and_then(|v| v.get_property())
3489        .cloned()
3490}
3491
3492/// Get shape-outside property. Returns Option<ShapeOutside> (cloned).
3493pub fn get_shape_outside(
3494    styled_dom: &StyledDom,
3495    node_id: NodeId,
3496    node_state: &StyledNodeState,
3497) -> Option<azul_css::props::layout::shape::ShapeOutside> {
3498    let node_data = &styled_dom.node_data.as_container()[node_id];
3499    styled_dom.css_property_cache.ptr
3500        .get_shape_outside(node_data, &node_id, node_state)
3501        .and_then(|v| v.get_property())
3502        .cloned()
3503}
3504
3505/// Get line-height as the full StyleLineHeight value for caller resolution.
3506pub fn get_line_height_value(
3507    styled_dom: &StyledDom,
3508    node_id: NodeId,
3509    node_state: &StyledNodeState,
3510) -> Option<azul_css::props::style::text::StyleLineHeight> {
3511    let node_data = &styled_dom.node_data.as_container()[node_id];
3512    styled_dom.css_property_cache.ptr
3513        .get_line_height(node_data, &node_id, node_state)
3514        .and_then(|v| v.get_property())
3515        .cloned()
3516}
3517
3518/// Get text-indent as the full StyleTextIndent value for caller resolution.
3519pub fn get_text_indent_value(
3520    styled_dom: &StyledDom,
3521    node_id: NodeId,
3522    node_state: &StyledNodeState,
3523) -> Option<azul_css::props::style::text::StyleTextIndent> {
3524    let node_data = &styled_dom.node_data.as_container()[node_id];
3525    styled_dom.css_property_cache.ptr
3526        .get_text_indent(node_data, &node_id, node_state)
3527        .and_then(|v| v.get_property())
3528        .cloned()
3529}
3530
3531/// Get column-count property. Returns Option<ColumnCount>.
3532pub fn get_column_count(
3533    styled_dom: &StyledDom,
3534    node_id: NodeId,
3535    node_state: &StyledNodeState,
3536) -> Option<azul_css::props::layout::column::ColumnCount> {
3537    let node_data = &styled_dom.node_data.as_container()[node_id];
3538    styled_dom.css_property_cache.ptr
3539        .get_column_count(node_data, &node_id, node_state)
3540        .and_then(|v| v.get_property())
3541        .cloned()
3542}
3543
3544/// Get column-gap as PixelValue. Returns Option.
3545pub fn get_column_gap_value(
3546    styled_dom: &StyledDom,
3547    node_id: NodeId,
3548    node_state: &StyledNodeState,
3549) -> Option<azul_css::props::layout::spacing::LayoutColumnGap> {
3550    let node_data = &styled_dom.node_data.as_container()[node_id];
3551    styled_dom.css_property_cache.ptr
3552        .get_column_gap(node_data, &node_id, node_state)
3553        .and_then(|v| v.get_property())
3554        .cloned()
3555}
3556
3557/// Get initial-letter property. Returns Option<StyleInitialLetter>.
3558pub fn get_initial_letter(
3559    styled_dom: &StyledDom,
3560    node_id: NodeId,
3561    node_state: &StyledNodeState,
3562) -> Option<azul_css::props::style::text::StyleInitialLetter> {
3563    let node_data = &styled_dom.node_data.as_container()[node_id];
3564    styled_dom.css_property_cache.ptr
3565        .get_initial_letter(node_data, &node_id, node_state)
3566        .and_then(|v| v.get_property())
3567        .cloned()
3568}
3569
3570/// Get line-clamp property. Returns Option<StyleLineClamp>.
3571pub fn get_line_clamp(
3572    styled_dom: &StyledDom,
3573    node_id: NodeId,
3574    node_state: &StyledNodeState,
3575) -> Option<azul_css::props::style::text::StyleLineClamp> {
3576    let node_data = &styled_dom.node_data.as_container()[node_id];
3577    styled_dom.css_property_cache.ptr
3578        .get_line_clamp(node_data, &node_id, node_state)
3579        .and_then(|v| v.get_property())
3580        .cloned()
3581}
3582
3583/// Get hanging-punctuation property. Returns Option<StyleHangingPunctuation>.
3584pub fn get_hanging_punctuation(
3585    styled_dom: &StyledDom,
3586    node_id: NodeId,
3587    node_state: &StyledNodeState,
3588) -> Option<azul_css::props::style::text::StyleHangingPunctuation> {
3589    let node_data = &styled_dom.node_data.as_container()[node_id];
3590    styled_dom.css_property_cache.ptr
3591        .get_hanging_punctuation(node_data, &node_id, node_state)
3592        .and_then(|v| v.get_property())
3593        .cloned()
3594}
3595
3596/// Get text-combine-upright property. Returns Option<StyleTextCombineUpright>.
3597pub fn get_text_combine_upright(
3598    styled_dom: &StyledDom,
3599    node_id: NodeId,
3600    node_state: &StyledNodeState,
3601) -> Option<azul_css::props::style::text::StyleTextCombineUpright> {
3602    let node_data = &styled_dom.node_data.as_container()[node_id];
3603    styled_dom.css_property_cache.ptr
3604        .get_text_combine_upright(node_data, &node_id, node_state)
3605        .and_then(|v| v.get_property())
3606        .cloned()
3607}
3608
3609/// Get exclusion-margin value. Returns f32 (default 0.0).
3610pub fn get_exclusion_margin(
3611    styled_dom: &StyledDom,
3612    node_id: NodeId,
3613    node_state: &StyledNodeState,
3614) -> f32 {
3615    let node_data = &styled_dom.node_data.as_container()[node_id];
3616    styled_dom.css_property_cache.ptr
3617        .get_exclusion_margin(node_data, &node_id, node_state)
3618        .and_then(|v| v.get_property())
3619        .map(|v| v.inner.get() as f32)
3620        .unwrap_or(0.0)
3621}
3622
3623/// Get hyphenation-language property. Returns Option<StyleHyphenationLanguage>.
3624pub fn get_hyphenation_language(
3625    styled_dom: &StyledDom,
3626    node_id: NodeId,
3627    node_state: &StyledNodeState,
3628) -> Option<azul_css::props::style::azul_exclusion::StyleHyphenationLanguage> {
3629    let node_data = &styled_dom.node_data.as_container()[node_id];
3630    styled_dom.css_property_cache.ptr
3631        .get_hyphenation_language(node_data, &node_id, node_state)
3632        .and_then(|v| v.get_property())
3633        .cloned()
3634}
3635
3636/// Get border-spacing property.
3637pub fn get_border_spacing(
3638    styled_dom: &StyledDom,
3639    node_id: NodeId,
3640    node_state: &StyledNodeState,
3641) -> azul_css::props::layout::table::LayoutBorderSpacing {
3642    use azul_css::props::basic::pixel::PixelValue;
3643
3644    // FAST PATH: compact cache for normal state
3645    if node_state.is_normal() {
3646        if let Some(ref cc) = styled_dom.css_property_cache.ptr.compact_cache {
3647            let h_raw = cc.get_border_spacing_h_raw(node_id.index());
3648            let v_raw = cc.get_border_spacing_v_raw(node_id.index());
3649            // Both 0 means no border-spacing set (default)
3650            // Sentinel means non-px unit → slow path
3651            if h_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
3652                && v_raw < azul_css::compact_cache::I16_SENTINEL_THRESHOLD
3653            {
3654                return azul_css::props::layout::table::LayoutBorderSpacing {
3655                    horizontal: PixelValue::px(h_raw as f32 / 10.0),
3656                    vertical: PixelValue::px(v_raw as f32 / 10.0),
3657                };
3658            }
3659        }
3660    }
3661
3662    // SLOW PATH
3663    let node_data = &styled_dom.node_data.as_container()[node_id];
3664    styled_dom.css_property_cache.ptr
3665        .get_border_spacing(node_data, &node_id, node_state)
3666        .and_then(|v| v.get_property())
3667        .cloned()
3668        .unwrap_or_default()
3669}
3670
3671/// Get opacity value. Returns f32 (default 1.0).
3672pub fn get_opacity(
3673    styled_dom: &StyledDom,
3674    node_id: NodeId,
3675    node_state: &StyledNodeState,
3676) -> f32 {
3677    let node_data = &styled_dom.node_data.as_container()[node_id];
3678    styled_dom.css_property_cache.ptr
3679        .get_opacity(node_data, &node_id, node_state)
3680        .and_then(|v| v.get_property())
3681        .map(|v| v.inner.normalized())
3682        .unwrap_or(1.0)
3683}
3684
3685/// Get filter property. Returns Option with cloned filter list.
3686pub fn get_filter(
3687    styled_dom: &StyledDom,
3688    node_id: NodeId,
3689    node_state: &StyledNodeState,
3690) -> Option<azul_css::props::style::filter::StyleFilterVec> {
3691    let node_data = &styled_dom.node_data.as_container()[node_id];
3692    styled_dom.css_property_cache.ptr
3693        .get_filter(node_data, &node_id, node_state)
3694        .and_then(|v| v.get_property())
3695        .cloned()
3696}
3697
3698/// Get backdrop-filter property. Returns Option with cloned filter list.
3699pub fn get_backdrop_filter(
3700    styled_dom: &StyledDom,
3701    node_id: NodeId,
3702    node_state: &StyledNodeState,
3703) -> Option<azul_css::props::style::filter::StyleFilterVec> {
3704    let node_data = &styled_dom.node_data.as_container()[node_id];
3705    styled_dom.css_property_cache.ptr
3706        .get_backdrop_filter(node_data, &node_id, node_state)
3707        .and_then(|v| v.get_property())
3708        .cloned()
3709}
3710
3711/// Get box-shadow for left side. Returns Option<StyleBoxShadow> (cloned).
3712pub fn get_box_shadow_left(
3713    styled_dom: &StyledDom,
3714    node_id: NodeId,
3715    node_state: &StyledNodeState,
3716) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
3717    let node_data = &styled_dom.node_data.as_container()[node_id];
3718    styled_dom.css_property_cache.ptr
3719        .get_box_shadow_left(node_data, &node_id, node_state)
3720        .and_then(|v| v.get_property())
3721        .cloned()
3722}
3723
3724/// Get box-shadow for right side. Returns Option<StyleBoxShadow> (cloned).
3725pub fn get_box_shadow_right(
3726    styled_dom: &StyledDom,
3727    node_id: NodeId,
3728    node_state: &StyledNodeState,
3729) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
3730    let node_data = &styled_dom.node_data.as_container()[node_id];
3731    styled_dom.css_property_cache.ptr
3732        .get_box_shadow_right(node_data, &node_id, node_state)
3733        .and_then(|v| v.get_property())
3734        .cloned()
3735}
3736
3737/// Get box-shadow for top side. Returns Option<StyleBoxShadow> (cloned).
3738pub fn get_box_shadow_top(
3739    styled_dom: &StyledDom,
3740    node_id: NodeId,
3741    node_state: &StyledNodeState,
3742) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
3743    let node_data = &styled_dom.node_data.as_container()[node_id];
3744    styled_dom.css_property_cache.ptr
3745        .get_box_shadow_top(node_data, &node_id, node_state)
3746        .and_then(|v| v.get_property())
3747        .cloned()
3748}
3749
3750/// Get box-shadow for bottom side. Returns Option<StyleBoxShadow> (cloned).
3751pub fn get_box_shadow_bottom(
3752    styled_dom: &StyledDom,
3753    node_id: NodeId,
3754    node_state: &StyledNodeState,
3755) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
3756    let node_data = &styled_dom.node_data.as_container()[node_id];
3757    styled_dom.css_property_cache.ptr
3758        .get_box_shadow_bottom(node_data, &node_id, node_state)
3759        .and_then(|v| v.get_property())
3760        .cloned()
3761}
3762
3763/// Get text-shadow property. Returns Option<StyleBoxShadow> (cloned).
3764pub fn get_text_shadow(
3765    styled_dom: &StyledDom,
3766    node_id: NodeId,
3767    node_state: &StyledNodeState,
3768) -> Option<azul_css::props::style::box_shadow::StyleBoxShadow> {
3769    let node_data = &styled_dom.node_data.as_container()[node_id];
3770    styled_dom.css_property_cache.ptr
3771        .get_text_shadow(node_data, &node_id, node_state)
3772        .and_then(|v| v.get_property())
3773        .cloned()
3774}
3775
3776/// Get transform property. Returns Option (non-empty transform list, cloned).
3777pub fn get_transform(
3778    styled_dom: &StyledDom,
3779    node_id: NodeId,
3780    node_state: &StyledNodeState,
3781) -> Option<azul_css::props::style::transform::StyleTransformVec> {
3782    let node_data = &styled_dom.node_data.as_container()[node_id];
3783    styled_dom.css_property_cache.ptr
3784        .get_transform(node_data, &node_id, node_state)
3785        .and_then(|v| v.get_property())
3786        .cloned()
3787}
3788
3789/// Get display property (raw). Returns Option<LayoutDisplay>.
3790pub fn get_display_raw(
3791    styled_dom: &StyledDom,
3792    node_id: NodeId,
3793    node_state: &StyledNodeState,
3794) -> Option<LayoutDisplay> {
3795    let node_data = &styled_dom.node_data.as_container()[node_id];
3796    styled_dom.css_property_cache.ptr
3797        .get_display(node_data, &node_id, node_state)
3798        .and_then(|v| v.get_property().copied())
3799}
3800
3801/// Get counter-reset property. Returns Option<CounterReset> (cloned).
3802pub fn get_counter_reset(
3803    styled_dom: &StyledDom,
3804    node_id: NodeId,
3805    node_state: &StyledNodeState,
3806) -> Option<azul_css::props::style::content::CounterReset> {
3807    let node_data = &styled_dom.node_data.as_container()[node_id];
3808    styled_dom.css_property_cache.ptr
3809        .get_counter_reset(node_data, &node_id, node_state)
3810        .and_then(|v| v.get_property())
3811        .cloned()
3812}
3813
3814/// Get counter-increment property. Returns Option<CounterIncrement> (cloned).
3815pub fn get_counter_increment(
3816    styled_dom: &StyledDom,
3817    node_id: NodeId,
3818    node_state: &StyledNodeState,
3819) -> Option<azul_css::props::style::content::CounterIncrement> {
3820    let node_data = &styled_dom.node_data.as_container()[node_id];
3821    styled_dom.css_property_cache.ptr
3822        .get_counter_increment(node_data, &node_id, node_state)
3823        .and_then(|v| v.get_property())
3824        .cloned()
3825}
3826
3827/// W3C-conformant contenteditable inheritance check.
3828///
3829/// In the W3C model, the `contenteditable` attribute is **inherited**:
3830/// - A node is editable if it has `contenteditable="true"` set directly
3831/// - OR if its parent has `isContentEditable` as true
3832/// - UNLESS the node explicitly sets `contenteditable="false"`
3833///
3834/// This function traverses up the DOM tree to determine editability.
3835///
3836/// # Returns
3837///
3838/// - `true` if the node is editable (either directly or via inheritance)
3839/// - `false` if the node is not editable or has `contenteditable="false"`
3840///
3841/// # Example
3842///
3843/// ```html
3844/// <div contenteditable="true">
3845///   A                              <!-- editable (inherited) -->
3846///   <div contenteditable="false">
3847///     B                            <!-- NOT editable (explicitly false) -->
3848///   </div>
3849///   C                              <!-- editable (inherited) -->
3850/// </div>
3851/// ```
3852pub fn is_node_contenteditable_inherited(styled_dom: &StyledDom, node_id: NodeId) -> bool {
3853    use azul_core::dom::AttributeType;
3854    
3855    let node_data_container = styled_dom.node_data.as_container();
3856    let hierarchy = styled_dom.node_hierarchy.as_container();
3857    
3858    let mut current_node_id = Some(node_id);
3859    
3860    while let Some(nid) = current_node_id {
3861        let node_data = &node_data_container[nid];
3862        
3863        // First check the direct contenteditable field (set via set_contenteditable())
3864        // This takes precedence as it's the API-level setting
3865        if node_data.is_contenteditable() {
3866            return true;
3867        }
3868        
3869        // Then check for explicit contenteditable attribute on this node
3870        // This handles HTML-style contenteditable="true" or contenteditable="false"
3871        for attr in node_data.attributes.as_ref().iter() {
3872            if let AttributeType::ContentEditable(is_editable) = attr {
3873                // If explicitly set to true, node is editable
3874                // If explicitly set to false, node is NOT editable (blocks inheritance)
3875                return *is_editable;
3876            }
3877        }
3878        
3879        // No explicit setting on this node, check parent for inheritance
3880        current_node_id = hierarchy.get(nid).and_then(|h| h.parent_id());
3881    }
3882    
3883    // Reached root without finding contenteditable - not editable
3884    false
3885}
3886
3887/// Find the contenteditable ancestor of a node.
3888///
3889/// When focus lands on a text node inside a contenteditable container,
3890/// we need to find the actual container that has the `contenteditable` attribute.
3891///
3892/// # Returns
3893///
3894/// - `Some(node_id)` of the contenteditable ancestor (may be the node itself)
3895/// - `None` if no contenteditable ancestor exists
3896pub fn find_contenteditable_ancestor(styled_dom: &StyledDom, node_id: NodeId) -> Option<NodeId> {
3897    use azul_core::dom::AttributeType;
3898    
3899    let node_data_container = styled_dom.node_data.as_container();
3900    let hierarchy = styled_dom.node_hierarchy.as_container();
3901    
3902    let mut current_node_id = Some(node_id);
3903    
3904    while let Some(nid) = current_node_id {
3905        let node_data = &node_data_container[nid];
3906        
3907        // First check the direct contenteditable field (set via set_contenteditable())
3908        if node_data.is_contenteditable() {
3909            return Some(nid);
3910        }
3911        
3912        // Then check for contenteditable attribute on this node
3913        for attr in node_data.attributes.as_ref().iter() {
3914            if let AttributeType::ContentEditable(is_editable) = attr {
3915                if *is_editable {
3916                    return Some(nid);
3917                } else {
3918                    // Explicitly not editable - stop search
3919                    return None;
3920                }
3921            }
3922        }
3923        
3924        // Check parent
3925        current_node_id = hierarchy.get(nid).and_then(|h| h.parent_id());
3926    }
3927    
3928    None
3929}
3930
3931// --- Taffy bridge property getters ---
3932//
3933// These getters return `Option<CssPropertyValue<T>>` (cloned from cache) for use
3934// by taffy_bridge.rs. The conversion from CssPropertyValue to taffy types is done
3935// in taffy_bridge.rs itself. Routing access through these functions centralizes
3936// all CSS property lookups for future cache optimizations (e.g., FxHash migration).
3937
3938macro_rules! get_css_property_value {
3939    ($fn_name:ident, $cache_method:ident, $ret_type:ty) => {
3940        pub fn $fn_name(
3941            styled_dom: &StyledDom,
3942            node_id: NodeId,
3943            node_state: &StyledNodeState,
3944        ) -> Option<$ret_type> {
3945            let node_data = &styled_dom.node_data.as_container()[node_id];
3946            styled_dom
3947                .css_property_cache
3948                .ptr
3949                .$cache_method(node_data, &node_id, node_state)
3950                .cloned()
3951        }
3952    };
3953}
3954
3955// Flexbox properties
3956get_css_property_value!(get_flex_direction_prop, get_flex_direction, LayoutFlexDirectionValue);
3957get_css_property_value!(get_flex_wrap_prop, get_flex_wrap, LayoutFlexWrapValue);
3958get_css_property_value!(get_flex_grow_prop, get_flex_grow, LayoutFlexGrowValue);
3959get_css_property_value!(get_flex_shrink_prop, get_flex_shrink, LayoutFlexShrinkValue);
3960get_css_property_value!(get_flex_basis_prop, get_flex_basis, LayoutFlexBasisValue);
3961
3962// Alignment properties
3963get_css_property_value!(get_align_items_prop, get_align_items, LayoutAlignItemsValue);
3964get_css_property_value!(get_align_self_prop, get_align_self, LayoutAlignSelfValue);
3965get_css_property_value!(get_align_content_prop, get_align_content, LayoutAlignContentValue);
3966get_css_property_value!(get_justify_content_prop, get_justify_content, LayoutJustifyContentValue);
3967get_css_property_value!(get_justify_items_prop, get_justify_items, LayoutJustifyItemsValue);
3968get_css_property_value!(get_justify_self_prop, get_justify_self, LayoutJustifySelfValue);
3969
3970// Gap
3971get_css_property_value!(get_gap_prop, get_gap, LayoutGapValue);
3972
3973// Grid properties
3974get_css_property_value!(get_grid_template_rows_prop, get_grid_template_rows, LayoutGridTemplateRowsValue);
3975get_css_property_value!(get_grid_template_columns_prop, get_grid_template_columns, LayoutGridTemplateColumnsValue);
3976get_css_property_value!(get_grid_auto_rows_prop, get_grid_auto_rows, LayoutGridAutoRowsValue);
3977get_css_property_value!(get_grid_auto_columns_prop, get_grid_auto_columns, LayoutGridAutoColumnsValue);
3978get_css_property_value!(get_grid_auto_flow_prop, get_grid_auto_flow, LayoutGridAutoFlowValue);
3979get_css_property_value!(get_grid_column_prop, get_grid_column, LayoutGridColumnValue);
3980get_css_property_value!(get_grid_row_prop, get_grid_row, LayoutGridRowValue);
3981
3982/// Get grid-template-areas property.
3983/// Uses the generic `get_property()` since CssPropertyCache lacks a specific getter.
3984/// Returns the inner `GridTemplateAreas` value (already unwrapped from CssPropertyValue).
3985pub fn get_grid_template_areas_prop(
3986    styled_dom: &StyledDom,
3987    node_id: NodeId,
3988    node_state: &StyledNodeState,
3989) -> Option<GridTemplateAreas> {
3990    let node_data = &styled_dom.node_data.as_container()[node_id];
3991    styled_dom
3992        .css_property_cache
3993        .ptr
3994        .get_property(node_data, &node_id, node_state, &CssPropertyType::GridTemplateAreas)
3995        .and_then(|p| {
3996            if let CssProperty::GridTemplateAreas(v) = p {
3997                v.get_property().cloned()
3998            } else {
3999                None
4000            }
4001        })
4002}