Skip to main content

azul_core/
compact_cache_builder.rs

1//! Builder function to convert CssPropertyCache → CompactLayoutCache.
2//!
3//! Called once after restyle + apply_ua_css + compute_inherited_values.
4//! Uses typed getters on CssPropertyCache (which cascade through all sources)
5//! to resolve each property for the "normal" state (all pseudo-states = false).
6
7use crate::dom::{NodeData, NodeId};
8use crate::prop_cache::CssPropertyCache;
9
10use crate::styled_dom::StyledNodeState;
11use azul_css::compact_cache::*;
12use azul_css::css::CssPropertyValue;
13use azul_css::props::property::CssProperty;
14use azul_css::props::basic::length::SizeMetric;
15use azul_css::props::layout::dimensions::{LayoutHeight, LayoutWidth};
16use azul_css::props::layout::flex::LayoutFlexBasis;
17use azul_css::props::layout::position::LayoutZIndex;
18use core::hash::{Hash, Hasher};
19use std::collections::hash_map::DefaultHasher;
20
21impl CssPropertyCache {
22    /// Build a CompactLayoutCache from the current property cache state.
23    ///
24    /// Must be called after `restyle()`, `apply_ua_css()`, and `compute_inherited_values()`.
25    /// Resolves all layout-relevant properties for every node in the "normal" state
26    /// (no hover/active/focus) and encodes them into compact arrays.
27    ///
28    /// Tier 1/2/2b provide fast-path access for layout-hot properties.
29    /// Non-compact properties (background, transform, box-shadow, etc.) are
30    /// resolved via the slow cascade path in `get_property_slow()`.
31    ///
32    /// `prev_font_hashes` is the per-node font hash array from the previous frame.
33    /// When non-empty, each node's new `font_family_hash` is compared against the
34    /// previous value, and differing nodes are recorded in `font_dirty_nodes`.
35    /// On the first build (empty slice), ALL text nodes are marked dirty.
36    pub fn build_compact_cache(
37        &self,
38        node_data: &[NodeData],
39        prev_font_hashes: &[u64],
40    ) -> CompactLayoutCache {
41        let node_count = self.node_count;
42        let default_state = StyledNodeState::default();
43        let mut result = CompactLayoutCache::with_capacity(node_count);
44
45        for i in 0..node_count {
46            let node_id = NodeId::new(i);
47            let nd = &node_data[i];
48
49            // =====================================================================
50            // Tier 1: Encode all 20 enum properties into u64
51            // =====================================================================
52
53            let display = self
54                .get_display(nd, &node_id, &default_state)
55                .and_then(|v| v.get_property().copied())
56                .unwrap_or_default();
57            let position = self
58                .get_position(nd, &node_id, &default_state)
59                .and_then(|v| v.get_property().copied())
60                .unwrap_or_default();
61            let float = self
62                .get_float(nd, &node_id, &default_state)
63                .and_then(|v| v.get_property().copied())
64                .unwrap_or_default();
65            let overflow_x = self
66                .get_overflow_x(nd, &node_id, &default_state)
67                .and_then(|v| v.get_property().copied())
68                .unwrap_or_default();
69            let overflow_y = self
70                .get_overflow_y(nd, &node_id, &default_state)
71                .and_then(|v| v.get_property().copied())
72                .unwrap_or_default();
73            let box_sizing = self
74                .get_box_sizing(nd, &node_id, &default_state)
75                .and_then(|v| v.get_property().copied())
76                .unwrap_or_default();
77            let flex_direction = self
78                .get_flex_direction(nd, &node_id, &default_state)
79                .and_then(|v| v.get_property().copied())
80                .unwrap_or_default();
81            let flex_wrap = self
82                .get_flex_wrap(nd, &node_id, &default_state)
83                .and_then(|v| v.get_property().copied())
84                .unwrap_or_default();
85            let justify_content = self
86                .get_justify_content(nd, &node_id, &default_state)
87                .and_then(|v| v.get_property().copied())
88                .unwrap_or_default();
89            let align_items = self
90                .get_align_items(nd, &node_id, &default_state)
91                .and_then(|v| v.get_property().copied())
92                .unwrap_or_default();
93            let align_content = self
94                .get_align_content(nd, &node_id, &default_state)
95                .and_then(|v| v.get_property().copied())
96                .unwrap_or_default();
97            let writing_mode = self
98                .get_writing_mode(nd, &node_id, &default_state)
99                .and_then(|v| v.get_property().copied())
100                .unwrap_or_default();
101            let clear = self
102                .get_clear(nd, &node_id, &default_state)
103                .and_then(|v| v.get_property().copied())
104                .unwrap_or_default();
105            let font_weight = self
106                .get_font_weight(nd, &node_id, &default_state)
107                .and_then(|v| v.get_property().copied())
108                .unwrap_or_default();
109            let font_style = self
110                .get_font_style(nd, &node_id, &default_state)
111                .and_then(|v| v.get_property().copied())
112                .unwrap_or_default();
113            let text_align = self
114                .get_text_align(nd, &node_id, &default_state)
115                .and_then(|v| v.get_property().copied())
116                .unwrap_or_default();
117            let visibility = self
118                .get_visibility(nd, &node_id, &default_state)
119                .and_then(|v| v.get_property().copied())
120                .unwrap_or_default();
121            let white_space = self
122                .get_white_space(nd, &node_id, &default_state)
123                .and_then(|v| v.get_property().copied())
124                .unwrap_or_default();
125            let direction = self
126                .get_direction(nd, &node_id, &default_state)
127                .and_then(|v| v.get_property().copied())
128                .unwrap_or_default();
129            let vertical_align = self
130                .get_vertical_align(nd, &node_id, &default_state)
131                .and_then(|v| v.get_property().copied())
132                .unwrap_or_default();
133
134            let border_collapse = self
135                .get_border_collapse(nd, &node_id, &default_state)
136                .and_then(|v| v.get_property().copied())
137                .unwrap_or_default();
138
139            result.tier1_enums[i] = encode_tier1(
140                display,
141                position,
142                float,
143                overflow_x,
144                overflow_y,
145                box_sizing,
146                flex_direction,
147                flex_wrap,
148                justify_content,
149                align_items,
150                align_content,
151                writing_mode,
152                clear,
153                font_weight,
154                font_style,
155                text_align,
156                visibility,
157                white_space,
158                direction,
159                vertical_align,
160                border_collapse,
161            );
162
163            // =====================================================================
164            // Tier 2: Encode numeric dimension properties
165            // =====================================================================
166
167            // Width/Height are enums: Auto | Px(PixelValue) | MinContent | MaxContent | Calc
168            if let Some(val) = self.get_width(nd, &node_id, &default_state) {
169                result.tier2_dims[i].width = encode_layout_width(val);
170            }
171            if let Some(val) = self.get_height(nd, &node_id, &default_state) {
172                result.tier2_dims[i].height = encode_layout_height(val);
173            }
174
175            // Min/Max Width/Height are simple PixelValue wrappers
176            if let Some(val) = self.get_min_width(nd, &node_id, &default_state) {
177                result.tier2_dims[i].min_width = encode_pixel_prop(val);
178            }
179            if let Some(val) = self.get_max_width(nd, &node_id, &default_state) {
180                result.tier2_dims[i].max_width = encode_pixel_prop(val);
181            }
182            if let Some(val) = self.get_min_height(nd, &node_id, &default_state) {
183                result.tier2_dims[i].min_height = encode_pixel_prop(val);
184            }
185            if let Some(val) = self.get_max_height(nd, &node_id, &default_state) {
186                result.tier2_dims[i].max_height = encode_pixel_prop(val);
187            }
188
189            // Flex basis (enum: Auto | Exact(PixelValue))
190            if let Some(val) = self.get_flex_basis(nd, &node_id, &default_state) {
191                result.tier2_dims[i].flex_basis = encode_flex_basis(val);
192            }
193
194            // Font size
195            if let Some(val) = self.get_font_size(nd, &node_id, &default_state) {
196                result.tier2_dims[i].font_size = encode_pixel_prop(val);
197            }
198
199            // Padding (i16 × 10 resolved px)
200            if let Some(val) = self.get_padding_top(nd, &node_id, &default_state) {
201                result.tier2_dims[i].padding_top = encode_css_pixel_as_i16(val);
202            }
203            if let Some(val) = self.get_padding_right(nd, &node_id, &default_state) {
204                result.tier2_dims[i].padding_right = encode_css_pixel_as_i16(val);
205            }
206            if let Some(val) = self.get_padding_bottom(nd, &node_id, &default_state) {
207                result.tier2_dims[i].padding_bottom = encode_css_pixel_as_i16(val);
208            }
209            if let Some(val) = self.get_padding_left(nd, &node_id, &default_state) {
210                result.tier2_dims[i].padding_left = encode_css_pixel_as_i16(val);
211            }
212
213            // Margin (i16, auto is special)
214            if let Some(val) = self.get_margin_top(nd, &node_id, &default_state) {
215                result.tier2_dims[i].margin_top = encode_margin_i16(val);
216            }
217            if let Some(val) = self.get_margin_right(nd, &node_id, &default_state) {
218                result.tier2_dims[i].margin_right = encode_margin_i16(val);
219            }
220            if let Some(val) = self.get_margin_bottom(nd, &node_id, &default_state) {
221                result.tier2_dims[i].margin_bottom = encode_margin_i16(val);
222            }
223            if let Some(val) = self.get_margin_left(nd, &node_id, &default_state) {
224                result.tier2_dims[i].margin_left = encode_margin_i16(val);
225            }
226
227            // Border widths (i16 × 10 resolved px)
228            if let Some(val) = self.get_border_top_width(nd, &node_id, &default_state) {
229                result.tier2_dims[i].border_top_width = encode_css_pixel_as_i16(val);
230            }
231            if let Some(val) = self.get_border_right_width(nd, &node_id, &default_state) {
232                result.tier2_dims[i].border_right_width = encode_css_pixel_as_i16(val);
233            }
234            if let Some(val) = self.get_border_bottom_width(nd, &node_id, &default_state) {
235                result.tier2_dims[i].border_bottom_width = encode_css_pixel_as_i16(val);
236            }
237            if let Some(val) = self.get_border_left_width(nd, &node_id, &default_state) {
238                result.tier2_dims[i].border_left_width = encode_css_pixel_as_i16(val);
239            }
240
241            // Position offsets (top/right/bottom/left)
242            if let Some(val) = self.get_top(nd, &node_id, &default_state) {
243                result.tier2_dims[i].top = encode_css_pixel_as_i16(val);
244            }
245            if let Some(val) = self.get_right(nd, &node_id, &default_state) {
246                result.tier2_dims[i].right = encode_css_pixel_as_i16(val);
247            }
248            if let Some(val) = self.get_bottom(nd, &node_id, &default_state) {
249                result.tier2_dims[i].bottom = encode_css_pixel_as_i16(val);
250            }
251            if let Some(val) = self.get_left(nd, &node_id, &default_state) {
252                result.tier2_dims[i].left = encode_css_pixel_as_i16(val);
253            }
254
255            // Flex grow/shrink (u16 × 100)
256            if let Some(val) = self.get_flex_grow(nd, &node_id, &default_state) {
257                if let Some(exact) = val.get_property() {
258                    result.tier2_dims[i].flex_grow = encode_flex_u16(exact.inner.get());
259                }
260            }
261            if let Some(val) = self.get_flex_shrink(nd, &node_id, &default_state) {
262                if let Some(exact) = val.get_property() {
263                    result.tier2_dims[i].flex_shrink = encode_flex_u16(exact.inner.get());
264                }
265            }
266
267            // =====================================================================
268            // Tier 2 cold: Paint-only properties
269            // =====================================================================
270
271            // Z-index
272            if let Some(val) = self.get_z_index(nd, &node_id, &default_state) {
273                if let Some(exact) = val.get_property() {
274                    match exact {
275                        LayoutZIndex::Auto => result.tier2_cold[i].z_index = I16_AUTO,
276                        LayoutZIndex::Integer(z) => {
277                            if *z >= I16_SENTINEL_THRESHOLD as i32 {
278                                result.tier2_cold[i].z_index = I16_SENTINEL;
279                            } else {
280                                result.tier2_cold[i].z_index = *z as i16;
281                            }
282                        }
283                    }
284                }
285            }
286
287            // Border styles (packed into u16)
288            {
289                let bts = self.get_border_top_style(nd, &node_id, &default_state)
290                    .and_then(|v| v.get_property().copied())
291                    .map(|v| v.inner)
292                    .unwrap_or_default();
293                let brs = self.get_border_right_style(nd, &node_id, &default_state)
294                    .and_then(|v| v.get_property().copied())
295                    .map(|v| v.inner)
296                    .unwrap_or_default();
297                let bbs = self.get_border_bottom_style(nd, &node_id, &default_state)
298                    .and_then(|v| v.get_property().copied())
299                    .map(|v| v.inner)
300                    .unwrap_or_default();
301                let bls = self.get_border_left_style(nd, &node_id, &default_state)
302                    .and_then(|v| v.get_property().copied())
303                    .map(|v| v.inner)
304                    .unwrap_or_default();
305                result.tier2_cold[i].border_styles_packed =
306                    encode_border_styles_packed(bts, brs, bbs, bls);
307            }
308
309            // Border colors (ColorU → u32 as 0xRRGGBBAA)
310            if let Some(val) = self.get_border_top_color(nd, &node_id, &default_state) {
311                if let Some(color) = val.get_property() {
312                    result.tier2_cold[i].border_top_color = encode_color_u32(&color.inner);
313                }
314            }
315            if let Some(val) = self.get_border_right_color(nd, &node_id, &default_state) {
316                if let Some(color) = val.get_property() {
317                    result.tier2_cold[i].border_right_color = encode_color_u32(&color.inner);
318                }
319            }
320            if let Some(val) = self.get_border_bottom_color(nd, &node_id, &default_state) {
321                if let Some(color) = val.get_property() {
322                    result.tier2_cold[i].border_bottom_color = encode_color_u32(&color.inner);
323                }
324            }
325            if let Some(val) = self.get_border_left_color(nd, &node_id, &default_state) {
326                if let Some(color) = val.get_property() {
327                    result.tier2_cold[i].border_left_color = encode_color_u32(&color.inner);
328                }
329            }
330
331            // Border spacing (two PixelValue → i16 × 10 resolved px)
332            if let Some(val) = self.get_border_spacing(nd, &node_id, &default_state) {
333                if let Some(spacing) = val.get_property() {
334                    if spacing.horizontal.metric == SizeMetric::Px {
335                        result.tier2_cold[i].border_spacing_h = encode_resolved_px_i16(spacing.horizontal.number.get());
336                    }
337                    if spacing.vertical.metric == SizeMetric::Px {
338                        result.tier2_cold[i].border_spacing_v = encode_resolved_px_i16(spacing.vertical.number.get());
339                    }
340                }
341            }
342
343            // Tab size (PixelValue → i16 × 10 resolved px)
344            if let Some(val) = self.get_tab_size(nd, &node_id, &default_state) {
345                result.tier2_cold[i].tab_size = encode_css_pixel_as_i16(val);
346            }
347
348            // =====================================================================
349            // Tier 2b: Text properties
350            // =====================================================================
351
352            // Text color (ColorU → u32 as 0xRRGGBBAA)
353            if let Some(val) = self.get_text_color(nd, &node_id, &default_state) {
354                if let Some(color) = val.get_property() {
355                    let c = &color.inner;
356                    result.tier2b_text[i].text_color =
357                        ((c.r as u32) << 24) | ((c.g as u32) << 16) | ((c.b as u32) << 8) | (c.a as u32);
358                }
359            }
360
361            // Font-family (hash the whole StyleFontFamilyVec for fast comparison)
362            if let Some(val) = self.get_font_family(nd, &node_id, &default_state) {
363                if let Some(families) = val.get_property() {
364                    let mut hasher = DefaultHasher::new();
365                    families.hash(&mut hasher);
366                    let h = hasher.finish();
367                    let h = if h == 0 { 1 } else { h };
368                    result.tier2b_text[i].font_family_hash = h;
369                    result.font_hash_to_families.insert(h, families.clone());
370                }
371            }
372
373            // Line-height (PercentageValue: internal number is value × 1000, we store % × 10)
374            if let Some(val) = self.get_line_height(nd, &node_id, &default_state) {
375                if let Some(lh) = val.get_property() {
376                    // lh.inner is PercentageValue, normalized() = value/100.
377                    // Internal number = percentage × 1000 (e.g. 120% → 120_000).
378                    // We store percentage × 10 as i16 (e.g. 120% → 1200).
379                    let pct_x10 = (lh.inner.normalized() * 1000.0).round() as i32;
380                    if pct_x10 >= -32768 && pct_x10 < I16_SENTINEL_THRESHOLD as i32 {
381                        result.tier2b_text[i].line_height = pct_x10 as i16;
382                    } else {
383                        result.tier2b_text[i].line_height = I16_SENTINEL;
384                    }
385                }
386            }
387
388            // Letter-spacing (PixelValue wrapper → i16 × 10 resolved px)
389            if let Some(val) = self.get_letter_spacing(nd, &node_id, &default_state) {
390                result.tier2b_text[i].letter_spacing = encode_css_pixel_as_i16(val);
391            }
392
393            // Word-spacing (PixelValue wrapper → i16 × 10 resolved px)
394            if let Some(val) = self.get_word_spacing(nd, &node_id, &default_state) {
395                result.tier2b_text[i].word_spacing = encode_css_pixel_as_i16(val);
396            }
397
398            // Text-indent (PixelValue wrapper → i16 × 10 resolved px)
399            if let Some(val) = self.get_text_indent(nd, &node_id, &default_state) {
400                result.tier2b_text[i].text_indent = encode_css_pixel_as_i16(val);
401            }
402        }
403
404        // =====================================================================
405        // Per-node font dirty tracking (P4)
406        // Compare each node's font_family_hash against the previous frame's hash.
407        // Nodes whose hash changed are recorded in font_dirty_nodes for
408        // incremental font chain re-resolution instead of all-or-nothing.
409        // =====================================================================
410        result.font_dirty_nodes.clear();
411        for i in 0..node_count {
412            let new_hash = result.tier2b_text[i].font_family_hash;
413            let old_hash = prev_font_hashes.get(i).copied().unwrap_or(0);
414            if new_hash != old_hash {
415                result.font_dirty_nodes.push(i);
416            }
417        }
418        // Save current hashes as prev_font_hashes for next frame comparison
419        result.prev_font_hashes = result.tier2b_text.iter().map(|t| t.font_family_hash).collect();
420
421        result
422    }
423
424    /// Build compact cache with inheritance in a single pass.
425    ///
426    /// Replaces the separate `compute_inherited_values()` + `build_compact_cache()` calls.
427    /// For each node (in DOM index order, which is pre-order = parents before children):
428    ///   1. Copy parent's compact values for INHERITABLE properties
429    ///   2. Apply this node's CSS properties on top (from css_props + inline + UA)
430    ///   3. Write directly to compact arrays
431    ///
432    /// This eliminates 50K Vec clones from compute_inherited_values and
433    /// avoids re-reading properties from 5 separate data structures.
434    pub fn build_compact_cache_with_inheritance(
435        &self,
436        node_data: &[NodeData],
437        node_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
438        prev_font_hashes: &[u64],
439    ) -> CompactLayoutCache {
440        self.build_compact_cache_with_inheritance_debug(node_data, node_hierarchy, prev_font_hashes, &mut None)
441    }
442
443    /// Same as `build_compact_cache_with_inheritance` but with optional debug logging.
444    pub fn build_compact_cache_with_inheritance_debug(
445        &self,
446        node_data: &[NodeData],
447        node_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
448        prev_font_hashes: &[u64],
449        debug_messages: &mut Option<Vec<azul_css::LayoutDebugMessage>>,
450    ) -> CompactLayoutCache {
451        let node_count = self.node_count;
452        let default_state = StyledNodeState::default();
453        let mut result = CompactLayoutCache::with_capacity(node_count);
454
455        // Pre-encode global CSS properties (from `*` rules) into compact form.
456        // These are applied as baseline for every node before inheritance.
457        let mut global_tier1: u64 = 0;
458        let mut global_dims = CompactNodeProps::default();
459        let mut global_cold = CompactNodePropsCold::default();
460        let mut global_text = CompactTextProps::default();
461        let has_global = !self.global_css_props.is_empty();
462
463        if has_global {
464            use azul_css::props::property::CssProperty;
465
466            for prop in &self.global_css_props {
467                // Apply each global property to the pre-encoded compact values
468                macro_rules! global_tier1_enum {
469                    ($variant:ident, $shift:ident, $mask:ident, $encoder:ident) => {
470                        if let CssProperty::$variant(v) = prop {
471                            if let Some(exact) = v.get_property() {
472                                let encoded = $encoder(*exact) as u64;
473                                let shifted_mask = $mask << $shift;
474                                global_tier1 = (global_tier1 & !shifted_mask) | ((encoded & $mask) << $shift);
475                            }
476                        }
477                    };
478                }
479
480                global_tier1_enum!(Display, DISPLAY_SHIFT, DISPLAY_MASK, layout_display_to_u8);
481                global_tier1_enum!(Position, POSITION_SHIFT, POSITION_MASK, layout_position_to_u8);
482                global_tier1_enum!(Float, FLOAT_SHIFT, FLOAT_MASK, layout_float_to_u8);
483                global_tier1_enum!(OverflowX, OVERFLOW_X_SHIFT, OVERFLOW_MASK, layout_overflow_to_u8);
484                global_tier1_enum!(OverflowY, OVERFLOW_Y_SHIFT, OVERFLOW_MASK, layout_overflow_to_u8);
485                global_tier1_enum!(BoxSizing, BOX_SIZING_SHIFT, BOX_SIZING_MASK, layout_box_sizing_to_u8);
486                global_tier1_enum!(FlexDirection, FLEX_DIRECTION_SHIFT, FLEX_DIR_MASK, layout_flex_direction_to_u8);
487                global_tier1_enum!(FlexWrap, FLEX_WRAP_SHIFT, FLEX_WRAP_MASK, layout_flex_wrap_to_u8);
488                global_tier1_enum!(JustifyContent, JUSTIFY_CONTENT_SHIFT, JUSTIFY_MASK, layout_justify_content_to_u8);
489                global_tier1_enum!(AlignItems, ALIGN_ITEMS_SHIFT, ALIGN_MASK, layout_align_items_to_u8);
490                global_tier1_enum!(AlignContent, ALIGN_CONTENT_SHIFT, ALIGN_MASK, layout_align_content_to_u8);
491                global_tier1_enum!(Clear, CLEAR_SHIFT, CLEAR_MASK, layout_clear_to_u8);
492                global_tier1_enum!(Visibility, VISIBILITY_SHIFT, VISIBILITY_MASK, style_visibility_to_u8);
493                global_tier1_enum!(WritingMode, WRITING_MODE_SHIFT, WRITING_MODE_MASK, layout_writing_mode_to_u8);
494                global_tier1_enum!(FontWeight, FONT_WEIGHT_SHIFT, FONT_WEIGHT_MASK, style_font_weight_to_u8);
495                global_tier1_enum!(FontStyle, FONT_STYLE_SHIFT, FONT_STYLE_MASK, style_font_style_to_u8);
496                global_tier1_enum!(TextAlign, TEXT_ALIGN_SHIFT, TEXT_ALIGN_MASK, style_text_align_to_u8);
497                global_tier1_enum!(WhiteSpace, WHITE_SPACE_SHIFT, WHITE_SPACE_MASK, style_white_space_to_u8);
498                global_tier1_enum!(Direction, DIRECTION_SHIFT, DIRECTION_MASK, style_direction_to_u8);
499                global_tier1_enum!(VerticalAlign, VERTICAL_ALIGN_SHIFT, VERTICAL_ALIGN_MASK, style_vertical_align_to_u8);
500                global_tier1_enum!(BorderCollapse, BORDER_COLLAPSE_SHIFT, BORDER_COLLAPSE_MASK, border_collapse_to_u8);
501
502                // Tier 2 dims
503                match prop {
504                    CssProperty::PaddingTop(v) => { global_dims.padding_top = encode_css_pixel_as_i16(v); }
505                    CssProperty::PaddingRight(v) => { global_dims.padding_right = encode_css_pixel_as_i16(v); }
506                    CssProperty::PaddingBottom(v) => { global_dims.padding_bottom = encode_css_pixel_as_i16(v); }
507                    CssProperty::PaddingLeft(v) => { global_dims.padding_left = encode_css_pixel_as_i16(v); }
508                    CssProperty::MarginTop(v) => { global_dims.margin_top = encode_margin_i16(v); }
509                    CssProperty::MarginRight(v) => { global_dims.margin_right = encode_margin_i16(v); }
510                    CssProperty::MarginBottom(v) => { global_dims.margin_bottom = encode_margin_i16(v); }
511                    CssProperty::MarginLeft(v) => { global_dims.margin_left = encode_margin_i16(v); }
512                    CssProperty::Width(v) => { global_dims.width = encode_layout_width(v); }
513                    CssProperty::Height(v) => { global_dims.height = encode_layout_height(v); }
514                    CssProperty::FontSize(v) => { global_dims.font_size = encode_pixel_prop(v); }
515                    CssProperty::BorderTopWidth(v) => { global_dims.border_top_width = encode_css_pixel_as_i16(v); }
516                    CssProperty::BorderRightWidth(v) => { global_dims.border_right_width = encode_css_pixel_as_i16(v); }
517                    CssProperty::BorderBottomWidth(v) => { global_dims.border_bottom_width = encode_css_pixel_as_i16(v); }
518                    CssProperty::BorderLeftWidth(v) => { global_dims.border_left_width = encode_css_pixel_as_i16(v); }
519                    _ => {}
520                }
521            }
522
523            if global_tier1 != 0 {
524                global_tier1 |= TIER1_POPULATED_BIT;
525            }
526        }
527
528        // Helper: push debug message if debug_messages is Some
529        macro_rules! cascade_debug {
530            ($($arg:tt)*) => {
531                if let Some(ref mut msgs) = debug_messages {
532                    msgs.push(azul_css::LayoutDebugMessage::css_getter(format!($($arg)*)));
533                }
534            };
535        }
536
537        for i in 0..node_count {
538            let node_id = NodeId::new(i);
539            let nd = &node_data[i];
540
541            // Step 0: Apply UA CSS defaults first (lowest priority).
542            // Then global `*` rules override UA (higher priority).
543            // Then per-node CSS (Step 3) overrides both.
544            //
545            // CSS cascade priority: UA < author `*` < author specific < inline
546
547            // Step 1: Inherit from parent's COMPACT values (not computed_values)
548            // Parent index is always < i in pre-order arena, so already computed.
549            //
550            // Step 1: Inherit ONLY inheritable CSS properties from parent.
551            // Non-inheritable fields (display, position, float, overflow, box-sizing,
552            // flex-*, clear, vertical-align, writing-mode) stay at 0 (CSS initial value).
553            // They get set by UA CSS (Step 2) and author CSS (Step 3).
554            const INHERITABLE_TIER1_MASK: u64 =
555                (FONT_WEIGHT_MASK << FONT_WEIGHT_SHIFT)
556                | (FONT_STYLE_MASK << FONT_STYLE_SHIFT)
557                | (TEXT_ALIGN_MASK << TEXT_ALIGN_SHIFT)
558                | (VISIBILITY_MASK << VISIBILITY_SHIFT)
559                | (WHITE_SPACE_MASK << WHITE_SPACE_SHIFT)
560                | (DIRECTION_MASK << DIRECTION_SHIFT)
561                | (BORDER_COLLAPSE_MASK << BORDER_COLLAPSE_SHIFT);
562
563            let parent_id = node_hierarchy[i].parent_id();
564            if let Some(pid) = parent_id {
565                let pi = pid.index();
566
567                // Copy only inheritable tier1 fields from parent
568                result.tier1_enums[i] = result.tier1_enums[pi] & INHERITABLE_TIER1_MASK;
569
570                // Inheritable tier2: font_size
571                result.tier2_dims[i].font_size = result.tier2_dims[pi].font_size;
572
573                // Inheritable tier2_cold: border_spacing, tab_size
574                result.tier2_cold[i].border_spacing_h = result.tier2_cold[pi].border_spacing_h;
575                result.tier2_cold[i].border_spacing_v = result.tier2_cold[pi].border_spacing_v;
576                result.tier2_cold[i].tab_size = result.tier2_cold[pi].tab_size;
577
578                // Inheritable tier2b: all text properties
579                result.tier2b_text[i] = result.tier2b_text[pi];
580            }
581
582            {
583                let d = &result.tier2_dims[i];
584                cascade_debug!("node[{}] {:?} after-inherit: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={} w={} h={}",
585                    i, nd.node_type, d.padding_top, d.padding_bottom, d.padding_left, d.padding_right,
586                    d.margin_top, d.margin_bottom, d.margin_left, d.margin_right, d.width, d.height);
587            }
588
589            // Step 2: Apply UA CSS defaults for this node type directly to compact values.
590            // UA defaults have lowest cascade priority — overridden by author CSS below.
591            apply_ua_css_to_compact(
592                &nd.node_type,
593                &mut result.tier1_enums[i],
594                &mut result.tier2_dims[i],
595                &mut result.tier2_cold[i],
596                &mut result.tier2b_text[i],
597                &mut result.font_hash_to_families,
598            );
599
600            {
601                let d = &result.tier2_dims[i];
602                cascade_debug!("node[{}] {:?} after-UA: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={}",
603                    i, nd.node_type, d.padding_top, d.padding_bottom, d.padding_left, d.padding_right,
604                    d.margin_top, d.margin_bottom, d.margin_left, d.margin_right);
605            }
606
607            // Step 2.5: Apply global `*` author CSS (overrides UA, overridden by specific rules)
608            // Apply each `*` rule property individually (not bulk-assign) so we only
609            // override properties the `*` rule actually set, preserving UA CSS for others.
610            //
611            // Per CSS spec, `*` matches all ELEMENTS. Text nodes are not elements —
612            // they must only inherit from their parent. Without this check, `* { color: #666 }`
613            // would overwrite the inherited `color: red` on a Text child of `<p>`,
614            // even though `<p>` correctly got red from `p { color: red }`.
615            if !nd.is_text_node() {
616                for prop in self.global_css_props.iter() {
617                    apply_css_property_to_compact(
618                        prop,
619                        &mut result.tier1_enums[i],
620                        &mut result.tier2_dims[i],
621                        &mut result.tier2_cold[i],
622                        &mut result.tier2b_text[i],
623                        &mut result.font_hash_to_families,
624                    );
625                    update_dom_declared_flags(prop, &mut result.dom_declared_flags);
626                }
627            }
628
629            {
630                let d = &result.tier2_dims[i];
631                cascade_debug!("node[{}] {:?} after-global-star: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={}",
632                    i, nd.node_type, d.padding_top, d.padding_bottom, d.padding_left, d.padding_right,
633                    d.margin_top, d.margin_bottom, d.margin_left, d.margin_right);
634                let n_props = self.css_props.get_slice(i).len();
635                let n_inline = nd.style.iter_inline_properties().count();
636                cascade_debug!("node[{}] css_props={} entries, inline={} entries", i, n_props, n_inline);
637                for prop in self.css_props.get_slice(i) {
638                    cascade_debug!("node[{}]   css_prop: state={:?} type={:?}", i, prop.state, prop.prop_type);
639                }
640            }
641
642            // Step 3: Apply this node's CSS properties directly to compact values.
643            // Per-node author CSS has higher specificity than global `*`.
644
645            // Scan css_props (stylesheet rules, sorted by (state, prop_type))
646            // Typically 5-15 entries per node. Only Normal state matters for layout.
647            for prop in self.css_props.get_slice(i) {
648                if prop.state != azul_css::dynamic_selector::PseudoStateType::Normal { continue; }
649                apply_css_property_to_compact(
650                    &prop.property,
651                    &mut result.tier1_enums[i],
652                    &mut result.tier2_dims[i],
653                    &mut result.tier2_cold[i],
654                    &mut result.tier2b_text[i],
655                    &mut result.font_hash_to_families,
656                );
657                update_dom_declared_flags(&prop.property, &mut result.dom_declared_flags);
658            }
659
660            {
661                let d = &result.tier2_dims[i];
662                cascade_debug!("node[{}] {:?} after-css-props: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={}",
663                    i, nd.node_type, d.padding_top, d.padding_bottom, d.padding_left, d.padding_right,
664                    d.margin_top, d.margin_bottom, d.margin_left, d.margin_right);
665            }
666
667            // Scan inline CSS (node_data.style — typically 0-3 properties).
668            // Inline CSS has highest specificity — applied last to override stylesheet.
669            for (prop, conds) in nd.style.iter_inline_properties() {
670                // Only apply Normal state (no pseudo-selectors like :hover)
671                let is_normal = conds.as_slice().is_empty()
672                    || conds.as_slice().iter().all(|c|
673                        matches!(c, azul_css::dynamic_selector::DynamicSelector::PseudoState(
674                            azul_css::dynamic_selector::PseudoStateType::Normal
675                        ))
676                    );
677                if !is_normal { continue; }
678                apply_css_property_to_compact(
679                    prop,
680                    &mut result.tier1_enums[i],
681                    &mut result.tier2_dims[i],
682                    &mut result.tier2_cold[i],
683                    &mut result.tier2b_text[i],
684                    &mut result.font_hash_to_families,
685                );
686                update_dom_declared_flags(prop, &mut result.dom_declared_flags);
687            }
688
689            // Resolve font-size from em/percent/pt/etc. to px.
690            // CSS 2.1: inherited font-size is the COMPUTED (px) value, not the specified value.
691            // Pre-order traversal guarantees parent's font_size is already resolved.
692            resolve_font_size_to_px(
693                &mut result.tier2_dims,
694                i,
695                parent_id,
696            );
697
698            // Set populated bit
699            if result.tier1_enums[i] != 0 {
700                result.tier1_enums[i] |= TIER1_POPULATED_BIT;
701            }
702        }
703
704        // Font dirty tracking.
705        // When prev_font_hashes is empty (first build for this DOM), mark ALL
706        // text nodes dirty to force font resolution. Without this, a DOM with
707        // no explicit font-family (all hashes 0) would compare 0==0 and skip
708        // resolution, even though font-weight/font-style may differ from the
709        // cached chains of a previous DOM.
710        result.font_dirty_nodes.clear();
711        let first_build = prev_font_hashes.is_empty();
712        for i in 0..node_count {
713            let new_hash = result.tier2b_text[i].font_family_hash;
714            let old_hash = prev_font_hashes.get(i).copied().unwrap_or(0);
715            if first_build || new_hash != old_hash {
716                result.font_dirty_nodes.push(i);
717            }
718        }
719        result.prev_font_hashes = result.tier2b_text.iter().map(|t| t.font_family_hash).collect();
720
721        result
722    }
723}
724
725// =============================================================================
726// Helpers extracted from build_compact_cache_with_inheritance_debug
727// =============================================================================
728
729/// Apply UA CSS defaults for a node type directly to compact values.
730/// UA defaults have lowest cascade priority — overridden by author CSS.
731fn apply_ua_css_to_compact(
732    node_type: &crate::dom::NodeType,
733    tier1: &mut u64,
734    dims: &mut CompactNodeProps,
735    cold: &mut CompactNodePropsCold,
736    text: &mut CompactTextProps,
737    font_hash_map: &mut alloc::collections::BTreeMap<u64, azul_css::props::basic::font::StyleFontFamilyVec>,
738) {
739    use azul_css::props::property::CssPropertyType as PT2;
740    const UA_PROPERTY_TYPES: &[PT2] = &[
741        // Tier1 enum properties
742        PT2::Display, PT2::Position, PT2::Float, PT2::Clear,
743        PT2::OverflowX, PT2::OverflowY, PT2::BoxSizing,
744        PT2::FlexDirection, PT2::FlexWrap, PT2::JustifyContent,
745        PT2::AlignItems, PT2::AlignContent, PT2::WritingMode,
746        PT2::FontWeight, PT2::FontStyle, PT2::TextAlign,
747        PT2::Visibility, PT2::WhiteSpace, PT2::Direction,
748        PT2::VerticalAlign, PT2::BorderCollapse,
749        // Tier2 dimension properties
750        PT2::Width, PT2::Height, PT2::FontSize,
751        PT2::MarginTop, PT2::MarginBottom, PT2::MarginLeft, PT2::MarginRight,
752        PT2::PaddingTop, PT2::PaddingBottom, PT2::PaddingLeft, PT2::PaddingRight,
753        PT2::BorderTopWidth, PT2::BorderTopStyle, PT2::BorderTopColor,
754        PT2::BorderRightWidth, PT2::BorderRightStyle, PT2::BorderRightColor,
755        PT2::BorderBottomWidth, PT2::BorderBottomStyle, PT2::BorderBottomColor,
756        PT2::BorderLeftWidth, PT2::BorderLeftStyle, PT2::BorderLeftColor,
757        // Text properties
758        PT2::TextColor, PT2::LineHeight, PT2::LetterSpacing, PT2::WordSpacing,
759        PT2::TextDecoration, PT2::Cursor, PT2::ListStyleType,
760    ];
761    for pt in UA_PROPERTY_TYPES {
762        if let Some(ua_prop) = crate::ua_css::get_ua_property(node_type, *pt) {
763            apply_css_property_to_compact(ua_prop, tier1, dims, cold, text, font_hash_map);
764        }
765    }
766}
767
768/// Resolve a node's font-size from relative units (em, %, rem, pt) to absolute px.
769/// CSS 2.1: inherited font-size is the COMPUTED (px) value, not the specified value.
770/// Pre-order traversal guarantees parent's font_size is already resolved.
771fn resolve_font_size_to_px(
772    tier2_dims: &mut [CompactNodeProps],
773    node_idx: usize,
774    parent_id: Option<NodeId>,
775) {
776    let raw_fs = tier2_dims[node_idx].font_size;
777    if raw_fs == U32_SENTINEL || raw_fs >= U32_SENTINEL_THRESHOLD {
778        return;
779    }
780    let pv = match decode_pixel_value_u32(raw_fs) {
781        Some(pv) if pv.metric != SizeMetric::Px => pv,
782        _ => return,
783    };
784
785    let parent_font_size_px = parent_id
786        .map(|pid| {
787            decode_pixel_value_u32(tier2_dims[pid.index()].font_size)
788                .map(|ppv| ppv.number.get())
789                .unwrap_or(16.0)
790        })
791        .unwrap_or(16.0);
792
793    let resolved_px = match pv.metric {
794        SizeMetric::Em => pv.number.get() * parent_font_size_px,
795        SizeMetric::Percent => pv.number.get() / 100.0 * parent_font_size_px,
796        SizeMetric::Rem => {
797            decode_pixel_value_u32(tier2_dims[0].font_size)
798                .map(|rpv| rpv.number.get())
799                .unwrap_or(16.0)
800                * pv.number.get()
801        }
802        SizeMetric::Pt => pv.number.get() * 96.0 / 72.0,
803        _ => pv.number.get(),
804    };
805    tier2_dims[node_idx].font_size =
806        encode_pixel_value_u32(&azul_css::props::basic::pixel::PixelValue::px(resolved_px));
807}
808
809// =============================================================================
810// Direct CssProperty → compact field writer
811// =============================================================================
812
813/// Apply a single CssProperty directly to the compact representation.
814/// Called once per property per node — replaces the old 56+ getter approach.
815#[inline]
816fn apply_css_property_to_compact(
817    prop: &CssProperty,
818    tier1: &mut u64,
819    dims: &mut CompactNodeProps,
820    cold: &mut CompactNodePropsCold,
821    text: &mut CompactTextProps,
822    font_hash_map: &mut alloc::collections::BTreeMap<u64, azul_css::props::basic::font::StyleFontFamilyVec>,
823) {
824    macro_rules! set_tier1 {
825        ($v:expr, $shift:expr, $mask:expr, $encoder:ident) => {
826            if let Some(exact) = $v.get_property() {
827                let encoded = $encoder(*exact) as u64;
828                let shifted_mask = $mask << $shift;
829                *tier1 = (*tier1 & !shifted_mask) | ((encoded & $mask) << $shift);
830            }
831        };
832    }
833
834    match prop {
835        // Tier 1 enums
836        CssProperty::Display(v) => set_tier1!(v, DISPLAY_SHIFT, DISPLAY_MASK, layout_display_to_u8),
837        CssProperty::Position(v) => set_tier1!(v, POSITION_SHIFT, POSITION_MASK, layout_position_to_u8),
838        CssProperty::Float(v) => set_tier1!(v, FLOAT_SHIFT, FLOAT_MASK, layout_float_to_u8),
839        CssProperty::OverflowX(v) => set_tier1!(v, OVERFLOW_X_SHIFT, OVERFLOW_MASK, layout_overflow_to_u8),
840        CssProperty::OverflowY(v) => set_tier1!(v, OVERFLOW_Y_SHIFT, OVERFLOW_MASK, layout_overflow_to_u8),
841        CssProperty::BoxSizing(v) => set_tier1!(v, BOX_SIZING_SHIFT, BOX_SIZING_MASK, layout_box_sizing_to_u8),
842        CssProperty::FlexDirection(v) => set_tier1!(v, FLEX_DIRECTION_SHIFT, FLEX_DIR_MASK, layout_flex_direction_to_u8),
843        CssProperty::FlexWrap(v) => set_tier1!(v, FLEX_WRAP_SHIFT, FLEX_WRAP_MASK, layout_flex_wrap_to_u8),
844        CssProperty::JustifyContent(v) => set_tier1!(v, JUSTIFY_CONTENT_SHIFT, JUSTIFY_MASK, layout_justify_content_to_u8),
845        CssProperty::AlignItems(v) => set_tier1!(v, ALIGN_ITEMS_SHIFT, ALIGN_MASK, layout_align_items_to_u8),
846        CssProperty::AlignContent(v) => set_tier1!(v, ALIGN_CONTENT_SHIFT, ALIGN_MASK, layout_align_content_to_u8),
847        CssProperty::WritingMode(v) => set_tier1!(v, WRITING_MODE_SHIFT, WRITING_MODE_MASK, layout_writing_mode_to_u8),
848        CssProperty::Clear(v) => set_tier1!(v, CLEAR_SHIFT, CLEAR_MASK, layout_clear_to_u8),
849        CssProperty::FontWeight(v) => set_tier1!(v, FONT_WEIGHT_SHIFT, FONT_WEIGHT_MASK, style_font_weight_to_u8),
850        CssProperty::FontStyle(v) => set_tier1!(v, FONT_STYLE_SHIFT, FONT_STYLE_MASK, style_font_style_to_u8),
851        CssProperty::TextAlign(v) => set_tier1!(v, TEXT_ALIGN_SHIFT, TEXT_ALIGN_MASK, style_text_align_to_u8),
852        CssProperty::Visibility(v) => set_tier1!(v, VISIBILITY_SHIFT, VISIBILITY_MASK, style_visibility_to_u8),
853        CssProperty::WhiteSpace(v) => set_tier1!(v, WHITE_SPACE_SHIFT, WHITE_SPACE_MASK, style_white_space_to_u8),
854        CssProperty::Direction(v) => set_tier1!(v, DIRECTION_SHIFT, DIRECTION_MASK, style_direction_to_u8),
855        CssProperty::VerticalAlign(v) => set_tier1!(v, VERTICAL_ALIGN_SHIFT, VERTICAL_ALIGN_MASK, style_vertical_align_to_u8),
856        CssProperty::BorderCollapse(v) => set_tier1!(v, BORDER_COLLAPSE_SHIFT, BORDER_COLLAPSE_MASK, border_collapse_to_u8),
857        CssProperty::AlignSelf(v) => set_tier1!(v, ALIGN_SELF_SHIFT, ALIGN_SELF_MASK, layout_align_self_to_u8),
858        CssProperty::JustifySelf(v) => set_tier1!(v, JUSTIFY_SELF_SHIFT, JUSTIFY_SELF_MASK, layout_justify_self_to_u8),
859        CssProperty::GridAutoFlow(v) => set_tier1!(v, GRID_AUTO_FLOW_SHIFT, GRID_AUTO_FLOW_MASK, layout_grid_auto_flow_to_u8),
860        CssProperty::JustifyItems(v) => set_tier1!(v, JUSTIFY_ITEMS_SHIFT, JUSTIFY_ITEMS_MASK, layout_justify_items_to_u8),
861
862        // Tier 2 dimensions
863        CssProperty::Width(v) => { dims.width = encode_layout_width(v); }
864        CssProperty::Height(v) => { dims.height = encode_layout_height(v); }
865        CssProperty::MinWidth(v) => { dims.min_width = encode_pixel_prop(v); }
866        CssProperty::MaxWidth(v) => { dims.max_width = encode_pixel_prop(v); }
867        CssProperty::MinHeight(v) => { dims.min_height = encode_pixel_prop(v); }
868        CssProperty::MaxHeight(v) => { dims.max_height = encode_pixel_prop(v); }
869        CssProperty::FlexBasis(v) => { dims.flex_basis = encode_flex_basis(v); }
870        CssProperty::FontSize(v) => { dims.font_size = encode_pixel_prop(v); }
871        CssProperty::PaddingTop(v) => { dims.padding_top = encode_css_pixel_as_i16(v); }
872        CssProperty::PaddingRight(v) => { dims.padding_right = encode_css_pixel_as_i16(v); }
873        CssProperty::PaddingBottom(v) => { dims.padding_bottom = encode_css_pixel_as_i16(v); }
874        CssProperty::PaddingLeft(v) => { dims.padding_left = encode_css_pixel_as_i16(v); }
875        CssProperty::MarginTop(v) => { dims.margin_top = encode_margin_i16(v); }
876        CssProperty::MarginRight(v) => { dims.margin_right = encode_margin_i16(v); }
877        CssProperty::MarginBottom(v) => { dims.margin_bottom = encode_margin_i16(v); }
878        CssProperty::MarginLeft(v) => { dims.margin_left = encode_margin_i16(v); }
879        CssProperty::BorderTopWidth(v) => { dims.border_top_width = encode_css_pixel_as_i16(v); }
880        CssProperty::BorderRightWidth(v) => { dims.border_right_width = encode_css_pixel_as_i16(v); }
881        CssProperty::BorderBottomWidth(v) => { dims.border_bottom_width = encode_css_pixel_as_i16(v); }
882        CssProperty::BorderLeftWidth(v) => { dims.border_left_width = encode_css_pixel_as_i16(v); }
883        CssProperty::Top(v) => { dims.top = encode_css_pixel_as_i16(v); }
884        CssProperty::Right(v) => { dims.right = encode_css_pixel_as_i16(v); }
885        CssProperty::Bottom(v) => { dims.bottom = encode_css_pixel_as_i16(v); }
886        CssProperty::Left(v) => { dims.left = encode_css_pixel_as_i16(v); }
887        CssProperty::FlexGrow(v) => {
888            if let Some(exact) = v.get_property() {
889                dims.flex_grow = encode_flex_u16(exact.inner.get());
890            }
891        }
892        CssProperty::FlexShrink(v) => {
893            if let Some(exact) = v.get_property() {
894                dims.flex_shrink = encode_flex_u16(exact.inner.get());
895            }
896        }
897
898        CssProperty::RowGap(v) => {
899            if let Some(g) = v.get_property() {
900                if g.inner.metric == SizeMetric::Px {
901                    dims.row_gap = encode_resolved_px_i16(g.inner.number.get());
902                }
903            }
904        }
905        CssProperty::ColumnGap(v) => {
906            if let Some(g) = v.get_property() {
907                if g.inner.metric == SizeMetric::Px {
908                    dims.column_gap = encode_resolved_px_i16(g.inner.number.get());
909                }
910            }
911        }
912        CssProperty::Gap(v) => {
913            if let Some(g) = v.get_property() {
914                if g.inner.metric == SizeMetric::Px {
915                    let enc = encode_resolved_px_i16(g.inner.number.get());
916                    dims.row_gap = enc;
917                    dims.column_gap = enc;
918                }
919            }
920        }
921
922        // Grid placement (compact encoding for common Auto/Line cases)
923        CssProperty::GridColumn(v) => {
924            if let Some(gp) = v.get_property() {
925                cold.grid_col_start = encode_grid_line(&gp.grid_start);
926                cold.grid_col_end = encode_grid_line(&gp.grid_end);
927            }
928        }
929        CssProperty::GridRow(v) => {
930            if let Some(gp) = v.get_property() {
931                cold.grid_row_start = encode_grid_line(&gp.grid_start);
932                cold.grid_row_end = encode_grid_line(&gp.grid_end);
933            }
934        }
935
936        // Tier 2 cold
937        CssProperty::ZIndex(v) => {
938            if let Some(exact) = v.get_property() {
939                match exact {
940                    LayoutZIndex::Auto => cold.z_index = I16_AUTO,
941                    LayoutZIndex::Integer(z) => {
942                        cold.z_index = if *z >= I16_SENTINEL_THRESHOLD as i32 { I16_SENTINEL } else { *z as i16 };
943                    }
944                }
945            }
946        }
947        CssProperty::BorderTopStyle(v) => {
948            if let Some(exact) = v.get_property() {
949                let bs = border_style_to_u8(exact.inner) as u16;
950                cold.border_styles_packed = (cold.border_styles_packed & !0x000F) | bs;
951            }
952        }
953        CssProperty::BorderRightStyle(v) => {
954            if let Some(exact) = v.get_property() {
955                let bs = border_style_to_u8(exact.inner) as u16;
956                cold.border_styles_packed = (cold.border_styles_packed & !0x00F0) | (bs << 4);
957            }
958        }
959        CssProperty::BorderBottomStyle(v) => {
960            if let Some(exact) = v.get_property() {
961                let bs = border_style_to_u8(exact.inner) as u16;
962                cold.border_styles_packed = (cold.border_styles_packed & !0x0F00) | (bs << 8);
963            }
964        }
965        CssProperty::BorderLeftStyle(v) => {
966            if let Some(exact) = v.get_property() {
967                let bs = border_style_to_u8(exact.inner) as u16;
968                cold.border_styles_packed = (cold.border_styles_packed & !0xF000) | (bs << 12);
969            }
970        }
971        CssProperty::BorderTopColor(v) => {
972            if let Some(c) = v.get_property() { cold.border_top_color = encode_color_u32(&c.inner); }
973        }
974        CssProperty::BorderRightColor(v) => {
975            if let Some(c) = v.get_property() { cold.border_right_color = encode_color_u32(&c.inner); }
976        }
977        CssProperty::BorderBottomColor(v) => {
978            if let Some(c) = v.get_property() { cold.border_bottom_color = encode_color_u32(&c.inner); }
979        }
980        CssProperty::BorderLeftColor(v) => {
981            if let Some(c) = v.get_property() { cold.border_left_color = encode_color_u32(&c.inner); }
982        }
983        CssProperty::BorderSpacing(v) => {
984            if let Some(spacing) = v.get_property() {
985                if spacing.horizontal.metric == SizeMetric::Px {
986                    cold.border_spacing_h = encode_resolved_px_i16(spacing.horizontal.number.get());
987                }
988                if spacing.vertical.metric == SizeMetric::Px {
989                    cold.border_spacing_v = encode_resolved_px_i16(spacing.vertical.number.get());
990                }
991            }
992        }
993        CssProperty::TabSize(v) => { cold.tab_size = encode_css_pixel_as_i16(v); }
994
995        // Tier 2b text
996        CssProperty::TextColor(v) => {
997            if let Some(color) = v.get_property() {
998                let c = &color.inner;
999                text.text_color = ((c.r as u32) << 24) | ((c.g as u32) << 16) | ((c.b as u32) << 8) | (c.a as u32);
1000            }
1001        }
1002        CssProperty::FontFamily(v) => {
1003            if let Some(families) = v.get_property() {
1004                let mut hasher = DefaultHasher::new();
1005                families.hash(&mut hasher);
1006                let h = hasher.finish();
1007                let h = if h == 0 { 1 } else { h };
1008                text.font_family_hash = h;
1009                font_hash_map.insert(h, families.clone());
1010            }
1011        }
1012        CssProperty::LineHeight(v) => {
1013            if let Some(lh) = v.get_property() {
1014                let pct_x10 = (lh.inner.normalized() * 1000.0).round() as i32;
1015                if pct_x10 >= -32768 && pct_x10 < I16_SENTINEL_THRESHOLD as i32 {
1016                    text.line_height = pct_x10 as i16;
1017                } else {
1018                    text.line_height = I16_SENTINEL;
1019                }
1020            }
1021        }
1022        CssProperty::LetterSpacing(v) => { text.letter_spacing = encode_css_pixel_as_i16(v); }
1023        CssProperty::WordSpacing(v) => { text.word_spacing = encode_css_pixel_as_i16(v); }
1024        CssProperty::TextIndent(v) => { text.text_indent = encode_css_pixel_as_i16(v); }
1025
1026        // Border radii (cold): encode px × 10 into i16; sentinel stays = unset/0
1027        CssProperty::BorderTopLeftRadius(v) => {
1028            if let Some(exact) = v.get_property() {
1029                if exact.inner.metric == SizeMetric::Px {
1030                    cold.border_top_left_radius = encode_resolved_px_i16(exact.inner.number.get());
1031                }
1032            }
1033        }
1034        CssProperty::BorderTopRightRadius(v) => {
1035            if let Some(exact) = v.get_property() {
1036                if exact.inner.metric == SizeMetric::Px {
1037                    cold.border_top_right_radius = encode_resolved_px_i16(exact.inner.number.get());
1038                }
1039            }
1040        }
1041        CssProperty::BorderBottomLeftRadius(v) => {
1042            if let Some(exact) = v.get_property() {
1043                if exact.inner.metric == SizeMetric::Px {
1044                    cold.border_bottom_left_radius = encode_resolved_px_i16(exact.inner.number.get());
1045                }
1046            }
1047        }
1048        CssProperty::BorderBottomRightRadius(v) => {
1049            if let Some(exact) = v.get_property() {
1050                if exact.inner.metric == SizeMetric::Px {
1051                    cold.border_bottom_right_radius = encode_resolved_px_i16(exact.inner.number.get());
1052                }
1053            }
1054        }
1055
1056        // Opacity: encode as 0-254, 255 = sentinel (unset/default = 1.0)
1057        CssProperty::Opacity(v) => {
1058            if let Some(exact) = v.get_property() {
1059                let o = exact.inner.normalized().clamp(0.0, 1.0);
1060                let byte = (o * 254.0).round() as u8;
1061                // byte is in [0, 254], never collides with OPACITY_SENTINEL=255
1062                cold.opacity = byte;
1063            }
1064        }
1065
1066        // has-flags: set bit whenever property is set (regardless of value).
1067        // Getter uses this as a fast "is the default" bail-out.
1068        CssProperty::Transform(v) => {
1069            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_TRANSFORM; }
1070        }
1071        CssProperty::TransformOrigin(v) => {
1072            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_TRANSFORM_ORIGIN; }
1073        }
1074        CssProperty::BoxShadowTop(v) => {
1075            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_BOX_SHADOW; }
1076        }
1077        CssProperty::BoxShadowBottom(v) => {
1078            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_BOX_SHADOW; }
1079        }
1080        CssProperty::BoxShadowLeft(v) => {
1081            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_BOX_SHADOW; }
1082        }
1083        CssProperty::BoxShadowRight(v) => {
1084            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_BOX_SHADOW; }
1085        }
1086        CssProperty::TextDecoration(v) => {
1087            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_TEXT_DECORATION; }
1088        }
1089        CssProperty::ScrollbarGutter(v) => {
1090            if let Some(exact) = v.get_property() {
1091                use azul_css::props::layout::overflow::StyleScrollbarGutter;
1092                let bits: u8 = match exact {
1093                    StyleScrollbarGutter::Auto => SCROLLBAR_GUTTER_AUTO,
1094                    StyleScrollbarGutter::Stable => SCROLLBAR_GUTTER_STABLE,
1095                    StyleScrollbarGutter::StableBothEdges => SCROLLBAR_GUTTER_BOTH_EDGES,
1096                };
1097                cold.hot_flags = (cold.hot_flags & !HOT_FLAG_SCROLLBAR_GUTTER_MASK)
1098                    | ((bits << HOT_FLAG_SCROLLBAR_GUTTER_SHIFT) & HOT_FLAG_SCROLLBAR_GUTTER_MASK);
1099            }
1100        }
1101        CssProperty::BackgroundContent(v) => {
1102            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_BACKGROUND; }
1103        }
1104        CssProperty::ClipPath(v) => {
1105            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_CLIP_PATH; }
1106        }
1107
1108        // Any scrollbar customisation sets the single `has_any_scrollbar_css`
1109        // bit. When unset, get_scrollbar_style can bail to UA defaults without
1110        // doing 8 cascade walks.
1111        CssProperty::ScrollbarTrack(v) => {
1112            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1113        }
1114        CssProperty::ScrollbarThumb(v) => {
1115            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1116        }
1117        CssProperty::ScrollbarButton(v) => {
1118            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1119        }
1120        CssProperty::ScrollbarCorner(v) => {
1121            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1122        }
1123        CssProperty::ScrollbarWidth(v) => {
1124            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1125        }
1126        CssProperty::ScrollbarColor(v) => {
1127            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1128        }
1129        CssProperty::ScrollbarVisibility(v) => {
1130            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1131        }
1132        CssProperty::ScrollbarFadeDelay(v) => {
1133            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1134        }
1135        CssProperty::ScrollbarFadeDuration(v) => {
1136            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1137        }
1138
1139        // Rare paint/layout props with dedicated fast-path bits.
1140        CssProperty::CounterReset(v) => {
1141            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_COUNTER; }
1142        }
1143        CssProperty::CounterIncrement(v) => {
1144            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_COUNTER; }
1145        }
1146        CssProperty::BreakBefore(v) => {
1147            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_BREAK; }
1148        }
1149        CssProperty::BreakAfter(v) => {
1150            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_BREAK; }
1151        }
1152        CssProperty::TextOrientation(v) => {
1153            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_TEXT_ORIENTATION; }
1154        }
1155        CssProperty::TextShadow(v) => {
1156            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_TEXT_SHADOW; }
1157        }
1158        CssProperty::BackdropFilter(v) => {
1159            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_BACKDROP_FILTER; }
1160        }
1161        CssProperty::Filter(v) => {
1162            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_FILTER; }
1163        }
1164        CssProperty::MixBlendMode(v) => {
1165            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_MIX_BLEND_MODE; }
1166        }
1167
1168        // Non-compact properties (background, etc.) — handled by get_property_slow fallback
1169        _ => {}
1170    }
1171}
1172
1173/// OR the DOM-level declared-flag for rarely-set text properties. Called once
1174/// per property per node so that when a flag bit is clear, callers
1175/// (e.g. `translate_to_text3_constraints`) can skip the cascade walk and use
1176/// the default value — the slow walk would never find a declaration anyway.
1177fn update_dom_declared_flags(prop: &CssProperty, flags: &mut u32) {
1178    // Only mark if the property value is actually "set" (not Auto/Initial/etc.).
1179    // Using `get_property().is_some()` mirrors the pattern used elsewhere in
1180    // this builder for has-X bits.
1181    match prop {
1182        CssProperty::ShapeInside(v) => if v.get_property().is_some() { *flags |= DOM_HAS_SHAPE_INSIDE; }
1183        CssProperty::ShapeOutside(v) => if v.get_property().is_some() { *flags |= DOM_HAS_SHAPE_OUTSIDE; }
1184        CssProperty::TextJustify(v) => if v.get_property().is_some() { *flags |= DOM_HAS_TEXT_JUSTIFY; }
1185        CssProperty::TextIndent(v) => if v.get_property().is_some() { *flags |= DOM_HAS_TEXT_INDENT; }
1186        CssProperty::ColumnCount(v) => if v.get_property().is_some() { *flags |= DOM_HAS_COLUMN_COUNT; }
1187        CssProperty::ColumnGap(v) => if v.get_property().is_some() { *flags |= DOM_HAS_COLUMN_GAP; }
1188        CssProperty::ColumnWidth(v) => if v.get_property().is_some() { *flags |= DOM_HAS_COLUMN_WIDTH; }
1189        CssProperty::InitialLetter(v) => if v.get_property().is_some() { *flags |= DOM_HAS_INITIAL_LETTER; }
1190        CssProperty::InitialLetterAlign(v) => if v.get_property().is_some() { *flags |= DOM_HAS_INITIAL_LETTER_ALIGN; }
1191        CssProperty::LineClamp(v) => if v.get_property().is_some() { *flags |= DOM_HAS_LINE_CLAMP; }
1192        CssProperty::HangingPunctuation(v) => if v.get_property().is_some() { *flags |= DOM_HAS_HANGING_PUNCTUATION; }
1193        CssProperty::TextCombineUpright(v) => if v.get_property().is_some() { *flags |= DOM_HAS_TEXT_COMBINE_UPRIGHT; }
1194        CssProperty::ExclusionMargin(v) => if v.get_property().is_some() { *flags |= DOM_HAS_EXCLUSION_MARGIN; }
1195        CssProperty::ShapeMargin(v) => if v.get_property().is_some() { *flags |= DOM_HAS_SHAPE_MARGIN; }
1196        CssProperty::HyphenationLanguage(v) => if v.get_property().is_some() { *flags |= DOM_HAS_HYPHENATION_LANGUAGE; }
1197        CssProperty::UnicodeBidi(v) => if v.get_property().is_some() { *flags |= DOM_HAS_UNICODE_BIDI; }
1198        CssProperty::TextBoxTrim(v) => if v.get_property().is_some() { *flags |= DOM_HAS_TEXT_BOX_TRIM; }
1199        CssProperty::Hyphens(v) => if v.get_property().is_some() { *flags |= DOM_HAS_HYPHENS; }
1200        CssProperty::WordBreak(v) => if v.get_property().is_some() { *flags |= DOM_HAS_WORD_BREAK; }
1201        CssProperty::OverflowWrap(v) => if v.get_property().is_some() { *flags |= DOM_HAS_OVERFLOW_WRAP; }
1202        CssProperty::LineBreak(v) => if v.get_property().is_some() { *flags |= DOM_HAS_LINE_BREAK; }
1203        CssProperty::TextAlignLast(v) => if v.get_property().is_some() { *flags |= DOM_HAS_TEXT_ALIGN_LAST; }
1204        CssProperty::LineHeight(v) => if v.get_property().is_some() { *flags |= DOM_HAS_LINE_HEIGHT; }
1205        _ => {}
1206    }
1207}
1208
1209// =============================================================================
1210// Helper encoders for dimension properties
1211// =============================================================================
1212
1213/// Encode a GridLine into i16: Auto=I16_AUTO, Line(n)=n, Span(n)=-(n).
1214/// Named lines fall back to I16_SENTINEL (not compact-encodable).
1215fn encode_grid_line(line: &azul_css::props::layout::grid::GridLine) -> i16 {
1216    use azul_css::props::layout::grid::GridLine;
1217    match line {
1218        GridLine::Auto => I16_AUTO,
1219        GridLine::Line(n) => {
1220            if *n >= -32000 && *n <= 32000 { *n as i16 } else { I16_SENTINEL }
1221        }
1222        GridLine::Span(n) => {
1223            if *n >= 1 && *n <= 32000 { -(*n as i16) } else { I16_SENTINEL }
1224        }
1225        GridLine::Named(_) => I16_SENTINEL,
1226    }
1227}
1228
1229/// Encode a CssPropertyValue<LayoutWidth> into u32 compact form.
1230fn encode_layout_width<T: LayoutWidthLike>(val: &CssPropertyValue<T>) -> u32 {
1231    match val {
1232        CssPropertyValue::Exact(w) => w.encode_compact_u32(),
1233        CssPropertyValue::Auto => U32_AUTO,
1234        CssPropertyValue::Initial => U32_INITIAL,
1235        CssPropertyValue::Inherit => U32_INHERIT,
1236        CssPropertyValue::None => U32_NONE,
1237        _ => U32_SENTINEL,
1238    }
1239}
1240
1241/// Encode a CssPropertyValue<LayoutHeight> into u32 compact form.
1242fn encode_layout_height<T: LayoutWidthLike>(val: &CssPropertyValue<T>) -> u32 {
1243    encode_layout_width(val)
1244}
1245
1246/// Trait for types that can be encoded as compact u32 dimension values.
1247/// Implemented for LayoutWidth, LayoutHeight (which are Auto|Px|MinContent|MaxContent|Calc enums).
1248trait LayoutWidthLike {
1249    fn encode_compact_u32(&self) -> u32;
1250}
1251
1252impl LayoutWidthLike for LayoutWidth {
1253    fn encode_compact_u32(&self) -> u32 {
1254        match self {
1255            LayoutWidth::Auto => U32_AUTO,
1256            LayoutWidth::Px(pv) => encode_pixel_value_u32(pv),
1257            LayoutWidth::MinContent => U32_MIN_CONTENT,
1258            LayoutWidth::MaxContent => U32_MAX_CONTENT,
1259            LayoutWidth::FitContent(_) => U32_SENTINEL,
1260            LayoutWidth::Calc(_) => U32_SENTINEL, // Calc → overflow to tier 3
1261        }
1262    }
1263}
1264
1265impl LayoutWidthLike for LayoutHeight {
1266    fn encode_compact_u32(&self) -> u32 {
1267        match self {
1268            LayoutHeight::Auto => U32_AUTO,
1269            LayoutHeight::Px(pv) => encode_pixel_value_u32(pv),
1270            LayoutHeight::MinContent => U32_MIN_CONTENT,
1271            LayoutHeight::MaxContent => U32_MAX_CONTENT,
1272            LayoutHeight::FitContent(_) => U32_SENTINEL,
1273            LayoutHeight::Calc(_) => U32_SENTINEL,
1274        }
1275    }
1276}
1277
1278/// Encode a CssPropertyValue wrapping a simple PixelValue struct (LayoutMinWidth, etc.)
1279fn encode_pixel_prop<T: HasInnerPixelValue>(val: &CssPropertyValue<T>) -> u32 {
1280    match val {
1281        CssPropertyValue::Exact(inner) => encode_pixel_value_u32(&inner.get_inner_pixel()),
1282        CssPropertyValue::Auto => U32_AUTO,
1283        CssPropertyValue::Initial => U32_INITIAL,
1284        CssPropertyValue::Inherit => U32_INHERIT,
1285        CssPropertyValue::None => U32_NONE,
1286        _ => U32_SENTINEL,
1287    }
1288}
1289
1290/// Trait for dimension structs wrapping `inner: PixelValue`.
1291trait HasInnerPixelValue {
1292    fn get_inner_pixel(&self) -> azul_css::props::basic::pixel::PixelValue;
1293}
1294
1295macro_rules! impl_has_inner_pixel {
1296    ($($ty:ty),*) => {
1297        $(
1298            impl HasInnerPixelValue for $ty {
1299                fn get_inner_pixel(&self) -> azul_css::props::basic::pixel::PixelValue {
1300                    self.inner
1301                }
1302            }
1303        )*
1304    };
1305}
1306
1307impl_has_inner_pixel!(
1308    azul_css::props::layout::dimensions::LayoutMinWidth,
1309    azul_css::props::layout::dimensions::LayoutMaxWidth,
1310    azul_css::props::layout::dimensions::LayoutMinHeight,
1311    azul_css::props::layout::dimensions::LayoutMaxHeight,
1312    azul_css::props::basic::font::StyleFontSize,
1313    azul_css::props::layout::spacing::LayoutPaddingTop,
1314    azul_css::props::layout::spacing::LayoutPaddingRight,
1315    azul_css::props::layout::spacing::LayoutPaddingBottom,
1316    azul_css::props::layout::spacing::LayoutPaddingLeft,
1317    azul_css::props::layout::spacing::LayoutMarginTop,
1318    azul_css::props::layout::spacing::LayoutMarginRight,
1319    azul_css::props::layout::spacing::LayoutMarginBottom,
1320    azul_css::props::layout::spacing::LayoutMarginLeft,
1321    azul_css::props::style::border::LayoutBorderTopWidth,
1322    azul_css::props::style::border::LayoutBorderRightWidth,
1323    azul_css::props::style::border::LayoutBorderBottomWidth,
1324    azul_css::props::style::border::LayoutBorderLeftWidth,
1325    azul_css::props::layout::position::LayoutTop,
1326    azul_css::props::layout::position::LayoutRight,
1327    azul_css::props::layout::position::LayoutInsetBottom,
1328    azul_css::props::layout::position::LayoutLeft,
1329    azul_css::props::style::text::StyleLetterSpacing,
1330    azul_css::props::style::text::StyleWordSpacing,
1331    azul_css::props::style::text::StyleTextIndent,
1332    azul_css::props::style::text::StyleTabSize
1333);
1334
1335/// Encode a CssPropertyValue<T> where T wraps a PixelValue, as i16 (×10 resolved px).
1336/// Delegates to the canonical `azul_css::compact_cache::encode_css_pixel_as_i16`.
1337fn encode_css_pixel_as_i16<T: HasInnerPixelValue>(val: &CssPropertyValue<T>) -> i16 {
1338    let mapped = match val {
1339        CssPropertyValue::Exact(inner) => CssPropertyValue::Exact(inner.get_inner_pixel()),
1340        CssPropertyValue::Auto => CssPropertyValue::Auto,
1341        CssPropertyValue::Initial => CssPropertyValue::Initial,
1342        CssPropertyValue::Inherit => CssPropertyValue::Inherit,
1343        CssPropertyValue::None => CssPropertyValue::None,
1344        _ => return I16_SENTINEL,
1345    };
1346    azul_css::compact_cache::encode_css_pixel_as_i16(&mapped)
1347}
1348
1349/// Encode margin: same as encode_css_pixel_as_i16 but Auto is a distinct value.
1350fn encode_margin_i16<T: HasInnerPixelValue>(val: &CssPropertyValue<T>) -> i16 {
1351    encode_css_pixel_as_i16(val)
1352}
1353
1354/// Encode CssPropertyValue<LayoutFlexBasis> — LayoutFlexBasis is Auto | Exact(PixelValue).
1355fn encode_flex_basis(val: &CssPropertyValue<LayoutFlexBasis>) -> u32 {
1356    match val {
1357        CssPropertyValue::Exact(fb) => match fb {
1358            LayoutFlexBasis::Auto => U32_AUTO,
1359            LayoutFlexBasis::Exact(pv) => encode_pixel_value_u32(pv),
1360        },
1361        CssPropertyValue::Auto => U32_AUTO,
1362        CssPropertyValue::Initial => U32_INITIAL,
1363        CssPropertyValue::Inherit => U32_INHERIT,
1364        CssPropertyValue::None => U32_NONE,
1365        _ => U32_SENTINEL,
1366    }
1367}