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},
17            pixel::{DEFAULT_FONT_SIZE, PT_TO_PX},
18            ColorU, PhysicalSize, PixelValue, PropertyContext, ResolutionContext,
19        },
20        layout::{
21            BoxDecorationBreak, BreakInside, LayoutBoxSizing, LayoutClear, LayoutDisplay,
22            LayoutFlexWrap, LayoutFloat, LayoutHeight, LayoutJustifyContent, LayoutOverflow,
23            LayoutPosition, LayoutWidth, LayoutWritingMode, Orphans, PageBreak, Widows,
24        },
25        property::{CssProperty, CssPropertyType},
26        style::{
27            border_radius::StyleBorderRadius,
28            lists::{StyleListStylePosition, StyleListStyleType},
29            StyleTextAlign, StyleUserSelect,
30        },
31    },
32};
33
34use crate::{
35    font_traits::{ParsedFontTrait, StyleProperties},
36    solver3::{
37        display_list::{BorderRadius, PhysicalSizeImport},
38        layout_tree::LayoutNode,
39        scrollbar::ScrollbarRequirements,
40    },
41};
42
43// Font-size resolution helper functions
44
45/// Helper function to get element's computed font-size
46pub fn get_element_font_size(
47    styled_dom: &StyledDom,
48    dom_id: NodeId,
49    node_state: &StyledNodeState,
50) -> f32 {
51    let node_data = &styled_dom.node_data.as_container()[dom_id];
52    let cache = &styled_dom.css_property_cache.ptr;
53
54    // Try to get from dependency chain first (proper resolution)
55    let cached_font_size = cache
56        .dependency_chains
57        .get(&dom_id)
58        .and_then(|chains| chains.get(&azul_css::props::property::CssPropertyType::FontSize))
59        .and_then(|chain| chain.cached_pixels);
60
61    if let Some(cached) = cached_font_size {
62        return cached;
63    }
64
65    // Fallback: get from property cache and resolve manually
66    let parent_font_size = styled_dom
67        .node_hierarchy
68        .as_container()
69        .get(dom_id)
70        .and_then(|node| node.parent_id())
71        .and_then(|parent_id| {
72            // Check parent's dependency chain first (avoids recursion)
73            cache
74                .dependency_chains
75                .get(&parent_id)
76                .and_then(|chains| {
77                    chains.get(&azul_css::props::property::CssPropertyType::FontSize)
78                })
79                .and_then(|chain| chain.cached_pixels)
80        })
81        .unwrap_or(DEFAULT_FONT_SIZE);
82
83    // Get root font-size (avoid recursion by checking cache first)
84    let root_font_size = {
85        let root_id = NodeId::new(0);
86        cache
87            .dependency_chains
88            .get(&root_id)
89            .and_then(|chains| chains.get(&azul_css::props::property::CssPropertyType::FontSize))
90            .and_then(|chain| chain.cached_pixels)
91            .unwrap_or(DEFAULT_FONT_SIZE)
92    };
93
94    // Resolve font-size with proper context
95    cache
96        .get_font_size(node_data, &dom_id, node_state)
97        .and_then(|v| v.get_property().cloned())
98        .map(|v| {
99            let context = ResolutionContext {
100                element_font_size: DEFAULT_FONT_SIZE, // Not used for FontSize property
101                parent_font_size,
102                root_font_size,
103                containing_block_size: PhysicalSize::new(0.0, 0.0),
104                element_size: None,
105                viewport_size: PhysicalSize::new(0.0, 0.0), // Not used for font-size resolution
106            };
107
108            v.inner
109                .resolve_with_context(&context, PropertyContext::FontSize)
110        })
111        .unwrap_or(DEFAULT_FONT_SIZE)
112}
113
114/// Helper function to get parent's computed font-size
115pub fn get_parent_font_size(
116    styled_dom: &StyledDom,
117    dom_id: NodeId,
118    node_state: &StyledNodeState,
119) -> f32 {
120    styled_dom
121        .node_hierarchy
122        .as_container()
123        .get(dom_id)
124        .and_then(|node| node.parent_id())
125        .map(|parent_id| get_element_font_size(styled_dom, parent_id, node_state))
126        .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE)
127}
128
129/// Helper function to get root element's font-size
130pub fn get_root_font_size(styled_dom: &StyledDom, node_state: &StyledNodeState) -> f32 {
131    // Root is always NodeId(0) in Azul
132    get_element_font_size(styled_dom, NodeId::new(0), node_state)
133}
134
135/// A value that can be Auto, Initial, Inherit, or an explicit value.
136/// This preserves CSS cascade semantics better than Option<T>.
137#[derive(Debug, Copy, Clone, PartialEq)]
138pub enum MultiValue<T> {
139    /// CSS 'auto' keyword
140    Auto,
141    /// CSS 'initial' keyword - use initial value
142    Initial,
143    /// CSS 'inherit' keyword - inherit from parent
144    Inherit,
145    /// Explicit value (e.g., "10px", "50%")
146    Exact(T),
147}
148
149impl<T> MultiValue<T> {
150    /// Returns true if this is an Auto value
151    pub fn is_auto(&self) -> bool {
152        matches!(self, MultiValue::Auto)
153    }
154
155    /// Returns true if this is an explicit value
156    pub fn is_exact(&self) -> bool {
157        matches!(self, MultiValue::Exact(_))
158    }
159
160    /// Gets the exact value if present
161    pub fn exact(self) -> Option<T> {
162        match self {
163            MultiValue::Exact(v) => Some(v),
164            _ => None,
165        }
166    }
167
168    /// Gets the exact value or returns the provided default
169    pub fn unwrap_or(self, default: T) -> T {
170        match self {
171            MultiValue::Exact(v) => v,
172            _ => default,
173        }
174    }
175
176    /// Gets the exact value or returns T::default()
177    pub fn unwrap_or_default(self) -> T
178    where
179        T: Default,
180    {
181        match self {
182            MultiValue::Exact(v) => v,
183            _ => T::default(),
184        }
185    }
186
187    /// Maps the inner value if Exact, otherwise returns self unchanged
188    pub fn map<U, F>(self, f: F) -> MultiValue<U>
189    where
190        F: FnOnce(T) -> U,
191    {
192        match self {
193            MultiValue::Exact(v) => MultiValue::Exact(f(v)),
194            MultiValue::Auto => MultiValue::Auto,
195            MultiValue::Initial => MultiValue::Initial,
196            MultiValue::Inherit => MultiValue::Inherit,
197        }
198    }
199}
200
201// Implement helper methods for LayoutOverflow specifically
202impl MultiValue<LayoutOverflow> {
203    /// Returns true if this overflow value causes content to be clipped.
204    /// This includes Hidden, Clip, Auto, and Scroll (all values except Visible).
205    pub fn is_clipped(&self) -> bool {
206        matches!(
207            self,
208            MultiValue::Exact(
209                LayoutOverflow::Hidden
210                    | LayoutOverflow::Clip
211                    | LayoutOverflow::Auto
212                    | LayoutOverflow::Scroll
213            )
214        )
215    }
216
217    pub fn is_scroll(&self) -> bool {
218        matches!(
219            self,
220            MultiValue::Exact(LayoutOverflow::Scroll | LayoutOverflow::Auto)
221        )
222    }
223
224    pub fn is_auto_overflow(&self) -> bool {
225        matches!(self, MultiValue::Exact(LayoutOverflow::Auto))
226    }
227
228    pub fn is_hidden(&self) -> bool {
229        matches!(self, MultiValue::Exact(LayoutOverflow::Hidden))
230    }
231
232    pub fn is_hidden_or_clip(&self) -> bool {
233        matches!(
234            self,
235            MultiValue::Exact(LayoutOverflow::Hidden | LayoutOverflow::Clip)
236        )
237    }
238
239    pub fn is_scroll_explicit(&self) -> bool {
240        matches!(self, MultiValue::Exact(LayoutOverflow::Scroll))
241    }
242
243    pub fn is_visible_or_clip(&self) -> bool {
244        matches!(
245            self,
246            MultiValue::Exact(LayoutOverflow::Visible | LayoutOverflow::Clip)
247        )
248    }
249}
250
251// Implement helper methods for LayoutPosition
252impl MultiValue<LayoutPosition> {
253    pub fn is_absolute_or_fixed(&self) -> bool {
254        matches!(
255            self,
256            MultiValue::Exact(LayoutPosition::Absolute | LayoutPosition::Fixed)
257        )
258    }
259}
260
261// Implement helper methods for LayoutFloat
262impl MultiValue<LayoutFloat> {
263    pub fn is_none(&self) -> bool {
264        matches!(
265            self,
266            MultiValue::Auto
267                | MultiValue::Initial
268                | MultiValue::Inherit
269                | MultiValue::Exact(LayoutFloat::None)
270        )
271    }
272}
273
274impl<T: Default> Default for MultiValue<T> {
275    fn default() -> Self {
276        MultiValue::Auto
277    }
278}
279
280/// Helper macro to reduce boilerplate for simple CSS property getters
281/// Returns the inner PixelValue wrapped in MultiValue
282macro_rules! get_css_property_pixel {
283    ($fn_name:ident, $cache_method:ident, $ua_property:expr) => {
284        pub fn $fn_name(
285            styled_dom: &StyledDom,
286            node_id: NodeId,
287            node_state: &StyledNodeState,
288        ) -> MultiValue<PixelValue> {
289            let node_data = &styled_dom.node_data.as_container()[node_id];
290
291            // 1. Check author CSS first
292            let author_css = styled_dom
293                .css_property_cache
294                .ptr
295                .$cache_method(node_data, &node_id, node_state);
296
297            if let Some(val) = author_css.and_then(|v| v.get_property().copied()) {
298                return MultiValue::Exact(val.inner);
299            }
300
301            // 2. Check User Agent CSS
302            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
303
304            if let Some(ua_prop) = ua_css {
305                if let Some(inner) = ua_prop.get_pixel_inner() {
306                    return MultiValue::Exact(inner);
307                }
308            }
309
310            // 3. Fallback to Auto (not set)
311            MultiValue::Auto
312        }
313    };
314}
315
316/// Helper trait to extract PixelValue from any CssProperty variant
317trait CssPropertyPixelInner {
318    fn get_pixel_inner(&self) -> Option<PixelValue>;
319}
320
321impl CssPropertyPixelInner for azul_css::props::property::CssProperty {
322    fn get_pixel_inner(&self) -> Option<PixelValue> {
323        match self {
324            CssProperty::Left(CssPropertyValue::Exact(v)) => Some(v.inner),
325            CssProperty::Right(CssPropertyValue::Exact(v)) => Some(v.inner),
326            CssProperty::Top(CssPropertyValue::Exact(v)) => Some(v.inner),
327            CssProperty::Bottom(CssPropertyValue::Exact(v)) => Some(v.inner),
328            CssProperty::MarginLeft(CssPropertyValue::Exact(v)) => Some(v.inner),
329            CssProperty::MarginRight(CssPropertyValue::Exact(v)) => Some(v.inner),
330            CssProperty::MarginTop(CssPropertyValue::Exact(v)) => Some(v.inner),
331            CssProperty::MarginBottom(CssPropertyValue::Exact(v)) => Some(v.inner),
332            CssProperty::PaddingLeft(CssPropertyValue::Exact(v)) => Some(v.inner),
333            CssProperty::PaddingRight(CssPropertyValue::Exact(v)) => Some(v.inner),
334            CssProperty::PaddingTop(CssPropertyValue::Exact(v)) => Some(v.inner),
335            CssProperty::PaddingBottom(CssPropertyValue::Exact(v)) => Some(v.inner),
336            _ => None,
337        }
338    }
339}
340
341/// Generic macro for CSS properties with UA CSS fallback - returns MultiValue<T>
342macro_rules! get_css_property {
343    ($fn_name:ident, $cache_method:ident, $return_type:ty, $ua_property:expr) => {
344        pub fn $fn_name(
345            styled_dom: &StyledDom,
346            node_id: NodeId,
347            node_state: &StyledNodeState,
348        ) -> MultiValue<$return_type> {
349            let node_data = &styled_dom.node_data.as_container()[node_id];
350
351            // 1. Check author CSS first
352            let author_css = styled_dom
353                .css_property_cache
354                .ptr
355                .$cache_method(node_data, &node_id, node_state);
356
357            if let Some(val) = author_css.and_then(|v| v.get_property().copied()) {
358                return MultiValue::Exact(val);
359            }
360
361            // 2. Check User Agent CSS
362            let ua_css = azul_core::ua_css::get_ua_property(&node_data.node_type, $ua_property);
363
364            if let Some(ua_prop) = ua_css {
365                if let Some(val) = extract_property_value::<$return_type>(ua_prop) {
366                    return MultiValue::Exact(val);
367                }
368            }
369
370            // 3. Fallback to Auto (not set)
371            MultiValue::Auto
372        }
373    };
374}
375
376/// Helper trait to extract typed values from UA CSS properties
377trait ExtractPropertyValue<T> {
378    fn extract(&self) -> Option<T>;
379}
380
381fn extract_property_value<T>(prop: &azul_css::props::property::CssProperty) -> Option<T>
382where
383    azul_css::props::property::CssProperty: ExtractPropertyValue<T>,
384{
385    prop.extract()
386}
387
388// Implement extraction for all layout types
389
390impl ExtractPropertyValue<LayoutWidth> for azul_css::props::property::CssProperty {
391    fn extract(&self) -> Option<LayoutWidth> {
392        match self {
393            Self::Width(CssPropertyValue::Exact(v)) => Some(*v),
394            _ => None,
395        }
396    }
397}
398
399impl ExtractPropertyValue<LayoutHeight> for azul_css::props::property::CssProperty {
400    fn extract(&self) -> Option<LayoutHeight> {
401        match self {
402            Self::Height(CssPropertyValue::Exact(v)) => Some(*v),
403            _ => None,
404        }
405    }
406}
407
408impl ExtractPropertyValue<LayoutMinWidth> for azul_css::props::property::CssProperty {
409    fn extract(&self) -> Option<LayoutMinWidth> {
410        match self {
411            Self::MinWidth(CssPropertyValue::Exact(v)) => Some(*v),
412            _ => None,
413        }
414    }
415}
416
417impl ExtractPropertyValue<LayoutMinHeight> for azul_css::props::property::CssProperty {
418    fn extract(&self) -> Option<LayoutMinHeight> {
419        match self {
420            Self::MinHeight(CssPropertyValue::Exact(v)) => Some(*v),
421            _ => None,
422        }
423    }
424}
425
426impl ExtractPropertyValue<LayoutMaxWidth> for azul_css::props::property::CssProperty {
427    fn extract(&self) -> Option<LayoutMaxWidth> {
428        match self {
429            Self::MaxWidth(CssPropertyValue::Exact(v)) => Some(*v),
430            _ => None,
431        }
432    }
433}
434
435impl ExtractPropertyValue<LayoutMaxHeight> for azul_css::props::property::CssProperty {
436    fn extract(&self) -> Option<LayoutMaxHeight> {
437        match self {
438            Self::MaxHeight(CssPropertyValue::Exact(v)) => Some(*v),
439            _ => None,
440        }
441    }
442}
443
444impl ExtractPropertyValue<LayoutDisplay> for azul_css::props::property::CssProperty {
445    fn extract(&self) -> Option<LayoutDisplay> {
446        match self {
447            Self::Display(CssPropertyValue::Exact(v)) => Some(*v),
448            _ => None,
449        }
450    }
451}
452
453impl ExtractPropertyValue<LayoutWritingMode> for azul_css::props::property::CssProperty {
454    fn extract(&self) -> Option<LayoutWritingMode> {
455        match self {
456            Self::WritingMode(CssPropertyValue::Exact(v)) => Some(*v),
457            _ => None,
458        }
459    }
460}
461
462impl ExtractPropertyValue<LayoutFlexWrap> for azul_css::props::property::CssProperty {
463    fn extract(&self) -> Option<LayoutFlexWrap> {
464        match self {
465            Self::FlexWrap(CssPropertyValue::Exact(v)) => Some(*v),
466            _ => None,
467        }
468    }
469}
470
471impl ExtractPropertyValue<LayoutJustifyContent> for azul_css::props::property::CssProperty {
472    fn extract(&self) -> Option<LayoutJustifyContent> {
473        match self {
474            Self::JustifyContent(CssPropertyValue::Exact(v)) => Some(*v),
475            _ => None,
476        }
477    }
478}
479
480impl ExtractPropertyValue<StyleTextAlign> for azul_css::props::property::CssProperty {
481    fn extract(&self) -> Option<StyleTextAlign> {
482        match self {
483            Self::TextAlign(CssPropertyValue::Exact(v)) => Some(*v),
484            _ => None,
485        }
486    }
487}
488
489impl ExtractPropertyValue<LayoutFloat> for azul_css::props::property::CssProperty {
490    fn extract(&self) -> Option<LayoutFloat> {
491        match self {
492            Self::Float(CssPropertyValue::Exact(v)) => Some(*v),
493            _ => None,
494        }
495    }
496}
497
498impl ExtractPropertyValue<LayoutClear> for azul_css::props::property::CssProperty {
499    fn extract(&self) -> Option<LayoutClear> {
500        match self {
501            Self::Clear(CssPropertyValue::Exact(v)) => Some(*v),
502            _ => None,
503        }
504    }
505}
506
507impl ExtractPropertyValue<LayoutOverflow> for azul_css::props::property::CssProperty {
508    fn extract(&self) -> Option<LayoutOverflow> {
509        match self {
510            Self::OverflowX(CssPropertyValue::Exact(v)) => Some(*v),
511            Self::OverflowY(CssPropertyValue::Exact(v)) => Some(*v),
512            _ => None,
513        }
514    }
515}
516
517impl ExtractPropertyValue<LayoutPosition> for azul_css::props::property::CssProperty {
518    fn extract(&self) -> Option<LayoutPosition> {
519        match self {
520            Self::Position(CssPropertyValue::Exact(v)) => Some(*v),
521            _ => None,
522        }
523    }
524}
525
526impl ExtractPropertyValue<LayoutBoxSizing> for azul_css::props::property::CssProperty {
527    fn extract(&self) -> Option<LayoutBoxSizing> {
528        match self {
529            Self::BoxSizing(CssPropertyValue::Exact(v)) => Some(*v),
530            _ => None,
531        }
532    }
533}
534
535impl ExtractPropertyValue<PixelValue> for azul_css::props::property::CssProperty {
536    fn extract(&self) -> Option<PixelValue> {
537        self.get_pixel_inner()
538    }
539}
540
541get_css_property!(
542    get_writing_mode,
543    get_writing_mode,
544    LayoutWritingMode,
545    azul_css::props::property::CssPropertyType::WritingMode
546);
547
548get_css_property!(
549    get_css_width,
550    get_width,
551    LayoutWidth,
552    azul_css::props::property::CssPropertyType::Width
553);
554
555get_css_property!(
556    get_css_height,
557    get_height,
558    LayoutHeight,
559    azul_css::props::property::CssPropertyType::Height
560);
561
562get_css_property!(
563    get_wrap,
564    get_flex_wrap,
565    LayoutFlexWrap,
566    azul_css::props::property::CssPropertyType::FlexWrap
567);
568
569get_css_property!(
570    get_justify_content,
571    get_justify_content,
572    LayoutJustifyContent,
573    azul_css::props::property::CssPropertyType::JustifyContent
574);
575
576get_css_property!(
577    get_text_align,
578    get_text_align,
579    StyleTextAlign,
580    azul_css::props::property::CssPropertyType::TextAlign
581);
582
583get_css_property!(
584    get_float,
585    get_float,
586    LayoutFloat,
587    azul_css::props::property::CssPropertyType::Float
588);
589
590get_css_property!(
591    get_clear,
592    get_clear,
593    LayoutClear,
594    azul_css::props::property::CssPropertyType::Clear
595);
596
597get_css_property!(
598    get_overflow_x,
599    get_overflow_x,
600    LayoutOverflow,
601    azul_css::props::property::CssPropertyType::OverflowX
602);
603
604get_css_property!(
605    get_overflow_y,
606    get_overflow_y,
607    LayoutOverflow,
608    azul_css::props::property::CssPropertyType::OverflowY
609);
610
611get_css_property!(
612    get_position,
613    get_position,
614    LayoutPosition,
615    azul_css::props::property::CssPropertyType::Position
616);
617
618get_css_property!(
619    get_css_box_sizing,
620    get_box_sizing,
621    LayoutBoxSizing,
622    azul_css::props::property::CssPropertyType::BoxSizing
623);
624// Complex Property Getters
625
626/// Get border radius for all four corners (raw CSS property values)
627pub fn get_style_border_radius(
628    styled_dom: &StyledDom,
629    node_id: NodeId,
630    node_state: &StyledNodeState,
631) -> azul_css::props::style::border_radius::StyleBorderRadius {
632    let node_data = &styled_dom.node_data.as_container()[node_id];
633
634    let top_left = styled_dom
635        .css_property_cache
636        .ptr
637        .get_border_top_left_radius(node_data, &node_id, node_state)
638        .and_then(|br| br.get_property_or_default())
639        .map(|v| v.inner)
640        .unwrap_or_default();
641
642    let top_right = styled_dom
643        .css_property_cache
644        .ptr
645        .get_border_top_right_radius(node_data, &node_id, node_state)
646        .and_then(|br| br.get_property_or_default())
647        .map(|v| v.inner)
648        .unwrap_or_default();
649
650    let bottom_right = styled_dom
651        .css_property_cache
652        .ptr
653        .get_border_bottom_right_radius(node_data, &node_id, node_state)
654        .and_then(|br| br.get_property_or_default())
655        .map(|v| v.inner)
656        .unwrap_or_default();
657
658    let bottom_left = styled_dom
659        .css_property_cache
660        .ptr
661        .get_border_bottom_left_radius(node_data, &node_id, node_state)
662        .and_then(|br| br.get_property_or_default())
663        .map(|v| v.inner)
664        .unwrap_or_default();
665
666    StyleBorderRadius {
667        top_left,
668        top_right,
669        bottom_right,
670        bottom_left,
671    }
672}
673
674/// Get border radius for all four corners (resolved to pixels)
675///
676/// # Arguments
677/// * `element_size` - The element's own size (width × height) for % resolution. According to CSS
678///   spec, border-radius % uses element's own dimensions.
679pub fn get_border_radius(
680    styled_dom: &StyledDom,
681    node_id: NodeId,
682    node_state: &StyledNodeState,
683    element_size: PhysicalSizeImport,
684    viewport_size: LogicalSize,
685) -> BorderRadius {
686    use azul_css::props::basic::{PhysicalSize, PropertyContext, ResolutionContext};
687
688    let node_data = &styled_dom.node_data.as_container()[node_id];
689
690    // Get font sizes for em/rem resolution
691    let element_font_size = get_element_font_size(styled_dom, node_id, node_state);
692    let parent_font_size = styled_dom
693        .node_hierarchy
694        .as_container()
695        .get(node_id)
696        .and_then(|node| node.parent_id())
697        .map(|p| get_element_font_size(styled_dom, p, node_state))
698        .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE);
699    let root_font_size = get_root_font_size(styled_dom, node_state);
700
701    // Create resolution context
702    let context = ResolutionContext {
703        element_font_size,
704        parent_font_size,
705        root_font_size,
706        containing_block_size: PhysicalSize::new(0.0, 0.0), // Not used for border-radius
707        element_size: Some(PhysicalSize::new(element_size.width, element_size.height)),
708        viewport_size: PhysicalSize::new(viewport_size.width, viewport_size.height),
709    };
710
711    let top_left = styled_dom
712        .css_property_cache
713        .ptr
714        .get_border_top_left_radius(node_data, &node_id, node_state)
715        .and_then(|br| br.get_property().cloned())
716        .unwrap_or_default();
717
718    let top_right = styled_dom
719        .css_property_cache
720        .ptr
721        .get_border_top_right_radius(node_data, &node_id, node_state)
722        .and_then(|br| br.get_property().cloned())
723        .unwrap_or_default();
724
725    let bottom_right = styled_dom
726        .css_property_cache
727        .ptr
728        .get_border_bottom_right_radius(node_data, &node_id, node_state)
729        .and_then(|br| br.get_property().cloned())
730        .unwrap_or_default();
731
732    let bottom_left = styled_dom
733        .css_property_cache
734        .ptr
735        .get_border_bottom_left_radius(node_data, &node_id, node_state)
736        .and_then(|br| br.get_property().cloned())
737        .unwrap_or_default();
738
739    BorderRadius {
740        top_left: top_left
741            .inner
742            .resolve_with_context(&context, PropertyContext::BorderRadius),
743        top_right: top_right
744            .inner
745            .resolve_with_context(&context, PropertyContext::BorderRadius),
746        bottom_right: bottom_right
747            .inner
748            .resolve_with_context(&context, PropertyContext::BorderRadius),
749        bottom_left: bottom_left
750            .inner
751            .resolve_with_context(&context, PropertyContext::BorderRadius),
752    }
753}
754
755/// Get z-index for stacking context ordering
756///
757/// NOTE: z-index CSS property exists but is not yet hooked up to the CSS cache API.
758/// This would require adding get_z_index() to CssPropertyCache.
759pub fn get_z_index(styled_dom: &StyledDom, node_id: Option<NodeId>) -> i32 {
760    // TODO: Add get_z_index() method to CSS cache, then query it here
761    let _ = (styled_dom, node_id);
762    0
763}
764
765// Rendering Property Getters
766
767/// Information about background color for a node
768///
769/// # CSS Background Propagation (Special Case for HTML Root)
770///
771/// According to CSS Backgrounds and Borders Module Level 3, Section "The Canvas Background
772/// and the HTML `<body>` Element":
773///
774/// For HTML documents where the root element is `<html>`, if the computed value of
775/// `background-image` on the root element is `none` AND its `background-color` is `transparent`,
776/// user agents **must propagate** the computed values of the background properties from the
777/// first `<body>` child element to the root element.
778///
779/// This behavior exists for backwards compatibility with older HTML where backgrounds were
780/// typically set on `<body>` using `bgcolor` attributes, and ensures that the `<body>`
781/// background covers the entire viewport/canvas even when `<body>` itself has constrained
782/// dimensions.
783///
784/// Implementation: When requesting the background of an `<html>` node, we first check if it
785/// has a transparent background with no image. If so, we look for a `<body>` child and use
786/// its background instead.
787pub fn get_background_color(
788    styled_dom: &StyledDom,
789    node_id: NodeId,
790    node_state: &StyledNodeState,
791) -> ColorU {
792    let node_data = &styled_dom.node_data.as_container()[node_id];
793
794    // Fast path: Get this node's background
795    let get_node_bg = |node_id: NodeId, node_data: &azul_core::dom::NodeData| {
796        styled_dom
797            .css_property_cache
798            .ptr
799            .get_background_content(node_data, &node_id, node_state)
800            .and_then(|bg| bg.get_property())
801            .and_then(|bg_vec| bg_vec.get(0))
802            .and_then(|first_bg| match first_bg {
803                azul_css::props::style::StyleBackgroundContent::Color(color) => Some(color.clone()),
804                azul_css::props::style::StyleBackgroundContent::Image(_) => None, // Has image, not transparent
805                _ => None,
806            })
807    };
808
809    let own_bg = get_node_bg(node_id, node_data);
810
811    // CSS Background Propagation: Special handling for <html> root element
812    // Only check propagation if this is an Html node AND has transparent background (no
813    // color/image)
814    if !matches!(node_data.node_type, NodeType::Html) || own_bg.is_some() {
815        // Not Html or has its own background - return own background or transparent
816        return own_bg.unwrap_or(ColorU {
817            r: 0,
818            g: 0,
819            b: 0,
820            a: 0,
821        });
822    }
823
824    // Html node with transparent background - check if we should propagate from <body>
825    let first_child = styled_dom
826        .node_hierarchy
827        .as_container()
828        .get(node_id)
829        .and_then(|node| node.first_child_id(node_id));
830
831    let Some(first_child) = first_child else {
832        return ColorU {
833            r: 0,
834            g: 0,
835            b: 0,
836            a: 0,
837        };
838    };
839
840    let first_child_data = &styled_dom.node_data.as_container()[first_child];
841
842    // Check if first child is <body>
843    if !matches!(first_child_data.node_type, NodeType::Body) {
844        return ColorU {
845            r: 0,
846            g: 0,
847            b: 0,
848            a: 0,
849        };
850    }
851
852    // Propagate <body>'s background to <html> (canvas)
853    get_node_bg(first_child, first_child_data).unwrap_or(ColorU {
854        r: 0,
855        g: 0,
856        b: 0,
857        a: 0,
858    })
859}
860
861/// Returns all background content layers for a node (colors, gradients, images).
862/// This is used for rendering backgrounds that may include linear/radial/conic gradients.
863///
864/// CSS Background Propagation (CSS Backgrounds 3, Section 2.11.2):
865/// For HTML documents, if the root `<html>` element has no background (transparent with no image),
866/// propagate the background from the first `<body>` child element.
867pub fn get_background_contents(
868    styled_dom: &StyledDom,
869    node_id: NodeId,
870    node_state: &StyledNodeState,
871) -> Vec<azul_css::props::style::StyleBackgroundContent> {
872    use azul_core::dom::NodeType;
873    use azul_css::props::style::StyleBackgroundContent;
874
875    let node_data = &styled_dom.node_data.as_container()[node_id];
876
877    // Helper to get backgrounds for a node
878    let get_node_backgrounds =
879        |nid: NodeId, ndata: &azul_core::dom::NodeData| -> Vec<StyleBackgroundContent> {
880            styled_dom
881                .css_property_cache
882                .ptr
883                .get_background_content(ndata, &nid, node_state)
884                .and_then(|bg| bg.get_property())
885                .map(|bg_vec| bg_vec.iter().cloned().collect())
886                .unwrap_or_default()
887        };
888
889    let own_backgrounds = get_node_backgrounds(node_id, node_data);
890
891    // CSS Background Propagation: Special handling for <html> root element
892    // Only check propagation if this is an Html node AND has no backgrounds
893    if !matches!(node_data.node_type, NodeType::Html) || !own_backgrounds.is_empty() {
894        return own_backgrounds;
895    }
896
897    // Html node with no backgrounds - check if we should propagate from <body>
898    let first_child = styled_dom
899        .node_hierarchy
900        .as_container()
901        .get(node_id)
902        .and_then(|node| node.first_child_id(node_id));
903
904    let Some(first_child) = first_child else {
905        return own_backgrounds;
906    };
907
908    let first_child_data = &styled_dom.node_data.as_container()[first_child];
909
910    // Check if first child is <body>
911    if !matches!(first_child_data.node_type, NodeType::Body) {
912        return own_backgrounds;
913    }
914
915    // Propagate <body>'s backgrounds to <html> (canvas)
916    get_node_backgrounds(first_child, first_child_data)
917}
918
919/// Information about border rendering
920pub struct BorderInfo {
921    pub widths: crate::solver3::display_list::StyleBorderWidths,
922    pub colors: crate::solver3::display_list::StyleBorderColors,
923    pub styles: crate::solver3::display_list::StyleBorderStyles,
924}
925
926pub fn get_border_info(
927    styled_dom: &StyledDom,
928    node_id: NodeId,
929    node_state: &StyledNodeState,
930) -> BorderInfo {
931    use crate::solver3::display_list::{StyleBorderColors, StyleBorderStyles, StyleBorderWidths};
932
933    let node_data = &styled_dom.node_data.as_container()[node_id];
934
935    // Get all border widths
936    let widths = StyleBorderWidths {
937        top: styled_dom
938            .css_property_cache
939            .ptr
940            .get_border_top_width(node_data, &node_id, node_state)
941            .cloned(),
942        right: styled_dom
943            .css_property_cache
944            .ptr
945            .get_border_right_width(node_data, &node_id, node_state)
946            .cloned(),
947        bottom: styled_dom
948            .css_property_cache
949            .ptr
950            .get_border_bottom_width(node_data, &node_id, node_state)
951            .cloned(),
952        left: styled_dom
953            .css_property_cache
954            .ptr
955            .get_border_left_width(node_data, &node_id, node_state)
956            .cloned(),
957    };
958
959    // Get all border colors
960    let colors = StyleBorderColors {
961        top: styled_dom
962            .css_property_cache
963            .ptr
964            .get_border_top_color(node_data, &node_id, node_state)
965            .cloned(),
966        right: styled_dom
967            .css_property_cache
968            .ptr
969            .get_border_right_color(node_data, &node_id, node_state)
970            .cloned(),
971        bottom: styled_dom
972            .css_property_cache
973            .ptr
974            .get_border_bottom_color(node_data, &node_id, node_state)
975            .cloned(),
976        left: styled_dom
977            .css_property_cache
978            .ptr
979            .get_border_left_color(node_data, &node_id, node_state)
980            .cloned(),
981    };
982
983    // Get all border styles
984    let styles = StyleBorderStyles {
985        top: styled_dom
986            .css_property_cache
987            .ptr
988            .get_border_top_style(node_data, &node_id, node_state)
989            .cloned(),
990        right: styled_dom
991            .css_property_cache
992            .ptr
993            .get_border_right_style(node_data, &node_id, node_state)
994            .cloned(),
995        bottom: styled_dom
996            .css_property_cache
997            .ptr
998            .get_border_bottom_style(node_data, &node_id, node_state)
999            .cloned(),
1000        left: styled_dom
1001            .css_property_cache
1002            .ptr
1003            .get_border_left_style(node_data, &node_id, node_state)
1004            .cloned(),
1005    };
1006
1007    BorderInfo {
1008        widths,
1009        colors,
1010        styles,
1011    }
1012}
1013
1014/// Convert BorderInfo to InlineBorderInfo for inline elements
1015///
1016/// This resolves the CSS property values to concrete pixel values and colors
1017/// that can be used during text rendering.
1018pub fn get_inline_border_info(
1019    styled_dom: &StyledDom,
1020    node_id: NodeId,
1021    node_state: &StyledNodeState,
1022    border_info: &BorderInfo,
1023) -> Option<crate::text3::cache::InlineBorderInfo> {
1024    use crate::text3::cache::InlineBorderInfo;
1025
1026    // Helper to extract pixel value from border width
1027    fn get_border_width_px(
1028        width: &Option<
1029            azul_css::css::CssPropertyValue<azul_css::props::style::border::LayoutBorderTopWidth>,
1030        >,
1031    ) -> f32 {
1032        width
1033            .as_ref()
1034            .and_then(|v| v.get_property())
1035            .map(|w| w.inner.number.get())
1036            .unwrap_or(0.0)
1037    }
1038
1039    fn get_border_width_px_right(
1040        width: &Option<
1041            azul_css::css::CssPropertyValue<azul_css::props::style::border::LayoutBorderRightWidth>,
1042        >,
1043    ) -> f32 {
1044        width
1045            .as_ref()
1046            .and_then(|v| v.get_property())
1047            .map(|w| w.inner.number.get())
1048            .unwrap_or(0.0)
1049    }
1050
1051    fn get_border_width_px_bottom(
1052        width: &Option<
1053            azul_css::css::CssPropertyValue<
1054                azul_css::props::style::border::LayoutBorderBottomWidth,
1055            >,
1056        >,
1057    ) -> f32 {
1058        width
1059            .as_ref()
1060            .and_then(|v| v.get_property())
1061            .map(|w| w.inner.number.get())
1062            .unwrap_or(0.0)
1063    }
1064
1065    fn get_border_width_px_left(
1066        width: &Option<
1067            azul_css::css::CssPropertyValue<azul_css::props::style::border::LayoutBorderLeftWidth>,
1068        >,
1069    ) -> f32 {
1070        width
1071            .as_ref()
1072            .and_then(|v| v.get_property())
1073            .map(|w| w.inner.number.get())
1074            .unwrap_or(0.0)
1075    }
1076
1077    // Helper to extract color from border color
1078    fn get_border_color_top(
1079        color: &Option<
1080            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderTopColor>,
1081        >,
1082    ) -> ColorU {
1083        color
1084            .as_ref()
1085            .and_then(|v| v.get_property())
1086            .map(|c| c.inner)
1087            .unwrap_or(ColorU::BLACK)
1088    }
1089
1090    fn get_border_color_right(
1091        color: &Option<
1092            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderRightColor>,
1093        >,
1094    ) -> ColorU {
1095        color
1096            .as_ref()
1097            .and_then(|v| v.get_property())
1098            .map(|c| c.inner)
1099            .unwrap_or(ColorU::BLACK)
1100    }
1101
1102    fn get_border_color_bottom(
1103        color: &Option<
1104            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderBottomColor>,
1105        >,
1106    ) -> ColorU {
1107        color
1108            .as_ref()
1109            .and_then(|v| v.get_property())
1110            .map(|c| c.inner)
1111            .unwrap_or(ColorU::BLACK)
1112    }
1113
1114    fn get_border_color_left(
1115        color: &Option<
1116            azul_css::css::CssPropertyValue<azul_css::props::style::border::StyleBorderLeftColor>,
1117        >,
1118    ) -> ColorU {
1119        color
1120            .as_ref()
1121            .and_then(|v| v.get_property())
1122            .map(|c| c.inner)
1123            .unwrap_or(ColorU::BLACK)
1124    }
1125
1126    // Extract border-radius (simplified - uses the average of all corners if uniform)
1127    fn get_border_radius_px(
1128        styled_dom: &StyledDom,
1129        node_id: NodeId,
1130        node_state: &StyledNodeState,
1131    ) -> Option<f32> {
1132        let node_data = &styled_dom.node_data.as_container()[node_id];
1133
1134        let top_left = styled_dom
1135            .css_property_cache
1136            .ptr
1137            .get_border_top_left_radius(node_data, &node_id, node_state)
1138            .and_then(|br| br.get_property().cloned())
1139            .map(|v| v.inner.number.get());
1140
1141        let top_right = styled_dom
1142            .css_property_cache
1143            .ptr
1144            .get_border_top_right_radius(node_data, &node_id, node_state)
1145            .and_then(|br| br.get_property().cloned())
1146            .map(|v| v.inner.number.get());
1147
1148        let bottom_left = styled_dom
1149            .css_property_cache
1150            .ptr
1151            .get_border_bottom_left_radius(node_data, &node_id, node_state)
1152            .and_then(|br| br.get_property().cloned())
1153            .map(|v| v.inner.number.get());
1154
1155        let bottom_right = styled_dom
1156            .css_property_cache
1157            .ptr
1158            .get_border_bottom_right_radius(node_data, &node_id, node_state)
1159            .and_then(|br| br.get_property().cloned())
1160            .map(|v| v.inner.number.get());
1161
1162        // If any radius is defined, use the maximum (for inline, uniform radius is most common)
1163        let radii: Vec<f32> = [top_left, top_right, bottom_left, bottom_right]
1164            .into_iter()
1165            .filter_map(|r| r)
1166            .collect();
1167
1168        if radii.is_empty() {
1169            None
1170        } else {
1171            Some(radii.into_iter().fold(0.0f32, |a, b| a.max(b)))
1172        }
1173    }
1174
1175    let top = get_border_width_px(&border_info.widths.top);
1176    let right = get_border_width_px_right(&border_info.widths.right);
1177    let bottom = get_border_width_px_bottom(&border_info.widths.bottom);
1178    let left = get_border_width_px_left(&border_info.widths.left);
1179
1180    // Only return Some if there's actually a border
1181    if top == 0.0 && right == 0.0 && bottom == 0.0 && left == 0.0 {
1182        return None;
1183    }
1184
1185    Some(InlineBorderInfo {
1186        top,
1187        right,
1188        bottom,
1189        left,
1190        top_color: get_border_color_top(&border_info.colors.top),
1191        right_color: get_border_color_right(&border_info.colors.right),
1192        bottom_color: get_border_color_bottom(&border_info.colors.bottom),
1193        left_color: get_border_color_left(&border_info.colors.left),
1194        radius: get_border_radius_px(styled_dom, node_id, node_state),
1195    })
1196}
1197
1198// Selection and Caret Styling
1199
1200/// Style information for text selection rendering
1201#[derive(Debug, Clone, Copy, Default)]
1202pub struct SelectionStyle {
1203    /// Background color of the selection highlight
1204    pub bg_color: ColorU,
1205    /// Text color when selected (overrides normal text color)
1206    pub text_color: Option<ColorU>,
1207    /// Border radius for selection rectangles
1208    pub radius: f32,
1209}
1210
1211/// Get selection style for a node
1212pub fn get_selection_style(styled_dom: &StyledDom, node_id: Option<NodeId>) -> SelectionStyle {
1213    let Some(node_id) = node_id else {
1214        return SelectionStyle::default();
1215    };
1216
1217    let node_data = &styled_dom.node_data.as_container()[node_id];
1218    let node_state = &StyledNodeState::default();
1219
1220    let bg_color = styled_dom
1221        .css_property_cache
1222        .ptr
1223        .get_selection_background_color(node_data, &node_id, node_state)
1224        .and_then(|c| c.get_property().cloned())
1225        .map(|c| c.inner)
1226        .unwrap_or(ColorU {
1227            r: 51,
1228            g: 153,
1229            b: 255, // Standard blue selection color
1230            a: 128, // Semi-transparent
1231        });
1232
1233    let text_color = styled_dom
1234        .css_property_cache
1235        .ptr
1236        .get_selection_color(node_data, &node_id, node_state)
1237        .and_then(|c| c.get_property().cloned())
1238        .map(|c| c.inner);
1239
1240    let radius = styled_dom
1241        .css_property_cache
1242        .ptr
1243        .get_selection_radius(node_data, &node_id, node_state)
1244        .and_then(|r| r.get_property().cloned())
1245        .map(|r| r.inner.to_pixels_internal(0.0, 16.0)) // percent=0, em=16px default font size
1246        .unwrap_or(0.0);
1247
1248    SelectionStyle {
1249        bg_color,
1250        text_color,
1251        radius,
1252    }
1253}
1254
1255/// Style information for caret rendering
1256#[derive(Debug, Clone, Copy, Default)]
1257pub struct CaretStyle {
1258    pub color: ColorU,
1259    pub width: f32,
1260    pub animation_duration: u32,
1261}
1262
1263/// Get caret style for a node
1264pub fn get_caret_style(styled_dom: &StyledDom, node_id: Option<NodeId>) -> CaretStyle {
1265    let Some(node_id) = node_id else {
1266        return CaretStyle::default();
1267    };
1268
1269    let node_data = &styled_dom.node_data.as_container()[node_id];
1270    let node_state = &StyledNodeState::default();
1271
1272    let color = styled_dom
1273        .css_property_cache
1274        .ptr
1275        .get_caret_color(node_data, &node_id, node_state)
1276        .and_then(|c| c.get_property().cloned())
1277        .map(|c| c.inner)
1278        .unwrap_or(ColorU {
1279            r: 255,
1280            g: 255,
1281            b: 255,
1282            a: 255, // White caret by default
1283        });
1284
1285    let width = styled_dom
1286        .css_property_cache
1287        .ptr
1288        .get_caret_width(node_data, &node_id, node_state)
1289        .and_then(|w| w.get_property().cloned())
1290        .map(|w| w.inner.to_pixels_internal(0.0, 16.0)) // 16.0 as default em size
1291        .unwrap_or(2.0); // 2px width by default
1292
1293    let animation_duration = styled_dom
1294        .css_property_cache
1295        .ptr
1296        .get_caret_animation_duration(node_data, &node_id, node_state)
1297        .and_then(|d| d.get_property().cloned())
1298        .map(|d| d.inner.inner) // Duration.inner is the u32 milliseconds value
1299        .unwrap_or(500); // 500ms blink by default
1300
1301    CaretStyle {
1302        color,
1303        width,
1304        animation_duration,
1305    }
1306}
1307
1308// Scrollbar Information
1309
1310/// Get scrollbar information from a layout node
1311pub fn get_scrollbar_info_from_layout(node: &LayoutNode) -> ScrollbarRequirements {
1312    // Use cached scrollbar_info if available (calculated during layout)
1313    if let Some(ref info) = node.scrollbar_info {
1314        return info.clone();
1315    }
1316
1317    // Fallback: Calculate based on content vs container size
1318    let container_size = node.used_size.unwrap_or_default();
1319
1320    // Get content size - check both inline layout and block children
1321    let content_size = if let Some(ref inline_layout) = node.inline_layout_result {
1322        // Has inline layout - use its bounds
1323        let bounds = inline_layout.layout.bounds();
1324        LogicalSize::new(bounds.width, bounds.height)
1325    } else if !node.children.is_empty() {
1326        // Has block children - calculate total content height from children
1327        // This is a rough estimate: sum of all children heights + margins
1328        // For a proper implementation, we'd need the actual positioned children
1329        let mut max_bottom: f32 = 0.0;
1330        let mut max_right: f32 = 0.0;
1331
1332        // Note: This is a simplified calculation. In reality we'd need the
1333        // calculated positions of children, but we don't have access to the
1334        // positioned_tree here. For now, estimate based on number of children
1335        // and typical item sizes.
1336        // TODO: Pass content bounds through from layout phase
1337
1338        // For overflow: auto/scroll containers, we know content overflows if
1339        // scrollbar_info was supposed to be set during layout. Since it wasn't,
1340        // check if we have many children as a heuristic.
1341        let num_children = node.children.len();
1342        if num_children > 3 {
1343            // Likely overflows - assume we need scrollbars
1344            LogicalSize::new(
1345                container_size.width,
1346                container_size.height * 2.0, // Force overflow detection
1347            )
1348        } else {
1349            container_size
1350        }
1351    } else {
1352        // No children - no scrollbar needed
1353        container_size
1354    };
1355
1356    // Standard scrollbar width (Chrome-like)
1357    const SCROLLBAR_SIZE: f32 = 12.0;
1358
1359    // Check if content overflows container
1360    let needs_vertical = content_size.height > container_size.height + 1.0;
1361    let needs_horizontal = content_size.width > container_size.width + 1.0;
1362
1363    ScrollbarRequirements {
1364        needs_vertical,
1365        needs_horizontal,
1366        scrollbar_width: if needs_vertical { SCROLLBAR_SIZE } else { 0.0 },
1367        scrollbar_height: if needs_horizontal {
1368            SCROLLBAR_SIZE
1369        } else {
1370            0.0
1371        },
1372    }
1373}
1374
1375get_css_property!(
1376    get_display_property_internal,
1377    get_display,
1378    LayoutDisplay,
1379    azul_css::props::property::CssPropertyType::Display
1380);
1381
1382pub fn get_display_property(
1383    styled_dom: &StyledDom,
1384    dom_id: Option<NodeId>,
1385) -> MultiValue<LayoutDisplay> {
1386    let Some(id) = dom_id else {
1387        return MultiValue::Exact(LayoutDisplay::Inline);
1388    };
1389    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
1390    get_display_property_internal(styled_dom, id, node_state)
1391}
1392
1393pub fn get_style_properties(styled_dom: &StyledDom, dom_id: NodeId) -> StyleProperties {
1394    use azul_css::props::basic::{PhysicalSize, PropertyContext, ResolutionContext};
1395
1396    let node_data = &styled_dom.node_data.as_container()[dom_id];
1397    let node_state = &styled_dom.styled_nodes.as_container()[dom_id].styled_node_state;
1398    let cache = &styled_dom.css_property_cache.ptr;
1399
1400    // NEW: Get ALL fonts from CSS font-family, not just first
1401    use azul_css::props::basic::font::{StyleFontFamily, StyleFontFamilyVec};
1402
1403    let font_families = cache
1404        .get_font_family(node_data, &dom_id, node_state)
1405        .and_then(|v| v.get_property().cloned())
1406        .unwrap_or_else(|| {
1407            // Default to serif (same as browser default)
1408            StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
1409        });
1410
1411    // Get parent's font-size for proper em resolution in font-size property
1412    let parent_font_size = styled_dom
1413        .node_hierarchy
1414        .as_container()
1415        .get(dom_id)
1416        .and_then(|node| {
1417            let parent_id = CoreNodeId::from_usize(node.parent)?;
1418            // Recursively get parent's font-size
1419            cache
1420                .get_font_size(
1421                    &styled_dom.node_data.as_container()[parent_id],
1422                    &parent_id,
1423                    &styled_dom.styled_nodes.as_container()[parent_id].styled_node_state,
1424                )
1425                .and_then(|v| v.get_property().cloned())
1426                .map(|v| {
1427                    // If parent also has em/rem, we'd need to recurse, but for now use fallback
1428                    use azul_css::props::basic::pixel::DEFAULT_FONT_SIZE;
1429                    v.inner.to_pixels_internal(0.0, DEFAULT_FONT_SIZE)
1430                })
1431        })
1432        .unwrap_or(azul_css::props::basic::pixel::DEFAULT_FONT_SIZE);
1433
1434    let root_font_size = get_root_font_size(styled_dom, node_state);
1435
1436    // Create resolution context for font-size (em refers to parent)
1437    let font_size_context = ResolutionContext {
1438        element_font_size: azul_css::props::basic::pixel::DEFAULT_FONT_SIZE, /* Not used for font-size property */
1439        parent_font_size,
1440        root_font_size,
1441        containing_block_size: PhysicalSize::new(0.0, 0.0),
1442        element_size: None,
1443        viewport_size: PhysicalSize::new(0.0, 0.0), // TODO: Pass viewport from LayoutContext
1444    };
1445
1446    // Get font-size: either from this node's CSS, or inherit from parent
1447    // font-size is an inheritable property, so if the node doesn't have
1448    // an explicit font-size, it should inherit from the parent (not default to 16px)
1449    let font_size = cache
1450        .get_font_size(node_data, &dom_id, node_state)
1451        .and_then(|v| v.get_property().cloned())
1452        .map(|v| {
1453            v.inner
1454                .resolve_with_context(&font_size_context, PropertyContext::FontSize)
1455        })
1456        .unwrap_or(parent_font_size);
1457
1458    let color_from_cache = cache
1459        .get_text_color(node_data, &dom_id, node_state)
1460        .and_then(|v| v.get_property().cloned())
1461        .map(|v| v.inner);
1462
1463    let color = color_from_cache.unwrap_or_default();
1464
1465    let line_height = cache
1466        .get_line_height(node_data, &dom_id, node_state)
1467        .and_then(|v| v.get_property().cloned())
1468        .map(|v| v.inner.normalized() * font_size)
1469        .unwrap_or(font_size * 1.2);
1470
1471    // Get background color for INLINE elements only
1472    // CSS background-color is NOT inherited. For block-level elements (th, td, div, etc.),
1473    // the background is painted separately by paint_element_background() in display_list.rs.
1474    // Only inline elements (span, em, strong, a, etc.) should have their background color
1475    // propagated through StyleProperties for the text rendering pipeline.
1476    use azul_css::props::layout::LayoutDisplay;
1477    let display = cache
1478        .get_display(node_data, &dom_id, node_state)
1479        .and_then(|v| v.get_property().cloned())
1480        .unwrap_or(LayoutDisplay::Inline);
1481
1482    // For inline and inline-block elements, get background content and border info
1483    // Block elements have their backgrounds/borders painted by display_list.rs
1484    let (background_color, background_content, border) =
1485        if matches!(display, LayoutDisplay::Inline | LayoutDisplay::InlineBlock) {
1486            let bg = get_background_color(styled_dom, dom_id, node_state);
1487            let bg_color = if bg.a > 0 { Some(bg) } else { None };
1488
1489            // Get full background contents (including gradients)
1490            let bg_contents = get_background_contents(styled_dom, dom_id, node_state);
1491
1492            // Get border info for inline elements
1493            let border_info = get_border_info(styled_dom, dom_id, node_state);
1494            let inline_border =
1495                get_inline_border_info(styled_dom, dom_id, node_state, &border_info);
1496
1497            (bg_color, bg_contents, inline_border)
1498        } else {
1499            // Block-level elements: background/border is painted by display_list.rs
1500            // via push_backgrounds_and_border() in DisplayListBuilder
1501            (None, Vec::new(), None)
1502        };
1503
1504    // Query font-weight from CSS cache
1505    let font_weight = cache
1506        .get_font_weight(node_data, &dom_id, node_state)
1507        .and_then(|v| v.get_property().copied())
1508        .unwrap_or(azul_css::props::basic::font::StyleFontWeight::Normal);
1509
1510    // Query font-style from CSS cache
1511    let font_style = cache
1512        .get_font_style(node_data, &dom_id, node_state)
1513        .and_then(|v| v.get_property().copied())
1514        .unwrap_or(azul_css::props::basic::font::StyleFontStyle::Normal);
1515
1516    // Convert StyleFontWeight/StyleFontStyle to fontconfig types
1517    let fc_weight = super::fc::convert_font_weight(font_weight);
1518    let fc_style = super::fc::convert_font_style(font_style);
1519
1520    // Check if any font family is a FontRef - if so, use FontStack::Ref
1521    // This allows embedded fonts (like Material Icons) to bypass fontconfig
1522    let font_stack = {
1523        // Look for a Ref in the font families
1524        let font_ref = (0..font_families.len())
1525            .find_map(|i| {
1526                match font_families.get(i).unwrap() {
1527                    azul_css::props::basic::font::StyleFontFamily::Ref(r) => Some(r.clone()),
1528                    _ => None,
1529                }
1530            });
1531        
1532        if let Some(font_ref) = font_ref {
1533            // Use FontStack::Ref for embedded fonts
1534            FontStack::Ref(font_ref)
1535        } else {
1536            // Build regular font stack from all font families
1537            let mut stack = Vec::with_capacity(font_families.len() + 3);
1538
1539            for i in 0..font_families.len() {
1540                stack.push(crate::text3::cache::FontSelector {
1541                    family: font_families.get(i).unwrap().as_string(),
1542                    weight: fc_weight,
1543                    style: fc_style,
1544                    unicode_ranges: Vec::new(),
1545                });
1546            }
1547
1548            // Add generic fallbacks (serif/sans-serif will be resolved based on Unicode ranges later)
1549            let generic_fallbacks = ["sans-serif", "serif", "monospace"];
1550            for fallback in &generic_fallbacks {
1551                if !stack
1552                    .iter()
1553                    .any(|f| f.family.to_lowercase() == fallback.to_lowercase())
1554                {
1555                    stack.push(crate::text3::cache::FontSelector {
1556                        family: fallback.to_string(),
1557                        weight: rust_fontconfig::FcWeight::Normal,
1558                        style: crate::text3::cache::FontStyle::Normal,
1559                        unicode_ranges: Vec::new(),
1560                    });
1561                }
1562            }
1563
1564            FontStack::Stack(stack)
1565        }
1566    };
1567
1568    // Get letter-spacing from CSS
1569    let letter_spacing = cache
1570        .get_letter_spacing(node_data, &dom_id, node_state)
1571        .and_then(|v| v.get_property().cloned())
1572        .map(|v| {
1573            // Convert PixelValue to Spacing
1574            // PixelValue can be px, em, rem, etc.
1575            let px_value = v.inner.resolve_with_context(&font_size_context, PropertyContext::FontSize);
1576            crate::text3::cache::Spacing::Px(px_value.round() as i32)
1577        })
1578        .unwrap_or_default();
1579
1580    // Get word-spacing from CSS
1581    let word_spacing = cache
1582        .get_word_spacing(node_data, &dom_id, node_state)
1583        .and_then(|v| v.get_property().cloned())
1584        .map(|v| {
1585            let px_value = v.inner.resolve_with_context(&font_size_context, PropertyContext::FontSize);
1586            crate::text3::cache::Spacing::Px(px_value.round() as i32)
1587        })
1588        .unwrap_or_default();
1589
1590    // Get text-decoration from CSS
1591    let text_decoration = cache
1592        .get_text_decoration(node_data, &dom_id, node_state)
1593        .and_then(|v| v.get_property().cloned())
1594        .map(|v| crate::text3::cache::TextDecoration::from_css(v))
1595        .unwrap_or_default();
1596
1597    // Get tab-size (tab-width) from CSS
1598    let tab_size = cache
1599        .get_tab_width(node_data, &dom_id, node_state)
1600        .and_then(|v| v.get_property().cloned())
1601        .map(|v| v.inner.number.get())
1602        .unwrap_or(8.0);
1603
1604    let properties = StyleProperties {
1605        font_stack,
1606        font_size_px: font_size,
1607        color,
1608        background_color,
1609        background_content,
1610        border,
1611        line_height,
1612        letter_spacing,
1613        word_spacing,
1614        text_decoration,
1615        tab_size,
1616        // These still use defaults - could be extended in future:
1617        // font_features, font_variations, text_transform, writing_mode, 
1618        // text_orientation, text_combine_upright, font_variant_*
1619        ..Default::default()
1620    };
1621
1622    properties
1623}
1624
1625pub fn get_list_style_type(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> StyleListStyleType {
1626    let Some(id) = dom_id else {
1627        return StyleListStyleType::default();
1628    };
1629    let node_data = &styled_dom.node_data.as_container()[id];
1630    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
1631    styled_dom
1632        .css_property_cache
1633        .ptr
1634        .get_list_style_type(node_data, &id, node_state)
1635        .and_then(|v| v.get_property().copied())
1636        .unwrap_or_default()
1637}
1638
1639pub fn get_list_style_position(
1640    styled_dom: &StyledDom,
1641    dom_id: Option<NodeId>,
1642) -> StyleListStylePosition {
1643    let Some(id) = dom_id else {
1644        return StyleListStylePosition::default();
1645    };
1646    let node_data = &styled_dom.node_data.as_container()[id];
1647    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
1648    styled_dom
1649        .css_property_cache
1650        .ptr
1651        .get_list_style_position(node_data, &id, node_state)
1652        .and_then(|v| v.get_property().copied())
1653        .unwrap_or_default()
1654}
1655
1656// New: Taffy Bridge Getters - Box Model Properties with Ua Css Fallback
1657
1658use azul_css::props::layout::{
1659    LayoutInsetBottom, LayoutLeft, LayoutMarginBottom, LayoutMarginLeft, LayoutMarginRight,
1660    LayoutMarginTop, LayoutMaxHeight, LayoutMaxWidth, LayoutMinHeight, LayoutMinWidth,
1661    LayoutPaddingBottom, LayoutPaddingLeft, LayoutPaddingRight, LayoutPaddingTop, LayoutRight,
1662    LayoutTop,
1663};
1664
1665/// Get inset (position) properties - returns MultiValue<PixelValue>
1666get_css_property_pixel!(
1667    get_css_left,
1668    get_left,
1669    azul_css::props::property::CssPropertyType::Left
1670);
1671get_css_property_pixel!(
1672    get_css_right,
1673    get_right,
1674    azul_css::props::property::CssPropertyType::Right
1675);
1676get_css_property_pixel!(
1677    get_css_top,
1678    get_top,
1679    azul_css::props::property::CssPropertyType::Top
1680);
1681get_css_property_pixel!(
1682    get_css_bottom,
1683    get_bottom,
1684    azul_css::props::property::CssPropertyType::Bottom
1685);
1686
1687/// Get margin properties - returns MultiValue<PixelValue>
1688get_css_property_pixel!(
1689    get_css_margin_left,
1690    get_margin_left,
1691    azul_css::props::property::CssPropertyType::MarginLeft
1692);
1693get_css_property_pixel!(
1694    get_css_margin_right,
1695    get_margin_right,
1696    azul_css::props::property::CssPropertyType::MarginRight
1697);
1698get_css_property_pixel!(
1699    get_css_margin_top,
1700    get_margin_top,
1701    azul_css::props::property::CssPropertyType::MarginTop
1702);
1703get_css_property_pixel!(
1704    get_css_margin_bottom,
1705    get_margin_bottom,
1706    azul_css::props::property::CssPropertyType::MarginBottom
1707);
1708
1709/// Get padding properties - returns MultiValue<PixelValue>
1710get_css_property_pixel!(
1711    get_css_padding_left,
1712    get_padding_left,
1713    azul_css::props::property::CssPropertyType::PaddingLeft
1714);
1715get_css_property_pixel!(
1716    get_css_padding_right,
1717    get_padding_right,
1718    azul_css::props::property::CssPropertyType::PaddingRight
1719);
1720get_css_property_pixel!(
1721    get_css_padding_top,
1722    get_padding_top,
1723    azul_css::props::property::CssPropertyType::PaddingTop
1724);
1725get_css_property_pixel!(
1726    get_css_padding_bottom,
1727    get_padding_bottom,
1728    azul_css::props::property::CssPropertyType::PaddingBottom
1729);
1730
1731/// Get min/max size properties
1732get_css_property!(
1733    get_css_min_width,
1734    get_min_width,
1735    LayoutMinWidth,
1736    azul_css::props::property::CssPropertyType::MinWidth
1737);
1738
1739get_css_property!(
1740    get_css_min_height,
1741    get_min_height,
1742    LayoutMinHeight,
1743    azul_css::props::property::CssPropertyType::MinHeight
1744);
1745
1746get_css_property!(
1747    get_css_max_width,
1748    get_max_width,
1749    LayoutMaxWidth,
1750    azul_css::props::property::CssPropertyType::MaxWidth
1751);
1752
1753get_css_property!(
1754    get_css_max_height,
1755    get_max_height,
1756    LayoutMaxHeight,
1757    azul_css::props::property::CssPropertyType::MaxHeight
1758);
1759
1760/// Get border width properties (no UA CSS fallback needed, defaults to 0)
1761get_css_property_pixel!(
1762    get_css_border_left_width,
1763    get_border_left_width,
1764    azul_css::props::property::CssPropertyType::BorderLeftWidth
1765);
1766get_css_property_pixel!(
1767    get_css_border_right_width,
1768    get_border_right_width,
1769    azul_css::props::property::CssPropertyType::BorderRightWidth
1770);
1771get_css_property_pixel!(
1772    get_css_border_top_width,
1773    get_border_top_width,
1774    azul_css::props::property::CssPropertyType::BorderTopWidth
1775);
1776get_css_property_pixel!(
1777    get_css_border_bottom_width,
1778    get_border_bottom_width,
1779    azul_css::props::property::CssPropertyType::BorderBottomWidth
1780);
1781
1782// Fragmentation (page breaking) properties
1783
1784/// Get break-before property for paged media
1785pub fn get_break_before(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> PageBreak {
1786    let Some(id) = dom_id else {
1787        return PageBreak::Auto;
1788    };
1789    let node_data = &styled_dom.node_data.as_container()[id];
1790    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
1791    styled_dom
1792        .css_property_cache
1793        .ptr
1794        .get_break_before(node_data, &id, node_state)
1795        .and_then(|v| v.get_property().cloned())
1796        .unwrap_or(PageBreak::Auto)
1797}
1798
1799/// Get break-after property for paged media
1800pub fn get_break_after(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> PageBreak {
1801    let Some(id) = dom_id else {
1802        return PageBreak::Auto;
1803    };
1804    let node_data = &styled_dom.node_data.as_container()[id];
1805    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
1806    styled_dom
1807        .css_property_cache
1808        .ptr
1809        .get_break_after(node_data, &id, node_state)
1810        .and_then(|v| v.get_property().cloned())
1811        .unwrap_or(PageBreak::Auto)
1812}
1813
1814/// Check if a PageBreak value forces a page break (always, page, left, right, etc.)
1815pub fn is_forced_page_break(page_break: PageBreak) -> bool {
1816    matches!(
1817        page_break,
1818        PageBreak::Always
1819            | PageBreak::Page
1820            | PageBreak::Left
1821            | PageBreak::Right
1822            | PageBreak::Recto
1823            | PageBreak::Verso
1824            | PageBreak::All
1825    )
1826}
1827
1828/// Get break-inside property for paged media
1829pub fn get_break_inside(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> BreakInside {
1830    let Some(id) = dom_id else {
1831        return BreakInside::Auto;
1832    };
1833    let node_data = &styled_dom.node_data.as_container()[id];
1834    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
1835    styled_dom
1836        .css_property_cache
1837        .ptr
1838        .get_break_inside(node_data, &id, node_state)
1839        .and_then(|v| v.get_property().cloned())
1840        .unwrap_or(BreakInside::Auto)
1841}
1842
1843/// Get orphans property (minimum lines at bottom of page)
1844pub fn get_orphans(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> u32 {
1845    let Some(id) = dom_id else {
1846        return 2; // Default value
1847    };
1848    let node_data = &styled_dom.node_data.as_container()[id];
1849    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
1850    styled_dom
1851        .css_property_cache
1852        .ptr
1853        .get_orphans(node_data, &id, node_state)
1854        .and_then(|v| v.get_property().cloned())
1855        .map(|o| o.inner)
1856        .unwrap_or(2)
1857}
1858
1859/// Get widows property (minimum lines at top of page)
1860pub fn get_widows(styled_dom: &StyledDom, dom_id: Option<NodeId>) -> u32 {
1861    let Some(id) = dom_id else {
1862        return 2; // Default value
1863    };
1864    let node_data = &styled_dom.node_data.as_container()[id];
1865    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
1866    styled_dom
1867        .css_property_cache
1868        .ptr
1869        .get_widows(node_data, &id, node_state)
1870        .and_then(|v| v.get_property().cloned())
1871        .map(|w| w.inner)
1872        .unwrap_or(2)
1873}
1874
1875/// Get box-decoration-break property
1876pub fn get_box_decoration_break(
1877    styled_dom: &StyledDom,
1878    dom_id: Option<NodeId>,
1879) -> BoxDecorationBreak {
1880    let Some(id) = dom_id else {
1881        return BoxDecorationBreak::Slice;
1882    };
1883    let node_data = &styled_dom.node_data.as_container()[id];
1884    let node_state = &styled_dom.styled_nodes.as_container()[id].styled_node_state;
1885    styled_dom
1886        .css_property_cache
1887        .ptr
1888        .get_box_decoration_break(node_data, &id, node_state)
1889        .and_then(|v| v.get_property().cloned())
1890        .unwrap_or(BoxDecorationBreak::Slice)
1891}
1892
1893// Helper functions for break properties
1894
1895/// Check if a PageBreak value is avoid
1896pub fn is_avoid_page_break(page_break: &PageBreak) -> bool {
1897    matches!(page_break, PageBreak::Avoid | PageBreak::AvoidPage)
1898}
1899
1900/// Check if a BreakInside value prevents breaks
1901pub fn is_avoid_break_inside(break_inside: &BreakInside) -> bool {
1902    matches!(
1903        break_inside,
1904        BreakInside::Avoid | BreakInside::AvoidPage | BreakInside::AvoidColumn
1905    )
1906}
1907
1908// Font Chain Resolution - Pre-Layout Font Loading
1909
1910use std::collections::HashMap;
1911
1912use rust_fontconfig::{FcFontCache, FcWeight, FontFallbackChain, PatternMatch};
1913
1914use crate::text3::cache::{FontChainKey, FontChainKeyOrRef, FontSelector, FontStack, FontStyle};
1915
1916/// Result of collecting font stacks from a StyledDom
1917/// Contains all unique font stacks and the mapping from StyleFontFamiliesHash to FontChainKey
1918#[derive(Debug, Clone)]
1919pub struct CollectedFontStacks {
1920    /// All unique font stacks found in the document (system/file fonts via fontconfig)
1921    pub font_stacks: Vec<Vec<FontSelector>>,
1922    /// Map from the font stack hash to the index in font_stacks
1923    pub hash_to_index: HashMap<u64, usize>,
1924    /// Direct FontRefs that bypass fontconfig (e.g., embedded icon fonts)
1925    /// These are keyed by their pointer address for uniqueness
1926    pub font_refs: HashMap<usize, azul_css::props::basic::font::FontRef>,
1927}
1928
1929/// Resolved font chains ready for use in layout
1930/// This is the result of resolving font stacks against FcFontCache
1931#[derive(Debug, Clone)]
1932pub struct ResolvedFontChains {
1933    /// Map from FontChainKeyOrRef to the resolved FontFallbackChain
1934    /// For FontChainKeyOrRef::Ref variants, the FontFallbackChain contains
1935    /// a single-font chain that covers the entire Unicode range.
1936    pub chains: HashMap<FontChainKeyOrRef, FontFallbackChain>,
1937}
1938
1939impl ResolvedFontChains {
1940    /// Get a font chain by its key
1941    pub fn get(&self, key: &FontChainKeyOrRef) -> Option<&FontFallbackChain> {
1942        self.chains.get(key)
1943    }
1944    
1945    /// Get a font chain by FontChainKey (for system fonts)
1946    pub fn get_by_chain_key(&self, key: &FontChainKey) -> Option<&FontFallbackChain> {
1947        self.chains.get(&FontChainKeyOrRef::Chain(key.clone()))
1948    }
1949
1950    /// Get a font chain for a font stack (via fontconfig)
1951    pub fn get_for_font_stack(&self, font_stack: &[FontSelector]) -> Option<&FontFallbackChain> {
1952        let key = FontChainKeyOrRef::Chain(FontChainKey::from_selectors(font_stack));
1953        self.chains.get(&key)
1954    }
1955    
1956    /// Get a font chain for a FontRef pointer
1957    pub fn get_for_font_ref(&self, ptr: usize) -> Option<&FontFallbackChain> {
1958        self.chains.get(&FontChainKeyOrRef::Ref(ptr))
1959    }
1960
1961    /// Consume self and return the inner HashMap with FontChainKeyOrRef keys
1962    ///
1963    /// This is useful when you need access to both Chain and Ref variants.
1964    pub fn into_inner(self) -> HashMap<FontChainKeyOrRef, FontFallbackChain> {
1965        self.chains
1966    }
1967
1968    /// Consume self and return only the fontconfig-resolved chains
1969    /// 
1970    /// This filters out FontRef entries and returns only the chains
1971    /// resolved via fontconfig. This is what FontManager expects.
1972    pub fn into_fontconfig_chains(self) -> HashMap<FontChainKey, FontFallbackChain> {
1973        self.chains
1974            .into_iter()
1975            .filter_map(|(key, chain)| {
1976                match key {
1977                    FontChainKeyOrRef::Chain(chain_key) => Some((chain_key, chain)),
1978                    FontChainKeyOrRef::Ref(_) => None,
1979                }
1980            })
1981            .collect()
1982    }
1983
1984    /// Get the number of resolved chains
1985    pub fn len(&self) -> usize {
1986        self.chains.len()
1987    }
1988
1989    /// Check if there are no resolved chains
1990    pub fn is_empty(&self) -> bool {
1991        self.chains.is_empty()
1992    }
1993    
1994    /// Get the number of direct FontRefs
1995    pub fn font_refs_len(&self) -> usize {
1996        self.chains.keys().filter(|k| k.is_ref()).count()
1997    }
1998}
1999
2000/// Collect all unique font stacks from a StyledDom
2001///
2002/// This is a pure function that iterates over all nodes in the DOM and
2003/// extracts the font-family property from each node that has text content.
2004///
2005/// # Arguments
2006/// * `styled_dom` - The styled DOM to extract font stacks from
2007///
2008/// # Returns
2009/// A `CollectedFontStacks` containing all unique font stacks and a hash-to-index mapping
2010pub fn collect_font_stacks_from_styled_dom(styled_dom: &StyledDom) -> CollectedFontStacks {
2011    let mut font_stacks = Vec::new();
2012    let mut hash_to_index: HashMap<u64, usize> = HashMap::new();
2013    let mut seen_hashes = std::collections::HashSet::new();
2014    let mut font_refs: HashMap<usize, azul_css::props::basic::font::FontRef> = HashMap::new();
2015
2016    let node_data_container = styled_dom.node_data.as_container();
2017    let styled_nodes_container = styled_dom.styled_nodes.as_container();
2018    let cache = &styled_dom.css_property_cache.ptr;
2019
2020    // Iterate over all nodes
2021    for (node_idx, node_data) in node_data_container.internal.iter().enumerate() {
2022        // Only process text nodes (they are the ones that need fonts)
2023        if !matches!(node_data.node_type, NodeType::Text(_)) {
2024            continue;
2025        }
2026
2027        let dom_id = match NodeId::from_usize(node_idx) {
2028            Some(id) => id,
2029            None => continue,
2030        };
2031
2032        let node_state = &styled_nodes_container[dom_id].styled_node_state;
2033
2034        // Get font families from CSS
2035        let font_families = cache
2036            .get_font_family(node_data, &dom_id, node_state)
2037            .and_then(|v| v.get_property().cloned())
2038            .unwrap_or_else(|| {
2039                StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("serif".into())])
2040            });
2041
2042        // Check if the first font family is a FontRef (direct embedded font)
2043        // If so, we don't need to go through fontconfig - just collect the FontRef
2044        if let Some(first_family) = font_families.get(0) {
2045            if let StyleFontFamily::Ref(font_ref) = first_family {
2046                let ptr = font_ref.parsed as usize;
2047                if !font_refs.contains_key(&ptr) {
2048                    font_refs.insert(ptr, font_ref.clone());
2049                }
2050                // Skip the normal font stack processing for FontRef
2051                continue;
2052            }
2053        }
2054
2055        // Get font weight and style
2056        let font_weight = cache
2057            .get_font_weight(node_data, &dom_id, node_state)
2058            .and_then(|v| v.get_property().copied())
2059            .unwrap_or(azul_css::props::basic::font::StyleFontWeight::Normal);
2060
2061        let font_style = cache
2062            .get_font_style(node_data, &dom_id, node_state)
2063            .and_then(|v| v.get_property().copied())
2064            .unwrap_or(azul_css::props::basic::font::StyleFontStyle::Normal);
2065
2066        // Convert to fontconfig types
2067        let fc_weight = super::fc::convert_font_weight(font_weight);
2068        let fc_style = super::fc::convert_font_style(font_style);
2069
2070        // Build font stack (only for non-Ref font families)
2071        let mut font_stack = Vec::with_capacity(font_families.len() + 3);
2072
2073        for i in 0..font_families.len() {
2074            let family = font_families.get(i).unwrap();
2075            // Skip FontRef entries in the stack - they're handled separately
2076            if matches!(family, StyleFontFamily::Ref(_)) {
2077                continue;
2078            }
2079            font_stack.push(FontSelector {
2080                family: family.as_string(),
2081                weight: fc_weight,
2082                style: fc_style,
2083                unicode_ranges: Vec::new(),
2084            });
2085        }
2086
2087        // Add generic fallbacks
2088        let generic_fallbacks = ["sans-serif", "serif", "monospace"];
2089        for fallback in &generic_fallbacks {
2090            if !font_stack
2091                .iter()
2092                .any(|f| f.family.to_lowercase() == fallback.to_lowercase())
2093            {
2094                font_stack.push(FontSelector {
2095                    family: fallback.to_string(),
2096                    weight: FcWeight::Normal,
2097                    style: FontStyle::Normal,
2098                    unicode_ranges: Vec::new(),
2099                });
2100            }
2101        }
2102
2103        // Skip empty font stacks (can happen if all families were FontRefs)
2104        if font_stack.is_empty() {
2105            continue;
2106        }
2107
2108        // Compute hash for deduplication
2109        let key = FontChainKey::from_selectors(&font_stack);
2110        let hash = {
2111            use std::hash::{Hash, Hasher};
2112            let mut hasher = std::collections::hash_map::DefaultHasher::new();
2113            key.hash(&mut hasher);
2114            hasher.finish()
2115        };
2116
2117        // Only add if not seen before
2118        if !seen_hashes.contains(&hash) {
2119            seen_hashes.insert(hash);
2120            let idx = font_stacks.len();
2121            font_stacks.push(font_stack);
2122            hash_to_index.insert(hash, idx);
2123        }
2124    }
2125
2126    CollectedFontStacks {
2127        font_stacks,
2128        hash_to_index,
2129        font_refs,
2130    }
2131}
2132
2133/// Resolve all font chains for the collected font stacks
2134///
2135/// This is a pure function that takes the collected font stacks and resolves
2136/// them against the FcFontCache to produce FontFallbackChains.
2137///
2138/// # Arguments
2139/// * `collected` - The collected font stacks from `collect_font_stacks_from_styled_dom`
2140/// * `fc_cache` - The fontconfig cache to resolve fonts against
2141///
2142/// # Returns
2143/// A `ResolvedFontChains` containing all resolved font chains
2144pub fn resolve_font_chains(
2145    collected: &CollectedFontStacks,
2146    fc_cache: &FcFontCache,
2147) -> ResolvedFontChains {
2148    let mut chains = HashMap::new();
2149
2150    // Resolve system/file font stacks via fontconfig
2151    for font_stack in &collected.font_stacks {
2152        if font_stack.is_empty() {
2153            continue;
2154        }
2155
2156        // Build font families list
2157        let font_families: Vec<String> = font_stack
2158            .iter()
2159            .map(|s| s.family.clone())
2160            .filter(|f| !f.is_empty())
2161            .collect();
2162
2163        let font_families = if font_families.is_empty() {
2164            vec!["sans-serif".to_string()]
2165        } else {
2166            font_families
2167        };
2168
2169        let weight = font_stack[0].weight;
2170        let is_italic = font_stack[0].style == FontStyle::Italic;
2171        let is_oblique = font_stack[0].style == FontStyle::Oblique;
2172
2173        let cache_key = FontChainKeyOrRef::Chain(FontChainKey {
2174            font_families: font_families.clone(),
2175            weight,
2176            italic: is_italic,
2177            oblique: is_oblique,
2178        });
2179
2180        // Skip if already resolved
2181        if chains.contains_key(&cache_key) {
2182            continue;
2183        }
2184
2185        // Resolve the font chain
2186        let italic = if is_italic {
2187            PatternMatch::True
2188        } else {
2189            PatternMatch::DontCare
2190        };
2191        let oblique = if is_oblique {
2192            PatternMatch::True
2193        } else {
2194            PatternMatch::DontCare
2195        };
2196
2197        let mut trace = Vec::new();
2198        let chain =
2199            fc_cache.resolve_font_chain(&font_families, weight, italic, oblique, &mut trace);
2200
2201        chains.insert(cache_key, chain);
2202    }
2203
2204    // Create single-font chains for direct FontRefs
2205    // These bypass fontconfig and cover the entire Unicode range
2206    // NOTE: FontRefs are handled differently - they don't go through fontconfig at all.
2207    // The shaping code checks style.font_stack for FontStack::Ref and uses the font directly.
2208    // We just need to record that we have these font refs for font loading purposes.
2209    for (ptr, _font_ref) in &collected.font_refs {
2210        let cache_key = FontChainKeyOrRef::Ref(*ptr);
2211        
2212        // For FontRef, we create an empty pattern that will be handled specially
2213        // during shaping. The font data is already available via the FontRef pointer.
2214        // We don't insert anything - the shaping code handles FontStack::Ref directly.
2215        let _ = cache_key; // Mark as used
2216    }
2217
2218    ResolvedFontChains { chains }
2219}
2220
2221/// Convenience function that collects and resolves font chains in one call
2222///
2223/// # Arguments
2224/// * `styled_dom` - The styled DOM to extract font stacks from
2225/// * `fc_cache` - The fontconfig cache to resolve fonts against
2226///
2227/// # Returns
2228/// A `ResolvedFontChains` containing all resolved font chains
2229pub fn collect_and_resolve_font_chains(
2230    styled_dom: &StyledDom,
2231    fc_cache: &FcFontCache,
2232) -> ResolvedFontChains {
2233    let collected = collect_font_stacks_from_styled_dom(styled_dom);
2234    resolve_font_chains(&collected, fc_cache)
2235}
2236
2237/// Register all embedded FontRefs from the styled DOM in the FontManager
2238/// 
2239/// This must be called BEFORE layout so that the fonts are available
2240/// for WebRender resource registration after layout.
2241pub fn register_embedded_fonts_from_styled_dom<T: crate::font_traits::ParsedFontTrait>(
2242    styled_dom: &StyledDom,
2243    font_manager: &crate::text3::cache::FontManager<T>,
2244) {
2245    let collected = collect_font_stacks_from_styled_dom(styled_dom);
2246    for (_ptr, font_ref) in &collected.font_refs {
2247        font_manager.register_embedded_font(font_ref);
2248    }
2249}
2250
2251// Font Loading Functions
2252
2253use std::collections::HashSet;
2254
2255use rust_fontconfig::FontId;
2256
2257/// Extract all unique FontIds from resolved font chains
2258///
2259/// This function collects all FontIds that are referenced in the font chains,
2260/// which represents the complete set of fonts that may be needed for rendering.
2261pub fn collect_font_ids_from_chains(chains: &ResolvedFontChains) -> HashSet<FontId> {
2262    let mut font_ids = HashSet::new();
2263
2264    for chain in chains.chains.values() {
2265        // Collect from CSS fallbacks
2266        for group in &chain.css_fallbacks {
2267            for font in &group.fonts {
2268                font_ids.insert(font.id);
2269            }
2270        }
2271
2272        // Collect from Unicode fallbacks
2273        for font in &chain.unicode_fallbacks {
2274            font_ids.insert(font.id);
2275        }
2276    }
2277
2278    font_ids
2279}
2280
2281/// Compute which fonts need to be loaded (diff with already loaded fonts)
2282///
2283/// # Arguments
2284/// * `required_fonts` - Set of FontIds that are needed
2285/// * `already_loaded` - Set of FontIds that are already loaded
2286///
2287/// # Returns
2288/// Set of FontIds that need to be loaded
2289pub fn compute_fonts_to_load(
2290    required_fonts: &HashSet<FontId>,
2291    already_loaded: &HashSet<FontId>,
2292) -> HashSet<FontId> {
2293    required_fonts.difference(already_loaded).cloned().collect()
2294}
2295
2296/// Result of loading fonts
2297#[derive(Debug)]
2298pub struct FontLoadResult<T> {
2299    /// Successfully loaded fonts
2300    pub loaded: HashMap<FontId, T>,
2301    /// FontIds that failed to load, with error messages
2302    pub failed: Vec<(FontId, String)>,
2303}
2304
2305/// Load fonts from disk using the provided loader function
2306///
2307/// This is a generic function that works with any font loading implementation.
2308/// The `load_fn` parameter should be a function that takes font bytes and an index,
2309/// and returns a parsed font or an error.
2310///
2311/// # Arguments
2312/// * `font_ids` - Set of FontIds to load
2313/// * `fc_cache` - The fontconfig cache to get font paths from
2314/// * `load_fn` - Function to load and parse font bytes
2315///
2316/// # Returns
2317/// A `FontLoadResult` containing successfully loaded fonts and any failures
2318pub fn load_fonts_from_disk<T, F>(
2319    font_ids: &HashSet<FontId>,
2320    fc_cache: &FcFontCache,
2321    load_fn: F,
2322) -> FontLoadResult<T>
2323where
2324    F: Fn(&[u8], usize) -> Result<T, crate::text3::cache::LayoutError>,
2325{
2326    let mut loaded = HashMap::new();
2327    let mut failed = Vec::new();
2328
2329    for font_id in font_ids {
2330        // Get font bytes from fc_cache
2331        let font_bytes = match fc_cache.get_font_bytes(font_id) {
2332            Some(bytes) => bytes,
2333            None => {
2334                failed.push((
2335                    *font_id,
2336                    format!("Could not get font bytes for {:?}", font_id),
2337                ));
2338                continue;
2339            }
2340        };
2341
2342        // Get font index (for font collections like .ttc files)
2343        let font_index = fc_cache
2344            .get_font_by_id(font_id)
2345            .and_then(|source| match source {
2346                rust_fontconfig::FontSource::Disk(path) => Some(path.font_index),
2347                rust_fontconfig::FontSource::Memory(font) => Some(font.font_index),
2348            })
2349            .unwrap_or(0) as usize;
2350
2351        // Load the font using the provided function
2352        match load_fn(&font_bytes, font_index) {
2353            Ok(font) => {
2354                loaded.insert(*font_id, font);
2355            }
2356            Err(e) => {
2357                failed.push((
2358                    *font_id,
2359                    format!("Failed to parse font {:?}: {:?}", font_id, e),
2360                ));
2361            }
2362        }
2363    }
2364
2365    FontLoadResult { loaded, failed }
2366}
2367
2368/// Convenience function to load all required fonts for a styled DOM
2369///
2370/// This function:
2371/// 1. Collects all font stacks from the DOM
2372/// 2. Resolves them to font chains
2373/// 3. Extracts all required FontIds
2374/// 4. Computes which fonts need to be loaded (diff with already loaded)
2375/// 5. Loads the missing fonts
2376///
2377/// # Arguments
2378/// * `styled_dom` - The styled DOM to extract font requirements from
2379/// * `fc_cache` - The fontconfig cache
2380/// * `already_loaded` - Set of FontIds that are already loaded
2381/// * `load_fn` - Function to load and parse font bytes
2382///
2383/// # Returns
2384/// A tuple of (ResolvedFontChains, FontLoadResult)
2385pub fn resolve_and_load_fonts<T, F>(
2386    styled_dom: &StyledDom,
2387    fc_cache: &FcFontCache,
2388    already_loaded: &HashSet<FontId>,
2389    load_fn: F,
2390) -> (ResolvedFontChains, FontLoadResult<T>)
2391where
2392    F: Fn(&[u8], usize) -> Result<T, crate::text3::cache::LayoutError>,
2393{
2394    // Step 1-2: Collect and resolve font chains
2395    let chains = collect_and_resolve_font_chains(styled_dom, fc_cache);
2396
2397    // Step 3: Extract all required FontIds
2398    let required_fonts = collect_font_ids_from_chains(&chains);
2399
2400    // Step 4: Compute diff
2401    let fonts_to_load = compute_fonts_to_load(&required_fonts, already_loaded);
2402
2403    // Step 5: Load missing fonts
2404    let load_result = load_fonts_from_disk(&fonts_to_load, fc_cache, load_fn);
2405
2406    (chains, load_result)
2407}
2408
2409// ============================================================================
2410// Scrollbar Style Getters
2411// ============================================================================
2412
2413use azul_css::props::style::scrollbar::{
2414    LayoutScrollbarWidth, ScrollbarColorCustom, ScrollbarInfo, StyleScrollbarColor,
2415    SCROLLBAR_CLASSIC_LIGHT,
2416};
2417
2418/// Computed scrollbar style for a node, combining CSS properties
2419#[derive(Debug, Clone)]
2420pub struct ComputedScrollbarStyle {
2421    /// The scrollbar width mode (auto/thin/none)
2422    pub width_mode: LayoutScrollbarWidth,
2423    /// Actual width in pixels (resolved from width_mode or scrollbar-style)
2424    pub width_px: f32,
2425    /// Thumb color
2426    pub thumb_color: ColorU,
2427    /// Track color
2428    pub track_color: ColorU,
2429    /// Button color (for scroll arrows)
2430    pub button_color: ColorU,
2431    /// Corner color (where scrollbars meet)
2432    pub corner_color: ColorU,
2433    /// Whether to clip the scrollbar to the container's border-radius
2434    pub clip_to_container_border: bool,
2435}
2436
2437impl Default for ComputedScrollbarStyle {
2438    fn default() -> Self {
2439        Self {
2440            width_mode: LayoutScrollbarWidth::Auto,
2441            width_px: 16.0, // Standard scrollbar width
2442            // Debug colors - bright magenta thumb, orange track
2443            thumb_color: ColorU::new(255, 0, 255, 255), // Magenta
2444            track_color: ColorU::new(255, 165, 0, 255), // Orange
2445            button_color: ColorU::new(0, 255, 0, 255),  // Green
2446            corner_color: ColorU::new(0, 0, 255, 255),  // Blue
2447            clip_to_container_border: false,
2448        }
2449    }
2450}
2451
2452/// Get the computed scrollbar style for a node
2453///
2454/// This combines:
2455/// - `scrollbar-width` property (auto/thin/none)
2456/// - `scrollbar-color` property (thumb and track colors)
2457/// - `-azul-scrollbar-style` property (full scrollbar customization)
2458pub fn get_scrollbar_style(
2459    styled_dom: &StyledDom,
2460    node_id: NodeId,
2461    node_state: &StyledNodeState,
2462) -> ComputedScrollbarStyle {
2463    let node_data = &styled_dom.node_data.as_container()[node_id];
2464
2465    // Start with defaults
2466    let mut result = ComputedScrollbarStyle::default();
2467
2468    // Check for -azul-scrollbar-style (full customization)
2469    if let Some(scrollbar_style) = styled_dom
2470        .css_property_cache
2471        .ptr
2472        .get_scrollbar_style(node_data, &node_id, node_state)
2473        .and_then(|v| v.get_property())
2474    {
2475        // Use the detailed scrollbar info
2476        result.width_px = match scrollbar_style.horizontal.width {
2477            azul_css::props::layout::dimensions::LayoutWidth::Px(px) => {
2478                // Use to_pixels_internal with 100% = 16px and 1em = 16px as reasonable defaults
2479                px.to_pixels_internal(16.0, 16.0)
2480            }
2481            _ => 16.0,
2482        };
2483        result.thumb_color = extract_color_from_background(&scrollbar_style.horizontal.thumb);
2484        result.track_color = extract_color_from_background(&scrollbar_style.horizontal.track);
2485        result.button_color = extract_color_from_background(&scrollbar_style.horizontal.button);
2486        result.corner_color = extract_color_from_background(&scrollbar_style.horizontal.corner);
2487        result.clip_to_container_border = scrollbar_style.horizontal.clip_to_container_border;
2488    }
2489
2490    // Check for scrollbar-width (overrides width)
2491    if let Some(scrollbar_width) = styled_dom
2492        .css_property_cache
2493        .ptr
2494        .get_scrollbar_width(node_data, &node_id, node_state)
2495        .and_then(|v| v.get_property())
2496    {
2497        result.width_mode = *scrollbar_width;
2498        result.width_px = match scrollbar_width {
2499            LayoutScrollbarWidth::Auto => 16.0,
2500            LayoutScrollbarWidth::Thin => 8.0,
2501            LayoutScrollbarWidth::None => 0.0,
2502        };
2503    }
2504
2505    // Check for scrollbar-color (overrides thumb/track colors)
2506    if let Some(scrollbar_color) = styled_dom
2507        .css_property_cache
2508        .ptr
2509        .get_scrollbar_color(node_data, &node_id, node_state)
2510        .and_then(|v| v.get_property())
2511    {
2512        match scrollbar_color {
2513            StyleScrollbarColor::Auto => {
2514                // Keep default colors
2515            }
2516            StyleScrollbarColor::Custom(custom) => {
2517                result.thumb_color = custom.thumb;
2518                result.track_color = custom.track;
2519            }
2520        }
2521    }
2522
2523    result
2524}
2525
2526/// Helper to extract a solid color from a StyleBackgroundContent
2527fn extract_color_from_background(
2528    bg: &azul_css::props::style::background::StyleBackgroundContent,
2529) -> ColorU {
2530    use azul_css::props::style::background::StyleBackgroundContent;
2531    match bg {
2532        StyleBackgroundContent::Color(c) => *c,
2533        _ => ColorU::TRANSPARENT,
2534    }
2535}
2536
2537/// Check if a node should clip its scrollbar to the container's border-radius
2538pub fn should_clip_scrollbar_to_border(
2539    styled_dom: &StyledDom,
2540    node_id: NodeId,
2541    node_state: &StyledNodeState,
2542) -> bool {
2543    let style = get_scrollbar_style(styled_dom, node_id, node_state);
2544    style.clip_to_container_border
2545}
2546
2547/// Get the scrollbar width in pixels for a node
2548pub fn get_scrollbar_width_px(
2549    styled_dom: &StyledDom,
2550    node_id: NodeId,
2551    node_state: &StyledNodeState,
2552) -> f32 {
2553    let style = get_scrollbar_style(styled_dom, node_id, node_state);
2554    style.width_px
2555}
2556
2557/// Checks if text in a node is selectable based on CSS `user-select` property.
2558///
2559/// Returns `true` if the text can be selected (default behavior),
2560/// `false` if `user-select: none` is set.
2561pub fn is_text_selectable(
2562    styled_dom: &StyledDom,
2563    node_id: NodeId,
2564    node_state: &StyledNodeState,
2565) -> bool {
2566    let node_data = &styled_dom.node_data.as_container()[node_id];
2567    
2568    styled_dom
2569        .css_property_cache
2570        .ptr
2571        .get_user_select(node_data, &node_id, node_state)
2572        .and_then(|v| v.get_property())
2573        .map(|us| *us != StyleUserSelect::None)
2574        .unwrap_or(true) // Default: text is selectable
2575}
2576
2577/// Checks if a node has the `contenteditable` attribute set directly.
2578///
2579/// Returns `true` if:
2580/// - The node has `contenteditable: true` set via `.set_contenteditable(true)`
2581/// - OR the node has `contenteditable` attribute set to `true`
2582///
2583/// This does NOT check inheritance - use `is_node_contenteditable_inherited` for that.
2584pub fn is_node_contenteditable(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2585    use azul_core::dom::AttributeType;
2586    
2587    let node_data = &styled_dom.node_data.as_container()[node_id];
2588    
2589    // First check the direct contenteditable field (primary method)
2590    if node_data.is_contenteditable() {
2591        return true;
2592    }
2593    
2594    // Also check the attribute for backwards compatibility
2595    // Only return true if the attribute value is explicitly true
2596    node_data.attributes.as_ref().iter().any(|attr| {
2597        matches!(attr, AttributeType::ContentEditable(true))
2598    })
2599}
2600
2601/// W3C-conformant contenteditable inheritance check.
2602///
2603/// In the W3C model, the `contenteditable` attribute is **inherited**:
2604/// - A node is editable if it has `contenteditable="true"` set directly
2605/// - OR if its parent has `isContentEditable` as true
2606/// - UNLESS the node explicitly sets `contenteditable="false"`
2607///
2608/// This function traverses up the DOM tree to determine editability.
2609///
2610/// # Returns
2611///
2612/// - `true` if the node is editable (either directly or via inheritance)
2613/// - `false` if the node is not editable or has `contenteditable="false"`
2614///
2615/// # Example
2616///
2617/// ```html
2618/// <div contenteditable="true">
2619///   A                              <!-- editable (inherited) -->
2620///   <div contenteditable="false">
2621///     B                            <!-- NOT editable (explicitly false) -->
2622///   </div>
2623///   C                              <!-- editable (inherited) -->
2624/// </div>
2625/// ```
2626pub fn is_node_contenteditable_inherited(styled_dom: &StyledDom, node_id: NodeId) -> bool {
2627    use azul_core::dom::AttributeType;
2628    
2629    let node_data_container = styled_dom.node_data.as_container();
2630    let hierarchy = styled_dom.node_hierarchy.as_container();
2631    
2632    let mut current_node_id = Some(node_id);
2633    
2634    while let Some(nid) = current_node_id {
2635        let node_data = &node_data_container[nid];
2636        
2637        // First check the direct contenteditable field (set via set_contenteditable())
2638        // This takes precedence as it's the API-level setting
2639        if node_data.is_contenteditable() {
2640            return true;
2641        }
2642        
2643        // Then check for explicit contenteditable attribute on this node
2644        // This handles HTML-style contenteditable="true" or contenteditable="false"
2645        for attr in node_data.attributes.as_ref().iter() {
2646            if let AttributeType::ContentEditable(is_editable) = attr {
2647                // If explicitly set to true, node is editable
2648                // If explicitly set to false, node is NOT editable (blocks inheritance)
2649                return *is_editable;
2650            }
2651        }
2652        
2653        // No explicit setting on this node, check parent for inheritance
2654        current_node_id = hierarchy.get(nid).and_then(|h| h.parent_id());
2655    }
2656    
2657    // Reached root without finding contenteditable - not editable
2658    false
2659}
2660
2661/// Find the contenteditable ancestor of a node.
2662///
2663/// When focus lands on a text node inside a contenteditable container,
2664/// we need to find the actual container that has the `contenteditable` attribute.
2665///
2666/// # Returns
2667///
2668/// - `Some(node_id)` of the contenteditable ancestor (may be the node itself)
2669/// - `None` if no contenteditable ancestor exists
2670pub fn find_contenteditable_ancestor(styled_dom: &StyledDom, node_id: NodeId) -> Option<NodeId> {
2671    use azul_core::dom::AttributeType;
2672    
2673    let node_data_container = styled_dom.node_data.as_container();
2674    let hierarchy = styled_dom.node_hierarchy.as_container();
2675    
2676    let mut current_node_id = Some(node_id);
2677    
2678    while let Some(nid) = current_node_id {
2679        let node_data = &node_data_container[nid];
2680        
2681        // First check the direct contenteditable field (set via set_contenteditable())
2682        if node_data.is_contenteditable() {
2683            return Some(nid);
2684        }
2685        
2686        // Then check for contenteditable attribute on this node
2687        for attr in node_data.attributes.as_ref().iter() {
2688            if let AttributeType::ContentEditable(is_editable) = attr {
2689                if *is_editable {
2690                    return Some(nid);
2691                } else {
2692                    // Explicitly not editable - stop search
2693                    return None;
2694                }
2695            }
2696        }
2697        
2698        // Check parent
2699        current_node_id = hierarchy.get(nid).and_then(|h| h.parent_id());
2700    }
2701    
2702    None
2703}