Skip to main content

azul_core/
compact.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;
11// wildcard import: this module is the consumer of the whole compact_cache codec
12// (encode/decode helpers + sentinel consts); enumerating them is unmaintainable.
13use crate::hash::DefaultHasher;
14use alloc::vec::Vec;
15#[allow(clippy::wildcard_imports)]
16use azul_css::compact_cache::*;
17use azul_css::css::CssPropertyValue;
18use azul_css::props::basic::length::SizeMetric;
19use azul_css::props::layout::dimensions::{LayoutHeight, LayoutWidth};
20use azul_css::props::layout::flex::LayoutFlexBasis;
21use azul_css::props::layout::position::LayoutZIndex;
22use azul_css::props::property::CssProperty;
23use core::hash::{Hash, Hasher};
24
25impl CssPropertyCache {
26    /// Build a `CompactLayoutCache` from the current property cache state.
27    ///
28    /// Must be called after `restyle()`, `apply_ua_css()`, and `compute_inherited_values()`.
29    /// Resolves all layout-relevant properties for every node in the "normal" state
30    /// (no hover/active/focus) and encodes them into compact arrays.
31    ///
32    /// Tier 1/2/2b provide fast-path access for layout-hot properties.
33    /// Non-compact properties (background, transform, box-shadow, etc.) are
34    /// resolved via the slow cascade path in `get_property_slow()`.
35    ///
36    /// `prev_font_hashes` is the per-node font hash array from the previous frame.
37    /// When non-empty, each node's new `font_family_hash` is compared against the
38    /// previous value, and differing nodes are recorded in `font_dirty_nodes`.
39    /// On the first build (empty slice), ALL text nodes are marked dirty.
40    // fixed-point encoders: z-index and line-height (%×10) are range-checked
41    // against the i16 sentinel threshold before the deliberate narrowing cast.
42    #[allow(clippy::cast_possible_truncation)]
43    #[allow(clippy::similar_names)] // domain-standard coordinate/control-point names
44    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
45    pub fn build_compact_cache(
46        &self,
47        node_data: &[NodeData],
48        prev_font_hashes: &[u64],
49    ) -> CompactLayoutCache {
50        let node_count = self.node_count;
51        let default_state = StyledNodeState::default();
52        let mut result = CompactLayoutCache::with_capacity(node_count);
53
54        for (i, nd) in node_data.iter().enumerate().take(node_count) {
55            let node_id = NodeId::new(i);
56
57            // =====================================================================
58            // Tier 1: Encode all 20 enum properties into u64
59            // =====================================================================
60
61            let display = self
62                .get_display(nd, &node_id, &default_state)
63                .and_then(|v| v.get_property().copied())
64                .unwrap_or_default();
65            let position = self
66                .get_position(nd, &node_id, &default_state)
67                .and_then(|v| v.get_property().copied())
68                .unwrap_or_default();
69            let float = self
70                .get_float(nd, &node_id, &default_state)
71                .and_then(|v| v.get_property().copied())
72                .unwrap_or_default();
73            let overflow_x = self
74                .get_overflow_x(nd, &node_id, &default_state)
75                .and_then(|v| v.get_property().copied())
76                .unwrap_or_default();
77            let overflow_y = self
78                .get_overflow_y(nd, &node_id, &default_state)
79                .and_then(|v| v.get_property().copied())
80                .unwrap_or_default();
81            let box_sizing = self
82                .get_box_sizing(nd, &node_id, &default_state)
83                .and_then(|v| v.get_property().copied())
84                .unwrap_or_default();
85            let flex_direction = self
86                .get_flex_direction(nd, &node_id, &default_state)
87                .and_then(|v| v.get_property().copied())
88                .unwrap_or_default();
89            let flex_wrap = self
90                .get_flex_wrap(nd, &node_id, &default_state)
91                .and_then(|v| v.get_property().copied())
92                .unwrap_or_default();
93            let justify_content = self
94                .get_justify_content(nd, &node_id, &default_state)
95                .and_then(|v| v.get_property().copied())
96                .unwrap_or_default();
97            let align_items = self
98                .get_align_items(nd, &node_id, &default_state)
99                .and_then(|v| v.get_property().copied())
100                .unwrap_or_default();
101            let align_content = self
102                .get_align_content(nd, &node_id, &default_state)
103                .and_then(|v| v.get_property().copied())
104                .unwrap_or_default();
105            let writing_mode = self
106                .get_writing_mode(nd, &node_id, &default_state)
107                .and_then(|v| v.get_property().copied())
108                .unwrap_or_default();
109            let clear = self
110                .get_clear(nd, &node_id, &default_state)
111                .and_then(|v| v.get_property().copied())
112                .unwrap_or_default();
113            let font_weight = self
114                .get_font_weight(nd, &node_id, &default_state)
115                .and_then(|v| v.get_property().copied())
116                .unwrap_or_default();
117            let font_style = self
118                .get_font_style(nd, &node_id, &default_state)
119                .and_then(|v| v.get_property().copied())
120                .unwrap_or_default();
121            let text_align = self
122                .get_text_align(nd, &node_id, &default_state)
123                .and_then(|v| v.get_property().copied())
124                .unwrap_or_default();
125            let visibility = self
126                .get_visibility(nd, &node_id, &default_state)
127                .and_then(|v| v.get_property().copied())
128                .unwrap_or_default();
129            let white_space = self
130                .get_white_space(nd, &node_id, &default_state)
131                .and_then(|v| v.get_property().copied())
132                .unwrap_or_default();
133            let direction = self
134                .get_direction(nd, &node_id, &default_state)
135                .and_then(|v| v.get_property().copied())
136                .unwrap_or_default();
137            let vertical_align = self
138                .get_vertical_align(nd, &node_id, &default_state)
139                .and_then(|v| v.get_property().copied())
140                .unwrap_or_default();
141
142            let border_collapse = self
143                .get_border_collapse(nd, &node_id, &default_state)
144                .and_then(|v| v.get_property().copied())
145                .unwrap_or_default();
146
147            result.tier1_enums[i] = encode_tier1(
148                display,
149                position,
150                float,
151                overflow_x,
152                overflow_y,
153                box_sizing,
154                flex_direction,
155                flex_wrap,
156                justify_content,
157                align_items,
158                align_content,
159                writing_mode,
160                clear,
161                font_weight,
162                font_style,
163                text_align,
164                visibility,
165                white_space,
166                direction,
167                vertical_align,
168                border_collapse,
169            );
170
171            // =====================================================================
172            // Tier 2: Encode numeric dimension properties
173            // =====================================================================
174
175            // Width/Height are enums: Auto | Px(PixelValue) | MinContent | MaxContent | Calc
176            if let Some(val) = self.get_width(nd, &node_id, &default_state) {
177                result.tier2_dims[i].width = encode_layout_width(val);
178            }
179            if let Some(val) = self.get_height(nd, &node_id, &default_state) {
180                result.tier2_dims[i].height = encode_layout_height(val);
181            }
182
183            // Min/Max Width/Height are simple PixelValue wrappers
184            if let Some(val) = self.get_min_width(nd, &node_id, &default_state) {
185                result.tier2_dims[i].min_width = encode_pixel_prop(val);
186            }
187            if let Some(val) = self.get_max_width(nd, &node_id, &default_state) {
188                result.tier2_dims[i].max_width = encode_pixel_prop(val);
189            }
190            if let Some(val) = self.get_min_height(nd, &node_id, &default_state) {
191                result.tier2_dims[i].min_height = encode_pixel_prop(val);
192            }
193            if let Some(val) = self.get_max_height(nd, &node_id, &default_state) {
194                result.tier2_dims[i].max_height = encode_pixel_prop(val);
195            }
196
197            // Flex basis (enum: Auto | Exact(PixelValue))
198            if let Some(val) = self.get_flex_basis(nd, &node_id, &default_state) {
199                result.tier2_dims[i].flex_basis = encode_flex_basis(val);
200            }
201
202            // Font size
203            if let Some(val) = self.get_font_size(nd, &node_id, &default_state) {
204                result.tier2_dims[i].font_size = encode_pixel_prop(val);
205            }
206
207            // Padding (i16 × 10 resolved px)
208            if let Some(val) = self.get_padding_top(nd, &node_id, &default_state) {
209                result.tier2_dims[i].padding_top = encode_css_pixel_as_i16(val);
210            }
211            if let Some(val) = self.get_padding_right(nd, &node_id, &default_state) {
212                result.tier2_dims[i].padding_right = encode_css_pixel_as_i16(val);
213            }
214            if let Some(val) = self.get_padding_bottom(nd, &node_id, &default_state) {
215                result.tier2_dims[i].padding_bottom = encode_css_pixel_as_i16(val);
216            }
217            if let Some(val) = self.get_padding_left(nd, &node_id, &default_state) {
218                result.tier2_dims[i].padding_left = encode_css_pixel_as_i16(val);
219            }
220
221            // Margin (i16, auto is special)
222            if let Some(val) = self.get_margin_top(nd, &node_id, &default_state) {
223                result.tier2_dims[i].margin_top = encode_margin_i16(val);
224            }
225            if let Some(val) = self.get_margin_right(nd, &node_id, &default_state) {
226                result.tier2_dims[i].margin_right = encode_margin_i16(val);
227            }
228            if let Some(val) = self.get_margin_bottom(nd, &node_id, &default_state) {
229                result.tier2_dims[i].margin_bottom = encode_margin_i16(val);
230            }
231            if let Some(val) = self.get_margin_left(nd, &node_id, &default_state) {
232                result.tier2_dims[i].margin_left = encode_margin_i16(val);
233            }
234
235            // Border widths (i16 × 10 resolved px)
236            if let Some(val) = self.get_border_top_width(nd, &node_id, &default_state) {
237                result.tier2_dims[i].border_top_width = encode_css_pixel_as_i16(val);
238            }
239            if let Some(val) = self.get_border_right_width(nd, &node_id, &default_state) {
240                result.tier2_dims[i].border_right_width = encode_css_pixel_as_i16(val);
241            }
242            if let Some(val) = self.get_border_bottom_width(nd, &node_id, &default_state) {
243                result.tier2_dims[i].border_bottom_width = encode_css_pixel_as_i16(val);
244            }
245            if let Some(val) = self.get_border_left_width(nd, &node_id, &default_state) {
246                result.tier2_dims[i].border_left_width = encode_css_pixel_as_i16(val);
247            }
248
249            // Position offsets (top/right/bottom/left)
250            if let Some(val) = self.get_top(nd, &node_id, &default_state) {
251                result.tier2_dims[i].top = encode_css_pixel_as_i16(val);
252            }
253            if let Some(val) = self.get_right(nd, &node_id, &default_state) {
254                result.tier2_dims[i].right = encode_css_pixel_as_i16(val);
255            }
256            if let Some(val) = self.get_bottom(nd, &node_id, &default_state) {
257                result.tier2_dims[i].bottom = encode_css_pixel_as_i16(val);
258            }
259            if let Some(val) = self.get_left(nd, &node_id, &default_state) {
260                result.tier2_dims[i].left = encode_css_pixel_as_i16(val);
261            }
262
263            // Flex grow/shrink (u16 × 100)
264            if let Some(val) = self.get_flex_grow(nd, &node_id, &default_state) {
265                if let Some(exact) = val.get_property() {
266                    result.tier2_dims[i].flex_grow = encode_flex_u16(exact.inner.get());
267                }
268            }
269            if let Some(val) = self.get_flex_shrink(nd, &node_id, &default_state) {
270                if let Some(exact) = val.get_property() {
271                    result.tier2_dims[i].flex_shrink = encode_flex_u16(exact.inner.get());
272                }
273            }
274
275            // =====================================================================
276            // Tier 2 cold: Paint-only properties
277            // =====================================================================
278
279            // Z-index
280            if let Some(val) = self.get_z_index(nd, &node_id, &default_state) {
281                if let Some(exact) = val.get_property() {
282                    match exact {
283                        LayoutZIndex::Auto => result.tier2_cold[i].z_index = I16_AUTO,
284                        LayoutZIndex::Integer(z) => {
285                            // Two-sided, like the line-height encoder: a large NEGATIVE z
286                            // used to fall through to `*z as i16` and WRAP positive
287                            // (-40000 -> +25536). Escape both out-of-range ends to the
288                            // sentinel (tier 3) so the real value is preserved.
289                            result.tier2_cold[i].z_index =
290                                if *z >= -32768 && *z < i32::from(I16_SENTINEL_THRESHOLD) {
291                                    *z as i16
292                                } else {
293                                    I16_SENTINEL
294                                };
295                        }
296                    }
297                }
298            }
299
300            // Border styles (packed into u16)
301            {
302                let bts = self
303                    .get_border_top_style(nd, &node_id, &default_state)
304                    .and_then(|v| v.get_property().copied())
305                    .map(|v| v.inner)
306                    .unwrap_or_default();
307                let brs = self
308                    .get_border_right_style(nd, &node_id, &default_state)
309                    .and_then(|v| v.get_property().copied())
310                    .map(|v| v.inner)
311                    .unwrap_or_default();
312                let bbs = self
313                    .get_border_bottom_style(nd, &node_id, &default_state)
314                    .and_then(|v| v.get_property().copied())
315                    .map(|v| v.inner)
316                    .unwrap_or_default();
317                let bls = self
318                    .get_border_left_style(nd, &node_id, &default_state)
319                    .and_then(|v| v.get_property().copied())
320                    .map(|v| v.inner)
321                    .unwrap_or_default();
322                result.tier2_cold[i].border_styles_packed =
323                    encode_border_styles_packed(bts, brs, bbs, bls);
324            }
325
326            // Border colors (ColorU → u32 as 0xRRGGBBAA)
327            if let Some(val) = self.get_border_top_color(nd, &node_id, &default_state) {
328                if let Some(color) = val.get_property() {
329                    result.tier2_cold[i].border_top_color = encode_color_u32(&color.inner);
330                }
331            }
332            if let Some(val) = self.get_border_right_color(nd, &node_id, &default_state) {
333                if let Some(color) = val.get_property() {
334                    result.tier2_cold[i].border_right_color = encode_color_u32(&color.inner);
335                }
336            }
337            if let Some(val) = self.get_border_bottom_color(nd, &node_id, &default_state) {
338                if let Some(color) = val.get_property() {
339                    result.tier2_cold[i].border_bottom_color = encode_color_u32(&color.inner);
340                }
341            }
342            if let Some(val) = self.get_border_left_color(nd, &node_id, &default_state) {
343                if let Some(color) = val.get_property() {
344                    result.tier2_cold[i].border_left_color = encode_color_u32(&color.inner);
345                }
346            }
347
348            // Border spacing (two PixelValue → i16 × 10 resolved px)
349            if let Some(val) = self.get_border_spacing(nd, &node_id, &default_state) {
350                if let Some(spacing) = val.get_property() {
351                    if spacing.horizontal.metric == SizeMetric::Px {
352                        result.tier2_cold[i].border_spacing_h =
353                            encode_resolved_px_i16(spacing.horizontal.number.get());
354                    }
355                    if spacing.vertical.metric == SizeMetric::Px {
356                        result.tier2_cold[i].border_spacing_v =
357                            encode_resolved_px_i16(spacing.vertical.number.get());
358                    }
359                }
360            }
361
362            // Tab size (PixelValue → i16 × 10 resolved px)
363            if let Some(val) = self.get_tab_size(nd, &node_id, &default_state) {
364                result.tier2_cold[i].tab_size = encode_css_pixel_as_i16(val);
365            }
366
367            // =====================================================================
368            // Tier 2b: Text properties
369            // =====================================================================
370
371            // Text color (ColorU → u32 as 0xRRGGBBAA)
372            if let Some(val) = self.get_text_color(nd, &node_id, &default_state) {
373                if let Some(color) = val.get_property() {
374                    let c = &color.inner;
375                    result.tier2b_text[i].text_color = (u32::from(c.r) << 24)
376                        | (u32::from(c.g) << 16)
377                        | (u32::from(c.b) << 8)
378                        | u32::from(c.a);
379                }
380            }
381
382            // Font-family (hash the whole StyleFontFamilyVec for fast comparison)
383            if let Some(val) = self.get_font_family(nd, &node_id, &default_state) {
384                if let Some(families) = val.get_property() {
385                    let mut hasher = DefaultHasher::new();
386                    families.hash(&mut hasher);
387                    let h = hasher.finish();
388                    let h = if h == 0 { 1 } else { h };
389                    result.tier2b_text[i].font_family_hash = h;
390                    result.font_hash_to_families.insert(h, families.clone());
391                }
392            }
393
394            // Line-height. Parser convention: a NEGATIVE normalized() is an
395            // ABSOLUTE pixel line-height, a positive one a unitless multiple
396            // (or percentage) of font-size. The two need different i16
397            // scales:
398            //  - positive: multiple × 1000 (120% -> 1200; range up to ~32x)
399            //  - negative: -px × 10 (line-height: 40px -> -400; ±3276.7px)
400            // The old single ×1000 scale overflowed i16 for any absolute
401            // line-height above 32.76px, stored the SENTINEL, and the getter
402            // decoded that as "line-height: normal" - `line-height: 40px`
403            // was silently dropped on every normal-state node.
404            if let Some(val) = self.get_line_height(nd, &node_id, &default_state) {
405                if let Some(lh) = val.get_property() {
406                    let n = lh.inner.normalized();
407                    let stored = if n < 0.0 {
408                        // Absolute px: clamp to the representable range
409                        // instead of falling to the sentinel ("normal").
410                        ((n * 10.0).round() as i32).max(-32768)
411                    } else {
412                        (n * 1000.0).round() as i32
413                    };
414                    if stored >= -32768 && stored < i32::from(I16_SENTINEL_THRESHOLD) {
415                        result.tier2b_text[i].line_height = stored as i16;
416                    } else {
417                        result.tier2b_text[i].line_height = I16_SENTINEL;
418                    }
419                }
420            }
421
422            // Letter-spacing (PixelValue wrapper → i16 × 10 resolved px)
423            if let Some(val) = self.get_letter_spacing(nd, &node_id, &default_state) {
424                result.tier2b_text[i].letter_spacing = encode_css_pixel_as_i16(val);
425            }
426
427            // Word-spacing (PixelValue wrapper → i16 × 10 resolved px)
428            if let Some(val) = self.get_word_spacing(nd, &node_id, &default_state) {
429                result.tier2b_text[i].word_spacing = encode_css_pixel_as_i16(val);
430            }
431
432            // Text-indent (PixelValue wrapper → i16 × 10 resolved px)
433            if let Some(val) = self.get_text_indent(nd, &node_id, &default_state) {
434                result.tier2b_text[i].text_indent = encode_css_pixel_as_i16(val);
435            }
436        }
437
438        // =====================================================================
439        // Per-node font dirty tracking (P4)
440        // Compare each node's font_family_hash against the previous frame's hash.
441        // Nodes whose hash changed are recorded in font_dirty_nodes for
442        // incremental font chain re-resolution instead of all-or-nothing.
443        // =====================================================================
444        result.font_dirty_nodes.clear();
445        for i in 0..node_count {
446            let new_hash = result.tier2b_text[i].font_family_hash;
447            let old_hash = prev_font_hashes.get(i).copied().unwrap_or(0);
448            if new_hash != old_hash {
449                result.font_dirty_nodes.push(i);
450            }
451        }
452        // Save current hashes as prev_font_hashes for next frame comparison
453        result.prev_font_hashes = result
454            .tier2b_text
455            .iter()
456            .map(|t| t.font_family_hash)
457            .collect();
458
459        result
460    }
461
462    /// Build compact cache with inheritance in a single pass.
463    ///
464    /// Replaces the separate `compute_inherited_values()` + `build_compact_cache()` calls.
465    /// For each node (in DOM index order, which is pre-order = parents before children):
466    ///   1. Copy parent's compact values for INHERITABLE properties
467    ///   2. Apply this node's CSS properties on top (from `css_props` + inline + UA)
468    ///   3. Write directly to compact arrays
469    ///
470    /// This eliminates 50K Vec clones from `compute_inherited_values` and
471    /// avoids re-reading properties from 5 separate data structures.
472    pub fn build_compact_cache_with_inheritance(
473        &self,
474        node_data: &[NodeData],
475        node_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
476        prev_font_hashes: &[u64],
477    ) -> CompactLayoutCache {
478        self.build_compact_cache_with_inheritance_debug(
479            node_data,
480            node_hierarchy,
481            prev_font_hashes,
482            &mut None,
483        )
484    }
485
486    /// Same as `build_compact_cache_with_inheritance` but with optional debug logging.
487    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
488    pub fn build_compact_cache_with_inheritance_debug(
489        &self,
490        node_data: &[NodeData],
491        node_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
492        prev_font_hashes: &[u64],
493        debug_messages: &mut Option<Vec<azul_css::LayoutDebugMessage>>,
494    ) -> CompactLayoutCache {
495        let node_count = self.node_count;
496        let default_state = StyledNodeState::default();
497        let mut result = CompactLayoutCache::with_capacity(node_count);
498
499        // Pre-encode global CSS properties (from `*` rules) into compact form.
500        // These are applied as baseline for every node before inheritance.
501        let mut global_tier1: u64 = 0;
502        let mut global_dims = CompactNodeProps::default();
503        let mut global_cold = CompactNodePropsCold::default();
504        let mut global_text = CompactTextProps::default();
505        let has_global = !self.global_css_props.is_empty();
506
507        if has_global {
508            use azul_css::props::property::CssProperty;
509
510            for prop in &self.global_css_props {
511                result.uses_viewport_units |= css_property_uses_viewport_units(prop);
512                // Apply each global property to the pre-encoded compact values
513                macro_rules! global_tier1_enum {
514                    ($variant:ident, $shift:ident, $mask:ident, $encoder:ident) => {
515                        if let CssProperty::$variant(v) = prop {
516                            if let Some(exact) = v.get_property() {
517                                let encoded = u64::from($encoder(*exact));
518                                let shifted_mask = $mask << $shift;
519                                global_tier1 =
520                                    (global_tier1 & !shifted_mask) | ((encoded & $mask) << $shift);
521                            }
522                        }
523                    };
524                }
525
526                global_tier1_enum!(Display, DISPLAY_SHIFT, DISPLAY_MASK, layout_display_to_u8);
527                global_tier1_enum!(
528                    Position,
529                    POSITION_SHIFT,
530                    POSITION_MASK,
531                    layout_position_to_u8
532                );
533                global_tier1_enum!(Float, FLOAT_SHIFT, FLOAT_MASK, layout_float_to_u8);
534                global_tier1_enum!(
535                    OverflowX,
536                    OVERFLOW_X_SHIFT,
537                    OVERFLOW_MASK,
538                    layout_overflow_to_u8
539                );
540                global_tier1_enum!(
541                    OverflowY,
542                    OVERFLOW_Y_SHIFT,
543                    OVERFLOW_MASK,
544                    layout_overflow_to_u8
545                );
546                global_tier1_enum!(
547                    BoxSizing,
548                    BOX_SIZING_SHIFT,
549                    BOX_SIZING_MASK,
550                    layout_box_sizing_to_u8
551                );
552                global_tier1_enum!(
553                    FlexDirection,
554                    FLEX_DIRECTION_SHIFT,
555                    FLEX_DIR_MASK,
556                    layout_flex_direction_to_u8
557                );
558                global_tier1_enum!(
559                    FlexWrap,
560                    FLEX_WRAP_SHIFT,
561                    FLEX_WRAP_MASK,
562                    layout_flex_wrap_to_u8
563                );
564                global_tier1_enum!(
565                    JustifyContent,
566                    JUSTIFY_CONTENT_SHIFT,
567                    JUSTIFY_MASK,
568                    layout_justify_content_to_u8
569                );
570                global_tier1_enum!(
571                    AlignItems,
572                    ALIGN_ITEMS_SHIFT,
573                    ALIGN_MASK,
574                    layout_align_items_to_u8
575                );
576                global_tier1_enum!(
577                    AlignContent,
578                    ALIGN_CONTENT_SHIFT,
579                    ALIGN_MASK,
580                    layout_align_content_to_u8
581                );
582                global_tier1_enum!(Clear, CLEAR_SHIFT, CLEAR_MASK, layout_clear_to_u8);
583                global_tier1_enum!(
584                    Visibility,
585                    VISIBILITY_SHIFT,
586                    VISIBILITY_MASK,
587                    style_visibility_to_u8
588                );
589                global_tier1_enum!(
590                    WritingMode,
591                    WRITING_MODE_SHIFT,
592                    WRITING_MODE_MASK,
593                    layout_writing_mode_to_u8
594                );
595                global_tier1_enum!(
596                    FontWeight,
597                    FONT_WEIGHT_SHIFT,
598                    FONT_WEIGHT_MASK,
599                    style_font_weight_to_u8
600                );
601                global_tier1_enum!(
602                    FontStyle,
603                    FONT_STYLE_SHIFT,
604                    FONT_STYLE_MASK,
605                    style_font_style_to_u8
606                );
607                global_tier1_enum!(
608                    TextAlign,
609                    TEXT_ALIGN_SHIFT,
610                    TEXT_ALIGN_MASK,
611                    style_text_align_to_u8
612                );
613                global_tier1_enum!(
614                    WhiteSpace,
615                    WHITE_SPACE_SHIFT,
616                    WHITE_SPACE_MASK,
617                    style_white_space_to_u8
618                );
619                global_tier1_enum!(
620                    Direction,
621                    DIRECTION_SHIFT,
622                    DIRECTION_MASK,
623                    style_direction_to_u8
624                );
625                global_tier1_enum!(
626                    VerticalAlign,
627                    VERTICAL_ALIGN_SHIFT,
628                    VERTICAL_ALIGN_MASK,
629                    style_vertical_align_to_u8
630                );
631                global_tier1_enum!(
632                    BorderCollapse,
633                    BORDER_COLLAPSE_SHIFT,
634                    BORDER_COLLAPSE_MASK,
635                    border_collapse_to_u8
636                );
637
638                // Tier 2 dims
639                match prop {
640                    CssProperty::PaddingTop(v) => {
641                        global_dims.padding_top = encode_css_pixel_as_i16(v);
642                    }
643                    CssProperty::PaddingRight(v) => {
644                        global_dims.padding_right = encode_css_pixel_as_i16(v);
645                    }
646                    CssProperty::PaddingBottom(v) => {
647                        global_dims.padding_bottom = encode_css_pixel_as_i16(v);
648                    }
649                    CssProperty::PaddingLeft(v) => {
650                        global_dims.padding_left = encode_css_pixel_as_i16(v);
651                    }
652                    CssProperty::MarginTop(v) => {
653                        global_dims.margin_top = encode_margin_i16(v);
654                    }
655                    CssProperty::MarginRight(v) => {
656                        global_dims.margin_right = encode_margin_i16(v);
657                    }
658                    CssProperty::MarginBottom(v) => {
659                        global_dims.margin_bottom = encode_margin_i16(v);
660                    }
661                    CssProperty::MarginLeft(v) => {
662                        global_dims.margin_left = encode_margin_i16(v);
663                    }
664                    CssProperty::Width(v) => {
665                        global_dims.width = encode_layout_width(v);
666                    }
667                    CssProperty::Height(v) => {
668                        global_dims.height = encode_layout_height(v);
669                    }
670                    CssProperty::FontSize(v) => {
671                        global_dims.font_size = encode_pixel_prop(v);
672                    }
673                    CssProperty::BorderTopWidth(v) => {
674                        global_dims.border_top_width = encode_css_pixel_as_i16(v);
675                    }
676                    CssProperty::BorderRightWidth(v) => {
677                        global_dims.border_right_width = encode_css_pixel_as_i16(v);
678                    }
679                    CssProperty::BorderBottomWidth(v) => {
680                        global_dims.border_bottom_width = encode_css_pixel_as_i16(v);
681                    }
682                    CssProperty::BorderLeftWidth(v) => {
683                        global_dims.border_left_width = encode_css_pixel_as_i16(v);
684                    }
685                    _ => {}
686                }
687            }
688
689            if global_tier1 != 0 {
690                global_tier1 |= TIER1_POPULATED_BIT;
691            }
692        }
693
694        // Helper: push debug message if debug_messages is Some
695        macro_rules! cascade_debug {
696            ($($arg:tt)*) => {
697                if let Some(ref mut msgs) = debug_messages {
698                    msgs.push(azul_css::LayoutDebugMessage::css_getter(format!($($arg)*)));
699                }
700            };
701        }
702
703        for i in 0..node_count {
704            let node_id = NodeId::new(i);
705            let nd = &node_data[i];
706
707            // Step 0: Apply UA CSS defaults first (lowest priority).
708            // Then global `*` rules override UA (higher priority).
709            // Then per-node CSS (Step 3) overrides both.
710            //
711            // CSS cascade priority: UA < author `*` < author specific < inline
712
713            // Step 1: Inherit from parent's COMPACT values (not computed_values)
714            // Parent index is always < i in pre-order arena, so already computed.
715            //
716            // Step 1: Inherit ONLY inheritable CSS properties from parent.
717            // Non-inheritable fields (display, position, float, overflow, box-sizing,
718            // flex-*, clear, vertical-align, writing-mode) stay at 0 (CSS initial value).
719            // They get set by UA CSS (Step 2) and author CSS (Step 3).
720            let parent_id = node_hierarchy[i].parent_id();
721            if let Some(pid) = parent_id {
722                let pi = pid.index();
723
724                // AUDIT: inheritance assumes a PRE-ORDER arena, i.e. a node's
725                // parent is always stored at a lower index (`pi < i`) and has
726                // therefore already been fully cascaded. A forward reference
727                // (`pi >= i`) would silently inherit that parent's still-default
728                // (all-zero) values, and an out-of-bounds `pi >= node_count`
729                // would panic. Guard against both: assert the pre-order
730                // invariant in debug builds, and skip inheritance (treat the
731                // node as a root) for any malformed reference in release builds.
732                debug_assert!(
733                    pi < i,
734                    "compact cascade: non-pre-order arena — node {i}'s parent {pi} \
735                     is not stored before it; inheritance would read default values",
736                );
737                if pi < i {
738                    // Copy only inheritable tier1 fields from parent
739                    result.tier1_enums[i] = result.tier1_enums[pi] & INHERITABLE_TIER1_MASK;
740
741                    // Inheritable tier2: font_size
742                    result.tier2_dims[i].font_size = result.tier2_dims[pi].font_size;
743
744                    // Inheritable tier2_cold: border_spacing, tab_size
745                    result.tier2_cold[i].border_spacing_h = result.tier2_cold[pi].border_spacing_h;
746                    result.tier2_cold[i].border_spacing_v = result.tier2_cold[pi].border_spacing_v;
747                    result.tier2_cold[i].tab_size = result.tier2_cold[pi].tab_size;
748                    // `cursor` is inheritable per spec - a `cursor: pointer`
749                    // on a button has to reach the text inside it.
750                    result.tier2_cold[i].cursor = result.tier2_cold[pi].cursor;
751
752                    // Inheritable tier2b: all text properties
753                    result.tier2b_text[i] = result.tier2b_text[pi];
754                }
755            }
756
757            {
758                let d = &result.tier2_dims[i];
759                cascade_debug!("node[{}] {:?} after-inherit: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={} w={} h={}",
760                    i, nd.node_type, d.padding_top, d.padding_bottom, d.padding_left, d.padding_right,
761                    d.margin_top, d.margin_bottom, d.margin_left, d.margin_right, d.width, d.height);
762            }
763
764            // Step 2: Apply UA CSS defaults for this node type directly to compact values.
765            // UA defaults have lowest cascade priority — overridden by author CSS below.
766            apply_ua_css_to_compact(
767                &nd.node_type,
768                &mut result.tier1_enums[i],
769                &mut result.tier2_dims[i],
770                &mut result.tier2_cold[i],
771                &mut result.tier2b_text[i],
772                &mut result.font_hash_to_families,
773            );
774
775            {
776                let d = &result.tier2_dims[i];
777                cascade_debug!(
778                    "node[{}] {:?} after-UA: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={}",
779                    i,
780                    nd.node_type,
781                    d.padding_top,
782                    d.padding_bottom,
783                    d.padding_left,
784                    d.padding_right,
785                    d.margin_top,
786                    d.margin_bottom,
787                    d.margin_left,
788                    d.margin_right
789                );
790            }
791
792            // Step 2.5: Apply global `*` author CSS (overrides UA, overridden by specific rules)
793            // Apply each `*` rule property individually (not bulk-assign) so we only
794            // override properties the `*` rule actually set, preserving UA CSS for others.
795            //
796            // Per CSS spec, `*` matches all ELEMENTS. Text nodes are not elements —
797            // they must only inherit from their parent. Without this check, `* { color: #666 }`
798            // would overwrite the inherited `color: red` on a Text child of `<p>`,
799            // even though `<p>` correctly got red from `p { color: red }`.
800            if !nd.is_text_node() {
801                for prop in &self.global_css_props {
802                    // (flag already accumulated in the has_global pre-pass)
803                    apply_css_property_to_compact(
804                        prop,
805                        &mut result.tier1_enums[i],
806                        &mut result.tier2_dims[i],
807                        &mut result.tier2_cold[i],
808                        &mut result.tier2b_text[i],
809                        &mut result.font_hash_to_families,
810                    );
811                    update_dom_declared_flags(prop, &mut result.dom_declared_flags);
812                }
813            }
814
815            {
816                let d = &result.tier2_dims[i];
817                cascade_debug!("node[{}] {:?} after-global-star: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={}",
818                    i, nd.node_type, d.padding_top, d.padding_bottom, d.padding_left, d.padding_right,
819                    d.margin_top, d.margin_bottom, d.margin_left, d.margin_right);
820                let n_props = self.css_props.get_slice(i).len();
821                let n_inline = nd.style.iter_inline_properties().count();
822                cascade_debug!(
823                    "node[{}] css_props={} entries, inline={} entries",
824                    i,
825                    n_props,
826                    n_inline
827                );
828                for prop in self.css_props.get_slice(i) {
829                    cascade_debug!(
830                        "node[{}]   css_prop: state={:?} type={:?}",
831                        i,
832                        prop.state,
833                        prop.prop_type
834                    );
835                }
836            }
837
838            // Step 3: Apply this node's CSS properties directly to compact values.
839            // Per-node author CSS has higher specificity than global `*`.
840
841            // Scan css_props (stylesheet rules, sorted by (state, prop_type))
842            // Typically 5-15 entries per node. Only Normal state matters for layout.
843            for prop in self.css_props.get_slice(i) {
844                if prop.state != azul_css::dynamic_selector::PseudoStateType::Normal {
845                    continue;
846                }
847                result.uses_viewport_units |= css_property_uses_viewport_units(&prop.property);
848                apply_css_property_to_compact(
849                    &prop.property,
850                    &mut result.tier1_enums[i],
851                    &mut result.tier2_dims[i],
852                    &mut result.tier2_cold[i],
853                    &mut result.tier2b_text[i],
854                    &mut result.font_hash_to_families,
855                );
856                update_dom_declared_flags(&prop.property, &mut result.dom_declared_flags);
857            }
858
859            {
860                let d = &result.tier2_dims[i];
861                cascade_debug!("node[{}] {:?} after-css-props: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={}",
862                    i, nd.node_type, d.padding_top, d.padding_bottom, d.padding_left, d.padding_right,
863                    d.margin_top, d.margin_bottom, d.margin_left, d.margin_right);
864            }
865
866            // Scan inline CSS (node_data.style — typically 0-3 properties).
867            // Inline CSS has highest specificity — applied last to override stylesheet.
868            for (prop, conds) in nd.style.iter_inline_properties() {
869                // Apply when the conditions hold for the RESTING state:
870                // pseudo-state conditions must be Normal, and every other
871                // condition (viewport/@media, theme, OS...) is evaluated
872                // against the window's dynamic context — the same rule
873                // get_property_slow applies, so the fast path and the slow
874                // path cannot disagree about a conditional property. A
875                // non-pseudo condition also flags the cache, so the window
876                // knows a context change requires a rebuild.
877                let is_normal = conds.as_slice().is_empty()
878                    || conds.as_slice().iter().all(|c| match c {
879                        azul_css::dynamic_selector::DynamicSelector::PseudoState(s) => {
880                            *s == azul_css::dynamic_selector::PseudoStateType::Normal
881                        }
882                        non_pseudo => {
883                            result.has_dynamic_conditions = true;
884                            // Harvest the thresholds this condition can flip
885                            // at — the resize decision regenerates when the
886                            // window crosses one (dedup/sort happens once,
887                            // after the node loop).
888                            {
889                                let mut w = Vec::new();
890                                let mut h = Vec::new();
891                                azul_css::dynamic_selector::collect_viewport_thresholds(
892                                    core::slice::from_ref(non_pseudo),
893                                    &mut w,
894                                    &mut h,
895                                );
896                                result
897                                    .inline_viewport_w
898                                    .extend(w.into_iter().map(f32::to_bits));
899                                result
900                                    .inline_viewport_h
901                                    .extend(h.into_iter().map(f32::to_bits));
902                            }
903                            self.dynamic_context
904                                .as_deref()
905                                .is_some_and(|ctx| non_pseudo.matches(ctx))
906                        }
907                    });
908                if !is_normal {
909                    continue;
910                }
911                result.uses_viewport_units |= css_property_uses_viewport_units(prop);
912                // Layout-critical props dispatched via single-variant `if let` (direct discriminant
913                // COMPARES, no indirect jump). apply_css_property_to_compact's ~100-arm `match` lowers
914                // to a jump table that remill mis-lifts (never reaches the right arm) — same class as the
915                // CssProperty::clone bug. With the conversion-clone fix the prop discriminant is now
916                // correct, so these compares match and apply the value; everything else falls back.
917                // (CssProperty is imported at module top.)
918                if let CssProperty::Width(v) = prop {
919                    result.tier2_dims[i].width = encode_layout_width(v);
920                } else if let CssProperty::Height(v) = prop {
921                    result.tier2_dims[i].height = encode_layout_height(v);
922                } else if let CssProperty::FlexGrow(v) = prop {
923                    if let Some(e) = v.get_property() {
924                        result.tier2_dims[i].flex_grow = encode_flex_u16(e.inner.get());
925                    }
926                } else if let CssProperty::Display(v) = prop {
927                    if let Some(e) = v.get_property() {
928                        let enc = u64::from(layout_display_to_u8(*e));
929                        let m = DISPLAY_MASK;
930                        let s = DISPLAY_SHIFT;
931                        result.tier1_enums[i] =
932                            (result.tier1_enums[i] & !(m << s)) | ((enc & m) << s);
933                    }
934                } else {
935                    apply_css_property_to_compact(
936                        prop,
937                        &mut result.tier1_enums[i],
938                        &mut result.tier2_dims[i],
939                        &mut result.tier2_cold[i],
940                        &mut result.tier2b_text[i],
941                        &mut result.font_hash_to_families,
942                    );
943                }
944                update_dom_declared_flags(prop, &mut result.dom_declared_flags);
945            }
946
947            // Step 4b: user-overridden properties (runtime patches via
948            // `set_css_property` / `restyle_user_property`). The resolver
949            // consults this layer FIRST, so the compact cache must apply it
950            // LAST — the cache is a projection of the same cascade and the
951            // two must agree. Without this step a rebuilt cache resurrected
952            // the pre-patch value: `restyle_user_property` rebuilds the cache
953            // right after recording the override, and the layout fast path
954            // then read the stale display/geometry the patch had just
955            // changed. Same dispatch shape as the inline loop above (the
956            // single-variant `if let`s exist for the remill lift, see there).
957            if let Some(user_props) = self.user_overridden_properties.get(i) {
958                for (_, prop) in user_props {
959                    result.uses_viewport_units |= css_property_uses_viewport_units(prop);
960                    if let CssProperty::Width(v) = prop {
961                        result.tier2_dims[i].width = encode_layout_width(v);
962                    } else if let CssProperty::Height(v) = prop {
963                        result.tier2_dims[i].height = encode_layout_height(v);
964                    } else if let CssProperty::FlexGrow(v) = prop {
965                        if let Some(e) = v.get_property() {
966                            result.tier2_dims[i].flex_grow = encode_flex_u16(e.inner.get());
967                        }
968                    } else if let CssProperty::Display(v) = prop {
969                        if let Some(e) = v.get_property() {
970                            let enc = u64::from(layout_display_to_u8(*e));
971                            let m = DISPLAY_MASK;
972                            let s = DISPLAY_SHIFT;
973                            result.tier1_enums[i] =
974                                (result.tier1_enums[i] & !(m << s)) | ((enc & m) << s);
975                        }
976                    } else {
977                        apply_css_property_to_compact(
978                            prop,
979                            &mut result.tier1_enums[i],
980                            &mut result.tier2_dims[i],
981                            &mut result.tier2_cold[i],
982                            &mut result.tier2b_text[i],
983                            &mut result.font_hash_to_families,
984                        );
985                    }
986                    update_dom_declared_flags(prop, &mut result.dom_declared_flags);
987                }
988            }
989
990            // Resolve font-size from em/percent/pt/etc. to px.
991            // CSS 2.1: inherited font-size is the COMPUTED (px) value, not the specified value.
992            // Pre-order traversal guarantees parent's font_size is already resolved.
993            resolve_font_size_to_px(&mut result.tier2_dims, i, parent_id);
994
995            // Set populated bit
996            if result.tier1_enums[i] != 0 {
997                result.tier1_enums[i] |= TIER1_POPULATED_BIT;
998            }
999        }
1000
1001        // Font dirty tracking.
1002        // When prev_font_hashes is empty (first build for this DOM), mark ALL
1003        // text nodes dirty to force font resolution. Without this, a DOM with
1004        // no explicit font-family (all hashes 0) would compare 0==0 and skip
1005        // resolution, even though font-weight/font-style may differ from the
1006        // cached chains of a previous DOM.
1007        result.font_dirty_nodes.clear();
1008        let first_build = prev_font_hashes.is_empty();
1009        for i in 0..node_count {
1010            let new_hash = result.tier2b_text[i].font_family_hash;
1011            let old_hash = prev_font_hashes.get(i).copied().unwrap_or(0);
1012            if first_build || new_hash != old_hash {
1013                result.font_dirty_nodes.push(i);
1014            }
1015        }
1016        result.prev_font_hashes = result
1017            .tier2b_text
1018            .iter()
1019            .map(|t| t.font_family_hash)
1020            .collect();
1021
1022        // Normalize the harvested viewport thresholds once (pushed raw per
1023        // node above): sorted + deduped by bit pattern.
1024        result.inline_viewport_w.sort_unstable();
1025        result.inline_viewport_w.dedup();
1026        result.inline_viewport_h.sort_unstable();
1027        result.inline_viewport_h.dedup();
1028
1029        result
1030    }
1031}
1032
1033// =============================================================================
1034// Helpers extracted from build_compact_cache_with_inheritance_debug
1035// =============================================================================
1036
1037/// Apply UA CSS defaults for a node type directly to compact values.
1038/// UA defaults have lowest cascade priority — overridden by author CSS.
1039/// Which tier-1 bits a node COPIES FROM ITS PARENT — the packed form of "this
1040/// property is inherited".
1041///
1042/// It must hold a slot exactly when that slot's property is `is_inheritable()`.
1043/// Getting it wrong is silent in BOTH directions: a missing slot makes every
1044/// descendant fall back to the CSS initial value while `get_property_slow` —
1045/// which inherits through `computed_values` — still answers correctly, so the
1046/// two cascade paths disagree about the computed value. That is the failure
1047/// mode that bit the VirtualView overflow default. An extra slot inherits a
1048/// property CSS says does not.
1049///
1050/// `every_inheritable_tier1_property_is_actually_inherited` checks the
1051/// correspondence slot by slot against `CssPropertyType::is_inheritable`, so a
1052/// slot added later cannot quietly omit itself.
1053pub const INHERITABLE_TIER1_MASK: u64 = (FONT_WEIGHT_MASK << FONT_WEIGHT_SHIFT)
1054    | (FONT_STYLE_MASK << FONT_STYLE_SHIFT)
1055    | (TEXT_ALIGN_MASK << TEXT_ALIGN_SHIFT)
1056    | (VISIBILITY_MASK << VISIBILITY_SHIFT)
1057    | (WHITE_SPACE_MASK << WHITE_SPACE_SHIFT)
1058    | (DIRECTION_MASK << DIRECTION_SHIFT)
1059    | (BORDER_COLLAPSE_MASK << BORDER_COLLAPSE_SHIFT)
1060    // `writing-mode` is inheritable per CSS and has had a tier-1 slot all
1061    // along, but was never copied from the parent: `writing-mode: vertical-rl`
1062    // on a container laid its children out horizontally in the compact path,
1063    // while the slow path inherited it correctly. Found by auditing this mask
1064    // against `is_inheritable()` — it was the only disagreement.
1065    | (WRITING_MODE_MASK << WRITING_MODE_SHIFT);
1066
1067fn apply_ua_css_to_compact(
1068    node_type: &crate::dom::NodeType,
1069    tier1: &mut u64,
1070    dims: &mut CompactNodeProps,
1071    cold: &mut CompactNodePropsCold,
1072    text: &mut CompactTextProps,
1073    font_hash_map: &mut alloc::collections::BTreeMap<
1074        u64,
1075        azul_css::props::basic::font::StyleFontFamilyVec,
1076    >,
1077) {
1078    use azul_css::props::property::CssPropertyType as PT2;
1079    const UA_PROPERTY_TYPES: &[PT2] = &[
1080        // Tier1 enum properties
1081        PT2::Display,
1082        PT2::Position,
1083        PT2::Float,
1084        PT2::Clear,
1085        PT2::OverflowX,
1086        PT2::OverflowY,
1087        PT2::BoxSizing,
1088        PT2::FlexDirection,
1089        PT2::FlexWrap,
1090        PT2::JustifyContent,
1091        PT2::AlignItems,
1092        PT2::AlignContent,
1093        PT2::WritingMode,
1094        PT2::FontWeight,
1095        PT2::FontStyle,
1096        PT2::TextAlign,
1097        PT2::Visibility,
1098        PT2::WhiteSpace,
1099        PT2::Direction,
1100        PT2::VerticalAlign,
1101        PT2::BorderCollapse,
1102        // Tier2 dimension properties
1103        PT2::Width,
1104        PT2::Height,
1105        PT2::FontSize,
1106        PT2::MarginTop,
1107        PT2::MarginBottom,
1108        PT2::MarginLeft,
1109        PT2::MarginRight,
1110        PT2::PaddingTop,
1111        PT2::PaddingBottom,
1112        PT2::PaddingLeft,
1113        PT2::PaddingRight,
1114        PT2::BorderTopWidth,
1115        PT2::BorderTopStyle,
1116        PT2::BorderTopColor,
1117        PT2::BorderRightWidth,
1118        PT2::BorderRightStyle,
1119        PT2::BorderRightColor,
1120        PT2::BorderBottomWidth,
1121        PT2::BorderBottomStyle,
1122        PT2::BorderBottomColor,
1123        PT2::BorderLeftWidth,
1124        PT2::BorderLeftStyle,
1125        PT2::BorderLeftColor,
1126        // Text properties
1127        PT2::TextColor,
1128        PT2::LineHeight,
1129        PT2::LetterSpacing,
1130        PT2::WordSpacing,
1131        PT2::TextDecoration,
1132        PT2::Cursor,
1133        PT2::ListStyleType,
1134        // Counters: the UA sheet resets `list-item` on <ol>/<ul> so each list
1135        // restarts numbering. Without these here the has_counter fast-path bit
1136        // stays unset for list containers, compute_counters skips the reset, and
1137        // the list-item counter runs globally (a <ul> then <ol> numbered 1,2 then
1138        // 3,4 instead of restarting at 1).
1139        PT2::CounterReset,
1140        PT2::CounterIncrement,
1141    ];
1142    for pt in UA_PROPERTY_TYPES {
1143        if let Some(ua_prop) = crate::ua_css::get_ua_property(node_type, *pt) {
1144            apply_css_property_to_compact(ua_prop, tier1, dims, cold, text, font_hash_map);
1145        }
1146    }
1147}
1148
1149/// Resolve a node's font-size from relative units (em, %, rem, pt) to absolute px.
1150/// CSS 2.1: inherited font-size is the COMPUTED (px) value, not the specified value.
1151/// Pre-order traversal guarantees parent's `font_size` is already resolved.
1152fn resolve_font_size_to_px(
1153    tier2_dims: &mut [CompactNodeProps],
1154    node_idx: usize,
1155    parent_id: Option<NodeId>,
1156) {
1157    let raw_fs = tier2_dims[node_idx].font_size;
1158    if raw_fs == U32_SENTINEL || raw_fs >= U32_SENTINEL_THRESHOLD {
1159        return;
1160    }
1161    let pv = match decode_pixel_value_u32(raw_fs) {
1162        Some(pv) if pv.metric != SizeMetric::Px => pv,
1163        _ => return,
1164    };
1165
1166    // AUDIT: pre-order arena assumed — the parent's font-size is already
1167    // resolved to px only when `pid < node_idx`. Use checked `get` so an
1168    // out-of-bounds parent ref cannot panic, and require `pid < node_idx` so a
1169    // forward reference falls back to the 16px CSS initial value instead of
1170    // reading an unresolved (still em/%) parent value.
1171    let parent_font_size_px = parent_id.map_or(16.0, |pid| {
1172        let pi = pid.index();
1173        debug_assert!(
1174            pi < node_idx,
1175            "compact font-size resolve: non-pre-order arena — node {node_idx}'s \
1176                 parent {pi} font-size is not yet resolved",
1177        );
1178        if pi < node_idx {
1179            tier2_dims
1180                .get(pi)
1181                .and_then(|p| decode_pixel_value_u32(p.font_size))
1182                .map_or(16.0, |ppv| ppv.number.get())
1183        } else {
1184            16.0
1185        }
1186    });
1187
1188    let resolved_px = match pv.metric {
1189        SizeMetric::Em => pv.number.get() * parent_font_size_px,
1190        SizeMetric::Percent => pv.number.get() / 100.0 * parent_font_size_px,
1191        SizeMetric::Rem => {
1192            // rem = the ROOT element's font size. For the root itself that is circular,
1193            // so CSS resolves root rem against the 16px INITIAL value (Selectors/Values:
1194            // "when specified on the root element, rem refers to the initial value").
1195            // tier2_dims[0] IS the root's slot, but while resolving the root it still
1196            // holds the root's own unresolved raw rem — so `html { font-size: 2rem }`
1197            // computed 2*2 = 4px instead of 2*16 = 32px.
1198            let rem_base = if parent_id.is_none() {
1199                16.0
1200            } else {
1201                tier2_dims
1202                    .first()
1203                    .and_then(|r| decode_pixel_value_u32(r.font_size))
1204                    .map_or(16.0, |rpv| rpv.number.get())
1205            };
1206            rem_base * pv.number.get()
1207        }
1208        SizeMetric::Pt => pv.number.get() * 96.0 / 72.0,
1209        _ => pv.number.get(),
1210    };
1211    tier2_dims[node_idx].font_size =
1212        encode_pixel_value_u32(&azul_css::props::basic::pixel::PixelValue::px(resolved_px));
1213}
1214
1215/// Does this property's value use a viewport-relative unit (vw/vh/vmin/vmax)?
1216///
1217/// Feeds `CompactLayoutCache::uses_viewport_units` from the property loops of
1218/// `build_compact_cache_with_inheritance` — one call per (node, property), on
1219/// data the loops are already iterating. See that field's docs for what the
1220/// flag buys (solver3 skips per-resize invalidation of every inline collection
1221/// for the overwhelming majority of documents that never mention a viewport
1222/// unit).
1223///
1224/// Coverage = the pixel-carrying properties the compact cache itself encodes,
1225/// which is a superset of what inline collection/measurement reads (the only
1226/// consumer). `calc()` widths/heights are flagged CONSERVATIVELY without
1227/// walking the AST — a false positive merely keeps the old always-invalidate
1228/// behaviour.
1229fn css_property_uses_viewport_units(prop: &CssProperty) -> bool {
1230    use azul_css::props::basic::length::SizeMetric;
1231    use azul_css::props::basic::pixel::PixelValue;
1232    const fn pv(p: &PixelValue) -> bool {
1233        matches!(
1234            p.metric,
1235            SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax
1236        )
1237    }
1238    fn inner<T: HasInnerPixelValue>(v: &CssPropertyValue<T>) -> bool {
1239        matches!(v, CssPropertyValue::Exact(x) if pv(&x.get_inner_pixel()))
1240    }
1241    use azul_css::props::layout::dimensions::{LayoutHeight, LayoutWidth};
1242    use azul_css::props::layout::flex::LayoutFlexBasis;
1243    match prop {
1244        CssProperty::Width(v) => matches!(v, CssPropertyValue::Exact(w) if match w {
1245            LayoutWidth::Px(p) | LayoutWidth::FitContent(p) => pv(p),
1246            LayoutWidth::Calc(_) => true,
1247            _ => false,
1248        }),
1249        CssProperty::Height(v) => matches!(v, CssPropertyValue::Exact(h) if match h {
1250            LayoutHeight::Px(p) | LayoutHeight::FitContent(p) => pv(p),
1251            LayoutHeight::Calc(_) => true,
1252            _ => false,
1253        }),
1254        CssProperty::FlexBasis(v) => {
1255            matches!(v, CssPropertyValue::Exact(LayoutFlexBasis::Exact(p)) if pv(p))
1256        }
1257        CssProperty::MinWidth(v) => inner(v),
1258        CssProperty::MaxWidth(v) => inner(v),
1259        CssProperty::MinHeight(v) => inner(v),
1260        CssProperty::MaxHeight(v) => inner(v),
1261        CssProperty::FontSize(v) => inner(v),
1262        CssProperty::PaddingTop(v) => inner(v),
1263        CssProperty::PaddingRight(v) => inner(v),
1264        CssProperty::PaddingBottom(v) => inner(v),
1265        CssProperty::PaddingLeft(v) => inner(v),
1266        CssProperty::MarginTop(v) => inner(v),
1267        CssProperty::MarginRight(v) => inner(v),
1268        CssProperty::MarginBottom(v) => inner(v),
1269        CssProperty::MarginLeft(v) => inner(v),
1270        CssProperty::BorderTopWidth(v) => inner(v),
1271        CssProperty::BorderRightWidth(v) => inner(v),
1272        CssProperty::BorderBottomWidth(v) => inner(v),
1273        CssProperty::BorderLeftWidth(v) => inner(v),
1274        CssProperty::Top(v) => inner(v),
1275        CssProperty::Right(v) => inner(v),
1276        CssProperty::Bottom(v) => inner(v),
1277        CssProperty::Left(v) => inner(v),
1278        CssProperty::LetterSpacing(v) => inner(v),
1279        CssProperty::WordSpacing(v) => inner(v),
1280        CssProperty::TextIndent(v) => inner(v),
1281        CssProperty::TabSize(v) => inner(v),
1282        _ => false,
1283    }
1284}
1285
1286// =============================================================================
1287// Direct CssProperty → compact field writer
1288// =============================================================================
1289
1290/// Apply a single `CssProperty` directly to the compact representation.
1291/// Called once per property per node — replaces the old 56+ getter approach.
1292#[inline]
1293// The scrollbar-* and counter-* arms have identical bodies
1294// (`if v.get_property().is_some() { flags |= … }`) but each variant wraps a
1295// DIFFERENT value type (StyleBackgroundContentValue, LayoutScrollbarWidthValue,
1296// StyleScrollbarColorValue, CounterResetValue, CounterIncrementValue, …), so an
1297// or-pattern binding `v` cannot be expressed across them.
1298#[allow(clippy::match_same_arms)]
1299// fixed-point encoders: z-index / line-height are range-checked before the
1300// narrowing cast, and opacity is clamped to [0,1] then scaled to [0,254] (u8).
1301#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1302#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
1303fn apply_css_property_to_compact(
1304    prop: &CssProperty,
1305    tier1: &mut u64,
1306    dims: &mut CompactNodeProps,
1307    cold: &mut CompactNodePropsCold,
1308    text: &mut CompactTextProps,
1309    font_hash_map: &mut alloc::collections::BTreeMap<
1310        u64,
1311        azul_css::props::basic::font::StyleFontFamilyVec,
1312    >,
1313) {
1314    macro_rules! set_tier1 {
1315        ($v:expr, $shift:expr, $mask:expr, $encoder:ident) => {
1316            if let Some(exact) = $v.get_property() {
1317                let encoded = u64::from($encoder(*exact));
1318                let shifted_mask = $mask << $shift;
1319                *tier1 = (*tier1 & !shifted_mask) | ((encoded & $mask) << $shift);
1320            }
1321        };
1322    }
1323
1324    match prop {
1325        // Tier 1 enums
1326        CssProperty::Display(v) => set_tier1!(v, DISPLAY_SHIFT, DISPLAY_MASK, layout_display_to_u8),
1327        CssProperty::Position(v) => {
1328            set_tier1!(v, POSITION_SHIFT, POSITION_MASK, layout_position_to_u8);
1329        }
1330        CssProperty::Float(v) => set_tier1!(v, FLOAT_SHIFT, FLOAT_MASK, layout_float_to_u8),
1331        CssProperty::OverflowX(v) => {
1332            set_tier1!(v, OVERFLOW_X_SHIFT, OVERFLOW_MASK, layout_overflow_to_u8);
1333        }
1334        CssProperty::OverflowY(v) => {
1335            set_tier1!(v, OVERFLOW_Y_SHIFT, OVERFLOW_MASK, layout_overflow_to_u8);
1336        }
1337        // +spec:overflow:17654b - overflow-block / overflow-inline resolve to
1338        // the physical axis through the writing mode. Application is in
1339        // declaration order (a later physical declaration overwrites the
1340        // same tier1 slot and vice versa), which is exactly CSS's
1341        // equal-specificity last-wins rule for logical/physical pairs. The
1342        // writing mode is read from tier1 AT THIS POINT: the inherited value
1343        // is already present (inheritance runs first), so only the exotic
1344        // "writing-mode declared AFTER a logical overflow on the SAME node"
1345        // ordering maps against the pre-declaration mode.
1346        CssProperty::OverflowBlock(v) => {
1347            if let Some(val) = v.get_property() {
1348                let wm_bits = ((*tier1 >> WRITING_MODE_SHIFT) & WRITING_MODE_MASK) as u8;
1349                let vertical = wm_bits
1350                    == layout_writing_mode_to_u8(
1351                        azul_css::props::layout::wrapping::LayoutWritingMode::VerticalRl,
1352                    )
1353                    || wm_bits
1354                        == layout_writing_mode_to_u8(
1355                            azul_css::props::layout::wrapping::LayoutWritingMode::VerticalLr,
1356                        );
1357                let shift = if vertical {
1358                    OVERFLOW_X_SHIFT
1359                } else {
1360                    OVERFLOW_Y_SHIFT
1361                };
1362                let enc = u64::from(layout_overflow_to_u8(*val));
1363                *tier1 = (*tier1 & !(OVERFLOW_MASK << shift)) | ((enc & OVERFLOW_MASK) << shift);
1364            }
1365        }
1366        CssProperty::OverflowInline(v) => {
1367            if let Some(val) = v.get_property() {
1368                let wm_bits = ((*tier1 >> WRITING_MODE_SHIFT) & WRITING_MODE_MASK) as u8;
1369                let vertical = wm_bits
1370                    == layout_writing_mode_to_u8(
1371                        azul_css::props::layout::wrapping::LayoutWritingMode::VerticalRl,
1372                    )
1373                    || wm_bits
1374                        == layout_writing_mode_to_u8(
1375                            azul_css::props::layout::wrapping::LayoutWritingMode::VerticalLr,
1376                        );
1377                let shift = if vertical {
1378                    OVERFLOW_Y_SHIFT
1379                } else {
1380                    OVERFLOW_X_SHIFT
1381                };
1382                let enc = u64::from(layout_overflow_to_u8(*val));
1383                *tier1 = (*tier1 & !(OVERFLOW_MASK << shift)) | ((enc & OVERFLOW_MASK) << shift);
1384            }
1385        }
1386        CssProperty::BoxSizing(v) => set_tier1!(
1387            v,
1388            BOX_SIZING_SHIFT,
1389            BOX_SIZING_MASK,
1390            layout_box_sizing_to_u8
1391        ),
1392        CssProperty::FlexDirection(v) => set_tier1!(
1393            v,
1394            FLEX_DIRECTION_SHIFT,
1395            FLEX_DIR_MASK,
1396            layout_flex_direction_to_u8
1397        ),
1398        CssProperty::FlexWrap(v) => {
1399            set_tier1!(v, FLEX_WRAP_SHIFT, FLEX_WRAP_MASK, layout_flex_wrap_to_u8);
1400        }
1401        CssProperty::JustifyContent(v) => set_tier1!(
1402            v,
1403            JUSTIFY_CONTENT_SHIFT,
1404            JUSTIFY_MASK,
1405            layout_justify_content_to_u8
1406        ),
1407        CssProperty::AlignItems(v) => {
1408            set_tier1!(v, ALIGN_ITEMS_SHIFT, ALIGN_MASK, layout_align_items_to_u8);
1409        }
1410        CssProperty::AlignContent(v) => set_tier1!(
1411            v,
1412            ALIGN_CONTENT_SHIFT,
1413            ALIGN_MASK,
1414            layout_align_content_to_u8
1415        ),
1416        CssProperty::WritingMode(v) => set_tier1!(
1417            v,
1418            WRITING_MODE_SHIFT,
1419            WRITING_MODE_MASK,
1420            layout_writing_mode_to_u8
1421        ),
1422        CssProperty::Clear(v) => set_tier1!(v, CLEAR_SHIFT, CLEAR_MASK, layout_clear_to_u8),
1423        CssProperty::FontWeight(v) => set_tier1!(
1424            v,
1425            FONT_WEIGHT_SHIFT,
1426            FONT_WEIGHT_MASK,
1427            style_font_weight_to_u8
1428        ),
1429        CssProperty::FontStyle(v) => {
1430            set_tier1!(v, FONT_STYLE_SHIFT, FONT_STYLE_MASK, style_font_style_to_u8);
1431        }
1432        CssProperty::TextAlign(v) => {
1433            set_tier1!(v, TEXT_ALIGN_SHIFT, TEXT_ALIGN_MASK, style_text_align_to_u8);
1434        }
1435        CssProperty::Visibility(v) => {
1436            set_tier1!(v, VISIBILITY_SHIFT, VISIBILITY_MASK, style_visibility_to_u8);
1437        }
1438        CssProperty::WhiteSpace(v) => set_tier1!(
1439            v,
1440            WHITE_SPACE_SHIFT,
1441            WHITE_SPACE_MASK,
1442            style_white_space_to_u8
1443        ),
1444        CssProperty::Direction(v) => {
1445            set_tier1!(v, DIRECTION_SHIFT, DIRECTION_MASK, style_direction_to_u8);
1446        }
1447        CssProperty::VerticalAlign(v) => set_tier1!(
1448            v,
1449            VERTICAL_ALIGN_SHIFT,
1450            VERTICAL_ALIGN_MASK,
1451            style_vertical_align_to_u8
1452        ),
1453        // `cursor` is INHERITABLE and is resolved on every mouse move to pick
1454        // the pointer shape, so it wants a flat per-node read. It is NOT in
1455        // the tier-1 word: that word is full, and the bit range it was given
1456        // belonged to `align-self`, so writing a cursor re-aligned the node.
1457        CssProperty::Cursor(v) => {
1458            if let Some(exact) = v.get_property() {
1459                cold.cursor = cursor_to_u8(*exact);
1460            }
1461        }
1462        CssProperty::BorderCollapse(v) => set_tier1!(
1463            v,
1464            BORDER_COLLAPSE_SHIFT,
1465            BORDER_COLLAPSE_MASK,
1466            border_collapse_to_u8
1467        ),
1468        CssProperty::AlignSelf(v) => set_tier1!(
1469            v,
1470            ALIGN_SELF_SHIFT,
1471            ALIGN_SELF_MASK,
1472            layout_align_self_to_u8
1473        ),
1474        CssProperty::JustifySelf(v) => set_tier1!(
1475            v,
1476            JUSTIFY_SELF_SHIFT,
1477            JUSTIFY_SELF_MASK,
1478            layout_justify_self_to_u8
1479        ),
1480        CssProperty::GridAutoFlow(v) => set_tier1!(
1481            v,
1482            GRID_AUTO_FLOW_SHIFT,
1483            GRID_AUTO_FLOW_MASK,
1484            layout_grid_auto_flow_to_u8
1485        ),
1486        CssProperty::JustifyItems(v) => set_tier1!(
1487            v,
1488            JUSTIFY_ITEMS_SHIFT,
1489            JUSTIFY_ITEMS_MASK,
1490            layout_justify_items_to_u8
1491        ),
1492
1493        // Tier 2 dimensions
1494        CssProperty::Width(v) => {
1495            dims.width = encode_layout_width(v);
1496        }
1497        CssProperty::Height(v) => {
1498            dims.height = encode_layout_height(v);
1499        }
1500        CssProperty::MinWidth(v) => {
1501            dims.min_width = encode_pixel_prop(v);
1502        }
1503        CssProperty::MaxWidth(v) => {
1504            dims.max_width = encode_pixel_prop(v);
1505        }
1506        CssProperty::MinHeight(v) => {
1507            dims.min_height = encode_pixel_prop(v);
1508        }
1509        CssProperty::MaxHeight(v) => {
1510            dims.max_height = encode_pixel_prop(v);
1511        }
1512        CssProperty::FlexBasis(v) => {
1513            dims.flex_basis = encode_flex_basis(v);
1514        }
1515        CssProperty::FontSize(v) => {
1516            dims.font_size = encode_pixel_prop(v);
1517        }
1518        CssProperty::PaddingTop(v) => {
1519            dims.padding_top = encode_css_pixel_as_i16(v);
1520        }
1521        CssProperty::PaddingRight(v) => {
1522            dims.padding_right = encode_css_pixel_as_i16(v);
1523        }
1524        CssProperty::PaddingBottom(v) => {
1525            dims.padding_bottom = encode_css_pixel_as_i16(v);
1526        }
1527        CssProperty::PaddingLeft(v) => {
1528            dims.padding_left = encode_css_pixel_as_i16(v);
1529        }
1530        CssProperty::MarginTop(v) => {
1531            dims.margin_top = encode_margin_i16(v);
1532        }
1533        CssProperty::MarginRight(v) => {
1534            dims.margin_right = encode_margin_i16(v);
1535        }
1536        CssProperty::MarginBottom(v) => {
1537            dims.margin_bottom = encode_margin_i16(v);
1538        }
1539        CssProperty::MarginLeft(v) => {
1540            dims.margin_left = encode_margin_i16(v);
1541        }
1542        CssProperty::BorderTopWidth(v) => {
1543            dims.border_top_width = encode_css_pixel_as_i16(v);
1544        }
1545        CssProperty::BorderRightWidth(v) => {
1546            dims.border_right_width = encode_css_pixel_as_i16(v);
1547        }
1548        CssProperty::BorderBottomWidth(v) => {
1549            dims.border_bottom_width = encode_css_pixel_as_i16(v);
1550        }
1551        CssProperty::BorderLeftWidth(v) => {
1552            dims.border_left_width = encode_css_pixel_as_i16(v);
1553        }
1554        CssProperty::Top(v) => {
1555            dims.top = encode_css_pixel_as_i16(v);
1556        }
1557        CssProperty::Right(v) => {
1558            dims.right = encode_css_pixel_as_i16(v);
1559        }
1560        CssProperty::Bottom(v) => {
1561            dims.bottom = encode_css_pixel_as_i16(v);
1562        }
1563        CssProperty::Left(v) => {
1564            dims.left = encode_css_pixel_as_i16(v);
1565        }
1566        CssProperty::FlexGrow(v) => {
1567            if let Some(exact) = v.get_property() {
1568                dims.flex_grow = encode_flex_u16(exact.inner.get());
1569            }
1570        }
1571        CssProperty::FlexShrink(v) => {
1572            if let Some(exact) = v.get_property() {
1573                dims.flex_shrink = encode_flex_u16(exact.inner.get());
1574            }
1575        }
1576
1577        CssProperty::RowGap(v) => {
1578            if let Some(g) = v.get_property() {
1579                if g.inner.metric == SizeMetric::Px {
1580                    dims.row_gap = encode_resolved_px_i16(g.inner.number.get());
1581                }
1582            }
1583        }
1584        CssProperty::ColumnGap(v) => {
1585            if let Some(g) = v.get_property() {
1586                if g.inner.metric == SizeMetric::Px {
1587                    dims.column_gap = encode_resolved_px_i16(g.inner.number.get());
1588                }
1589            }
1590        }
1591        CssProperty::Gap(v) => {
1592            if let Some(g) = v.get_property() {
1593                if g.inner.metric == SizeMetric::Px {
1594                    let enc = encode_resolved_px_i16(g.inner.number.get());
1595                    dims.row_gap = enc;
1596                    dims.column_gap = enc;
1597                }
1598            }
1599        }
1600
1601        // Grid placement (compact encoding for common Auto/Line cases)
1602        CssProperty::GridColumn(v) => {
1603            if let Some(gp) = v.get_property() {
1604                cold.grid_col_start = encode_grid_line(&gp.grid_start);
1605                cold.grid_col_end = encode_grid_line(&gp.grid_end);
1606            }
1607        }
1608        CssProperty::GridRow(v) => {
1609            if let Some(gp) = v.get_property() {
1610                cold.grid_row_start = encode_grid_line(&gp.grid_start);
1611                cold.grid_row_end = encode_grid_line(&gp.grid_end);
1612            }
1613        }
1614
1615        // Tier 2 cold
1616        CssProperty::ZIndex(v) => {
1617            if let Some(exact) = v.get_property() {
1618                match exact {
1619                    LayoutZIndex::Auto => cold.z_index = I16_AUTO,
1620                    LayoutZIndex::Integer(z) => {
1621                        // Two-sided (see the tier2_cold path above): a large negative z
1622                        // used to wrap positive via `*z as i16`. Escape both ends.
1623                        cold.z_index = if *z >= -32768 && *z < i32::from(I16_SENTINEL_THRESHOLD) {
1624                            *z as i16
1625                        } else {
1626                            I16_SENTINEL
1627                        };
1628                    }
1629                }
1630            }
1631        }
1632        CssProperty::BorderTopStyle(v) => {
1633            if let Some(exact) = v.get_property() {
1634                let bs = u16::from(border_style_to_u8(exact.inner));
1635                cold.border_styles_packed = (cold.border_styles_packed & !0x000F) | bs;
1636            }
1637        }
1638        CssProperty::BorderRightStyle(v) => {
1639            if let Some(exact) = v.get_property() {
1640                let bs = u16::from(border_style_to_u8(exact.inner));
1641                cold.border_styles_packed = (cold.border_styles_packed & !0x00F0) | (bs << 4);
1642            }
1643        }
1644        CssProperty::BorderBottomStyle(v) => {
1645            if let Some(exact) = v.get_property() {
1646                let bs = u16::from(border_style_to_u8(exact.inner));
1647                cold.border_styles_packed = (cold.border_styles_packed & !0x0F00) | (bs << 8);
1648            }
1649        }
1650        CssProperty::BorderLeftStyle(v) => {
1651            if let Some(exact) = v.get_property() {
1652                let bs = u16::from(border_style_to_u8(exact.inner));
1653                cold.border_styles_packed = (cold.border_styles_packed & !0xF000) | (bs << 12);
1654            }
1655        }
1656        CssProperty::BorderTopColor(v) => {
1657            if let Some(c) = v.get_property() {
1658                cold.border_top_color = encode_color_u32(&c.inner);
1659            }
1660        }
1661        CssProperty::BorderRightColor(v) => {
1662            if let Some(c) = v.get_property() {
1663                cold.border_right_color = encode_color_u32(&c.inner);
1664            }
1665        }
1666        CssProperty::BorderBottomColor(v) => {
1667            if let Some(c) = v.get_property() {
1668                cold.border_bottom_color = encode_color_u32(&c.inner);
1669            }
1670        }
1671        CssProperty::BorderLeftColor(v) => {
1672            if let Some(c) = v.get_property() {
1673                cold.border_left_color = encode_color_u32(&c.inner);
1674            }
1675        }
1676        CssProperty::BorderSpacing(v) => {
1677            if let Some(spacing) = v.get_property() {
1678                if spacing.horizontal.metric == SizeMetric::Px {
1679                    cold.border_spacing_h = encode_resolved_px_i16(spacing.horizontal.number.get());
1680                }
1681                if spacing.vertical.metric == SizeMetric::Px {
1682                    cold.border_spacing_v = encode_resolved_px_i16(spacing.vertical.number.get());
1683                }
1684            }
1685        }
1686        CssProperty::TabSize(v) => {
1687            cold.tab_size = encode_css_pixel_as_i16(v);
1688        }
1689
1690        // Tier 2b text
1691        CssProperty::TextColor(v) => {
1692            if let Some(color) = v.get_property() {
1693                let c = &color.inner;
1694                text.text_color = (u32::from(c.r) << 24)
1695                    | (u32::from(c.g) << 16)
1696                    | (u32::from(c.b) << 8)
1697                    | u32::from(c.a);
1698            }
1699        }
1700        CssProperty::FontFamily(v) => {
1701            if let Some(families) = v.get_property() {
1702                let mut hasher = DefaultHasher::new();
1703                families.hash(&mut hasher);
1704                let h = hasher.finish();
1705                let h = if h == 0 { 1 } else { h };
1706                text.font_family_hash = h;
1707                font_hash_map.insert(h, families.clone());
1708            }
1709        }
1710        CssProperty::LineHeight(v) => {
1711            if let Some(lh) = v.get_property() {
1712                // Split scale by SIGN (see the builder's line-height pre-pass
1713                // and compact_cache.rs field doc): negative normalized =
1714                // absolute px, stored as -px x 10; positive = multiple,
1715                // stored x 1000. A single x1000 scale overflowed i16 for any
1716                // absolute line-height above 32.76px and silently became
1717                // "normal" via the sentinel.
1718                let n = lh.inner.normalized();
1719                let stored = if n < 0.0 {
1720                    ((n * 10.0).round() as i32).max(-32768)
1721                } else {
1722                    (n * 1000.0).round() as i32
1723                };
1724                if stored >= -32768 && stored < i32::from(I16_SENTINEL_THRESHOLD) {
1725                    text.line_height = stored as i16;
1726                } else {
1727                    text.line_height = I16_SENTINEL;
1728                }
1729            }
1730        }
1731        CssProperty::LetterSpacing(v) => {
1732            text.letter_spacing = encode_css_pixel_as_i16(v);
1733        }
1734        CssProperty::WordSpacing(v) => {
1735            text.word_spacing = encode_css_pixel_as_i16(v);
1736        }
1737        CssProperty::TextIndent(v) => {
1738            text.text_indent = encode_css_pixel_as_i16(v);
1739        }
1740
1741        // Border radii (cold): encode px × 10 into i16; sentinel stays = unset/0
1742        CssProperty::BorderTopLeftRadius(v) => {
1743            if let Some(exact) = v.get_property() {
1744                if exact.inner.metric == SizeMetric::Px {
1745                    cold.border_top_left_radius = encode_resolved_px_i16(exact.inner.number.get());
1746                }
1747            }
1748        }
1749        CssProperty::BorderTopRightRadius(v) => {
1750            if let Some(exact) = v.get_property() {
1751                if exact.inner.metric == SizeMetric::Px {
1752                    cold.border_top_right_radius = encode_resolved_px_i16(exact.inner.number.get());
1753                }
1754            }
1755        }
1756        CssProperty::BorderBottomLeftRadius(v) => {
1757            if let Some(exact) = v.get_property() {
1758                if exact.inner.metric == SizeMetric::Px {
1759                    cold.border_bottom_left_radius =
1760                        encode_resolved_px_i16(exact.inner.number.get());
1761                }
1762            }
1763        }
1764        CssProperty::BorderBottomRightRadius(v) => {
1765            if let Some(exact) = v.get_property() {
1766                if exact.inner.metric == SizeMetric::Px {
1767                    cold.border_bottom_right_radius =
1768                        encode_resolved_px_i16(exact.inner.number.get());
1769                }
1770            }
1771        }
1772
1773        // Opacity: encode as 0-254, 255 = sentinel (unset/default = 1.0)
1774        CssProperty::Opacity(v) => {
1775            if let Some(exact) = v.get_property() {
1776                let o = exact.inner.normalized().clamp(0.0, 1.0);
1777                let byte = (o * 254.0).round() as u8;
1778                // byte is in [0, 254], never collides with OPACITY_SENTINEL=255
1779                cold.opacity = byte;
1780            }
1781        }
1782
1783        // has-flags: set bit whenever property is set (regardless of value).
1784        // Getter uses this as a fast "is the default" bail-out.
1785        CssProperty::Transform(v) => {
1786            if v.get_property().is_some() {
1787                cold.hot_flags |= HOT_FLAG_HAS_TRANSFORM;
1788            }
1789        }
1790        CssProperty::TransformOrigin(v) => {
1791            if v.get_property().is_some() {
1792                cold.hot_flags |= HOT_FLAG_HAS_TRANSFORM_ORIGIN;
1793            }
1794        }
1795        // All four shadow sides wrap the same StyleBoxShadowValue and set the
1796        // single has-box-shadow bit.
1797        CssProperty::BoxShadowTop(v)
1798        | CssProperty::BoxShadowBottom(v)
1799        | CssProperty::BoxShadowLeft(v)
1800        | CssProperty::BoxShadowRight(v) => {
1801            if v.get_property().is_some() {
1802                cold.hot_flags |= HOT_FLAG_HAS_BOX_SHADOW;
1803            }
1804        }
1805        CssProperty::TextDecoration(v) => {
1806            if v.get_property().is_some() {
1807                cold.hot_flags |= HOT_FLAG_HAS_TEXT_DECORATION;
1808            }
1809        }
1810        CssProperty::ScrollbarGutter(v) => {
1811            if let Some(exact) = v.get_property() {
1812                use azul_css::props::layout::overflow::StyleScrollbarGutter;
1813                let bits: u8 = match exact {
1814                    StyleScrollbarGutter::Auto => SCROLLBAR_GUTTER_AUTO,
1815                    StyleScrollbarGutter::Stable => SCROLLBAR_GUTTER_STABLE,
1816                    StyleScrollbarGutter::StableBothEdges => SCROLLBAR_GUTTER_BOTH_EDGES,
1817                };
1818                cold.hot_flags = (cold.hot_flags & !HOT_FLAG_SCROLLBAR_GUTTER_MASK)
1819                    | ((bits << HOT_FLAG_SCROLLBAR_GUTTER_SHIFT) & HOT_FLAG_SCROLLBAR_GUTTER_MASK);
1820            }
1821        }
1822        CssProperty::BackgroundContent(v) => {
1823            if v.get_property().is_some() {
1824                cold.hot_flags |= HOT_FLAG_HAS_BACKGROUND;
1825            }
1826        }
1827        CssProperty::ClipPath(v) => {
1828            if v.get_property().is_some() {
1829                cold.hot_flags |= HOT_FLAG_HAS_CLIP_PATH;
1830            }
1831        }
1832
1833        // Any scrollbar customisation sets the single `has_any_scrollbar_css`
1834        // bit. When unset, get_scrollbar_style can bail to UA defaults without
1835        // doing 8 cascade walks.
1836        CssProperty::ScrollbarTrack(v) => {
1837            if v.get_property().is_some() {
1838                cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS;
1839            }
1840        }
1841        CssProperty::ScrollbarThumb(v) => {
1842            if v.get_property().is_some() {
1843                cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS;
1844            }
1845        }
1846        CssProperty::ScrollbarButton(v) => {
1847            if v.get_property().is_some() {
1848                cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS;
1849            }
1850        }
1851        CssProperty::ScrollbarCorner(v) => {
1852            if v.get_property().is_some() {
1853                cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS;
1854            }
1855        }
1856        CssProperty::ScrollbarWidth(v) => {
1857            if v.get_property().is_some() {
1858                cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS;
1859            }
1860        }
1861        CssProperty::ScrollbarColor(v) => {
1862            if v.get_property().is_some() {
1863                cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS;
1864            }
1865        }
1866        CssProperty::ScrollbarVisibility(v) => {
1867            if v.get_property().is_some() {
1868                cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS;
1869            }
1870        }
1871        CssProperty::ScrollbarFadeDelay(v) => {
1872            if v.get_property().is_some() {
1873                cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS;
1874            }
1875        }
1876        CssProperty::ScrollbarFadeDuration(v) => {
1877            if v.get_property().is_some() {
1878                cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS;
1879            }
1880        }
1881
1882        // Rare paint/layout props with dedicated fast-path bits.
1883        CssProperty::CounterReset(v) => {
1884            if v.get_property().is_some() {
1885                cold.extra_flags |= EXTRA_FLAG_HAS_COUNTER;
1886            }
1887        }
1888        CssProperty::CounterIncrement(v) => {
1889            if v.get_property().is_some() {
1890                cold.extra_flags |= EXTRA_FLAG_HAS_COUNTER;
1891            }
1892        }
1893        // Both break-before/after wrap PageBreakValue and set the has-break bit.
1894        CssProperty::BreakBefore(v) | CssProperty::BreakAfter(v) => {
1895            if v.get_property().is_some() {
1896                cold.extra_flags |= EXTRA_FLAG_HAS_BREAK;
1897            }
1898        }
1899        CssProperty::TextOrientation(v) => {
1900            if v.get_property().is_some() {
1901                cold.extra_flags |= EXTRA_FLAG_HAS_TEXT_ORIENTATION;
1902            }
1903        }
1904        CssProperty::TextShadow(v) => {
1905            if v.get_property().is_some() {
1906                cold.extra_flags |= EXTRA_FLAG_HAS_TEXT_SHADOW;
1907            }
1908        }
1909        CssProperty::BackdropFilter(v) => {
1910            if v.get_property().is_some() {
1911                cold.extra_flags |= EXTRA_FLAG_HAS_BACKDROP_FILTER;
1912            }
1913        }
1914        CssProperty::Filter(v) => {
1915            if v.get_property().is_some() {
1916                cold.extra_flags |= EXTRA_FLAG_HAS_FILTER;
1917            }
1918        }
1919        CssProperty::MixBlendMode(v) => {
1920            if v.get_property().is_some() {
1921                cold.extra_flags |= EXTRA_FLAG_HAS_MIX_BLEND_MODE;
1922            }
1923        }
1924
1925        // Non-compact properties (background, etc.) — handled by get_property_slow fallback
1926        _ => {}
1927    }
1928}
1929
1930/// OR the DOM-level declared-flag for rarely-set text properties. Called once
1931/// per property per node so that when a flag bit is clear, callers
1932/// (e.g. `translate_to_text3_constraints`) can skip the cascade walk and use
1933/// the default value — the slow walk would never find a declaration anyway.
1934#[allow(clippy::too_many_lines)] // large but cohesive: one arm per declared-flag property
1935const fn update_dom_declared_flags(prop: &CssProperty, flags: &mut u32) {
1936    // Only mark if the property value is actually "set" (not Auto/Initial/etc.).
1937    // Using `get_property().is_some()` mirrors the pattern used elsewhere in
1938    // this builder for has-X bits.
1939    match prop {
1940        CssProperty::ShapeInside(v) => {
1941            if v.get_property().is_some() {
1942                *flags |= DOM_HAS_SHAPE_INSIDE;
1943            }
1944        }
1945        CssProperty::ShapeOutside(v) => {
1946            if v.get_property().is_some() {
1947                *flags |= DOM_HAS_SHAPE_OUTSIDE;
1948            }
1949        }
1950        CssProperty::TextJustify(v) => {
1951            if v.get_property().is_some() {
1952                *flags |= DOM_HAS_TEXT_JUSTIFY;
1953            }
1954        }
1955        CssProperty::TextIndent(v) => {
1956            if v.get_property().is_some() {
1957                *flags |= DOM_HAS_TEXT_INDENT;
1958            }
1959        }
1960        CssProperty::ColumnCount(v) => {
1961            if v.get_property().is_some() {
1962                *flags |= DOM_HAS_COLUMN_COUNT;
1963            }
1964        }
1965        CssProperty::ColumnGap(v) => {
1966            if v.get_property().is_some() {
1967                *flags |= DOM_HAS_COLUMN_GAP;
1968            }
1969        }
1970        CssProperty::ColumnWidth(v) => {
1971            if v.get_property().is_some() {
1972                *flags |= DOM_HAS_COLUMN_WIDTH;
1973            }
1974        }
1975        CssProperty::InitialLetter(v) => {
1976            if v.get_property().is_some() {
1977                *flags |= DOM_HAS_INITIAL_LETTER;
1978            }
1979        }
1980        CssProperty::InitialLetterAlign(v) => {
1981            if v.get_property().is_some() {
1982                *flags |= DOM_HAS_INITIAL_LETTER_ALIGN;
1983            }
1984        }
1985        CssProperty::LineClamp(v) => {
1986            if v.get_property().is_some() {
1987                *flags |= DOM_HAS_LINE_CLAMP;
1988            }
1989        }
1990        CssProperty::HangingPunctuation(v) => {
1991            if v.get_property().is_some() {
1992                *flags |= DOM_HAS_HANGING_PUNCTUATION;
1993            }
1994        }
1995        CssProperty::TextCombineUpright(v) => {
1996            if v.get_property().is_some() {
1997                *flags |= DOM_HAS_TEXT_COMBINE_UPRIGHT;
1998            }
1999        }
2000        CssProperty::ExclusionMargin(v) => {
2001            if v.get_property().is_some() {
2002                *flags |= DOM_HAS_EXCLUSION_MARGIN;
2003            }
2004        }
2005        CssProperty::ShapeMargin(v) => {
2006            if v.get_property().is_some() {
2007                *flags |= DOM_HAS_SHAPE_MARGIN;
2008            }
2009        }
2010        CssProperty::HyphenationLanguage(v) => {
2011            if v.get_property().is_some() {
2012                *flags |= DOM_HAS_HYPHENATION_LANGUAGE;
2013            }
2014        }
2015        CssProperty::UnicodeBidi(v) => {
2016            if v.get_property().is_some() {
2017                *flags |= DOM_HAS_UNICODE_BIDI;
2018            }
2019        }
2020        CssProperty::TextBoxTrim(v) => {
2021            if v.get_property().is_some() {
2022                *flags |= DOM_HAS_TEXT_BOX_TRIM;
2023            }
2024        }
2025        CssProperty::Hyphens(v) => {
2026            if v.get_property().is_some() {
2027                *flags |= DOM_HAS_HYPHENS;
2028            }
2029        }
2030        CssProperty::WordBreak(v) => {
2031            if v.get_property().is_some() {
2032                *flags |= DOM_HAS_WORD_BREAK;
2033            }
2034        }
2035        CssProperty::OverflowWrap(v) => {
2036            if v.get_property().is_some() {
2037                *flags |= DOM_HAS_OVERFLOW_WRAP;
2038            }
2039        }
2040        CssProperty::LineBreak(v) => {
2041            if v.get_property().is_some() {
2042                *flags |= DOM_HAS_LINE_BREAK;
2043            }
2044        }
2045        CssProperty::TextAlignLast(v) => {
2046            if v.get_property().is_some() {
2047                *flags |= DOM_HAS_TEXT_ALIGN_LAST;
2048            }
2049        }
2050        CssProperty::LineHeight(v) => {
2051            if v.get_property().is_some() {
2052                *flags |= DOM_HAS_LINE_HEIGHT;
2053            }
2054        }
2055        _ => {}
2056    }
2057}
2058
2059// =============================================================================
2060// Helper encoders for dimension properties
2061// =============================================================================
2062
2063/// Encode a `GridLine` into i16: `Auto=I16_AUTO`, Line(n)=n, Span(n)=-(n).
2064/// Named lines fall back to `I16_SENTINEL` (not compact-encodable).
2065// const fn: the `n as i16` casts are guarded by explicit +/-32000 range checks.
2066#[allow(clippy::cast_possible_truncation)]
2067const fn encode_grid_line(line: &azul_css::props::layout::grid::GridLine) -> i16 {
2068    use azul_css::props::layout::grid::GridLine;
2069    match line {
2070        GridLine::Auto => I16_AUTO,
2071        GridLine::Line(n) => {
2072            if *n >= -32000 && *n <= 32000 {
2073                *n as i16
2074            } else {
2075                I16_SENTINEL
2076            }
2077        }
2078        GridLine::Span(n) => {
2079            if *n >= 1 && *n <= 32000 {
2080                -(*n as i16)
2081            } else {
2082                I16_SENTINEL
2083            }
2084        }
2085        GridLine::Named(_) => I16_SENTINEL,
2086    }
2087}
2088
2089/// Encode a `CssPropertyValue`<LayoutWidth> into u32 compact form.
2090fn encode_layout_width<T: LayoutWidthLike>(val: &CssPropertyValue<T>) -> u32 {
2091    match val {
2092        CssPropertyValue::Exact(w) => w.encode_compact_u32(),
2093        CssPropertyValue::Auto => U32_AUTO,
2094        CssPropertyValue::Initial => U32_INITIAL,
2095        CssPropertyValue::Inherit => U32_INHERIT,
2096        CssPropertyValue::None => U32_NONE,
2097        _ => U32_SENTINEL,
2098    }
2099}
2100
2101/// Encode a `CssPropertyValue`<LayoutHeight> into u32 compact form.
2102fn encode_layout_height<T: LayoutWidthLike>(val: &CssPropertyValue<T>) -> u32 {
2103    encode_layout_width(val)
2104}
2105
2106/// Trait for types that can be encoded as compact u32 dimension values.
2107/// Implemented for `LayoutWidth`, `LayoutHeight` (which are Auto|Px|MinContent|MaxContent|Calc enums).
2108trait LayoutWidthLike {
2109    fn encode_compact_u32(&self) -> u32;
2110}
2111
2112impl LayoutWidthLike for LayoutWidth {
2113    fn encode_compact_u32(&self) -> u32 {
2114        match self {
2115            Self::Auto => U32_AUTO,
2116            Self::Px(pv) => encode_pixel_value_u32(pv),
2117            Self::MinContent => U32_MIN_CONTENT,
2118            Self::MaxContent => U32_MAX_CONTENT,
2119            // FitContent/Calc are not compact-encodable → overflow to tier 3.
2120            Self::FitContent(_) | Self::Calc(_) => U32_SENTINEL,
2121        }
2122    }
2123}
2124
2125impl LayoutWidthLike for LayoutHeight {
2126    fn encode_compact_u32(&self) -> u32 {
2127        match self {
2128            Self::Auto => U32_AUTO,
2129            Self::Px(pv) => encode_pixel_value_u32(pv),
2130            Self::MinContent => U32_MIN_CONTENT,
2131            Self::MaxContent => U32_MAX_CONTENT,
2132            // FitContent/Calc are not compact-encodable → overflow to tier 3.
2133            Self::FitContent(_) | Self::Calc(_) => U32_SENTINEL,
2134        }
2135    }
2136}
2137
2138/// Encode a `CssPropertyValue` wrapping a simple `PixelValue` struct (`LayoutMinWidth`, etc.)
2139fn encode_pixel_prop<T: HasInnerPixelValue>(val: &CssPropertyValue<T>) -> u32 {
2140    match val {
2141        CssPropertyValue::Exact(inner) => encode_pixel_value_u32(&inner.get_inner_pixel()),
2142        CssPropertyValue::Auto => U32_AUTO,
2143        CssPropertyValue::Initial => U32_INITIAL,
2144        CssPropertyValue::Inherit => U32_INHERIT,
2145        CssPropertyValue::None => U32_NONE,
2146        _ => U32_SENTINEL,
2147    }
2148}
2149
2150/// Trait for dimension structs wrapping `inner: PixelValue`.
2151trait HasInnerPixelValue {
2152    fn get_inner_pixel(&self) -> azul_css::props::basic::pixel::PixelValue;
2153}
2154
2155macro_rules! impl_has_inner_pixel {
2156    ($($ty:ty),*) => {
2157        $(
2158            impl HasInnerPixelValue for $ty {
2159                fn get_inner_pixel(&self) -> azul_css::props::basic::pixel::PixelValue {
2160                    self.inner
2161                }
2162            }
2163        )*
2164    };
2165}
2166
2167impl_has_inner_pixel!(
2168    azul_css::props::layout::dimensions::LayoutMinWidth,
2169    azul_css::props::layout::dimensions::LayoutMaxWidth,
2170    azul_css::props::layout::dimensions::LayoutMinHeight,
2171    azul_css::props::layout::dimensions::LayoutMaxHeight,
2172    azul_css::props::basic::font::StyleFontSize,
2173    azul_css::props::layout::spacing::LayoutPaddingTop,
2174    azul_css::props::layout::spacing::LayoutPaddingRight,
2175    azul_css::props::layout::spacing::LayoutPaddingBottom,
2176    azul_css::props::layout::spacing::LayoutPaddingLeft,
2177    azul_css::props::layout::spacing::LayoutMarginTop,
2178    azul_css::props::layout::spacing::LayoutMarginRight,
2179    azul_css::props::layout::spacing::LayoutMarginBottom,
2180    azul_css::props::layout::spacing::LayoutMarginLeft,
2181    azul_css::props::style::border::LayoutBorderTopWidth,
2182    azul_css::props::style::border::LayoutBorderRightWidth,
2183    azul_css::props::style::border::LayoutBorderBottomWidth,
2184    azul_css::props::style::border::LayoutBorderLeftWidth,
2185    azul_css::props::layout::position::LayoutTop,
2186    azul_css::props::layout::position::LayoutRight,
2187    azul_css::props::layout::position::LayoutInsetBottom,
2188    azul_css::props::layout::position::LayoutLeft,
2189    azul_css::props::style::text::StyleLetterSpacing,
2190    azul_css::props::style::text::StyleWordSpacing,
2191    azul_css::props::style::text::StyleTextIndent,
2192    azul_css::props::style::text::StyleTabSize
2193);
2194
2195/// Encode a `CssPropertyValue`<T> where T wraps a `PixelValue`, as i16 (×10 resolved px).
2196/// Delegates to the canonical `azul_css::compact_cache::encode_css_pixel_as_i16`.
2197fn encode_css_pixel_as_i16<T: HasInnerPixelValue>(val: &CssPropertyValue<T>) -> i16 {
2198    let mapped = match val {
2199        CssPropertyValue::Exact(inner) => CssPropertyValue::Exact(inner.get_inner_pixel()),
2200        CssPropertyValue::Auto => CssPropertyValue::Auto,
2201        CssPropertyValue::Initial => CssPropertyValue::Initial,
2202        CssPropertyValue::Inherit => CssPropertyValue::Inherit,
2203        CssPropertyValue::None => CssPropertyValue::None,
2204        _ => return I16_SENTINEL,
2205    };
2206    azul_css::compact_cache::encode_css_pixel_as_i16(&mapped)
2207}
2208
2209/// Encode margin: same as `encode_css_pixel_as_i16` but Auto is a distinct value.
2210fn encode_margin_i16<T: HasInnerPixelValue>(val: &CssPropertyValue<T>) -> i16 {
2211    encode_css_pixel_as_i16(val)
2212}
2213
2214/// Encode `CssPropertyValue`<LayoutFlexBasis> — `LayoutFlexBasis` is Auto | Exact(PixelValue).
2215fn encode_flex_basis(val: &CssPropertyValue<LayoutFlexBasis>) -> u32 {
2216    match val {
2217        CssPropertyValue::Exact(fb) => match fb {
2218            LayoutFlexBasis::Auto => U32_AUTO,
2219            LayoutFlexBasis::Exact(pv) => encode_pixel_value_u32(pv),
2220        },
2221        CssPropertyValue::Auto => U32_AUTO,
2222        CssPropertyValue::Initial => U32_INITIAL,
2223        CssPropertyValue::Inherit => U32_INHERIT,
2224        CssPropertyValue::None => U32_NONE,
2225        _ => U32_SENTINEL,
2226    }
2227}
2228
2229#[cfg(test)]
2230#[path = "compact_test.rs"]
2231mod compact_test;