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.
13#[allow(clippy::wildcard_imports)]
14use azul_css::compact_cache::*;
15use azul_css::css::CssPropertyValue;
16use azul_css::props::property::CssProperty;
17use azul_css::props::basic::length::SizeMetric;
18use azul_css::props::layout::dimensions::{LayoutHeight, LayoutWidth};
19use azul_css::props::layout::flex::LayoutFlexBasis;
20use azul_css::props::layout::position::LayoutZIndex;
21use core::hash::{Hash, Hasher};
22use alloc::vec::Vec;
23use crate::hash::DefaultHasher;
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.get_border_top_style(nd, &node_id, &default_state)
303                    .and_then(|v| v.get_property().copied())
304                    .map(|v| v.inner)
305                    .unwrap_or_default();
306                let brs = self.get_border_right_style(nd, &node_id, &default_state)
307                    .and_then(|v| v.get_property().copied())
308                    .map(|v| v.inner)
309                    .unwrap_or_default();
310                let bbs = self.get_border_bottom_style(nd, &node_id, &default_state)
311                    .and_then(|v| v.get_property().copied())
312                    .map(|v| v.inner)
313                    .unwrap_or_default();
314                let bls = self.get_border_left_style(nd, &node_id, &default_state)
315                    .and_then(|v| v.get_property().copied())
316                    .map(|v| v.inner)
317                    .unwrap_or_default();
318                result.tier2_cold[i].border_styles_packed =
319                    encode_border_styles_packed(bts, brs, bbs, bls);
320            }
321
322            // Border colors (ColorU → u32 as 0xRRGGBBAA)
323            if let Some(val) = self.get_border_top_color(nd, &node_id, &default_state) {
324                if let Some(color) = val.get_property() {
325                    result.tier2_cold[i].border_top_color = encode_color_u32(&color.inner);
326                }
327            }
328            if let Some(val) = self.get_border_right_color(nd, &node_id, &default_state) {
329                if let Some(color) = val.get_property() {
330                    result.tier2_cold[i].border_right_color = encode_color_u32(&color.inner);
331                }
332            }
333            if let Some(val) = self.get_border_bottom_color(nd, &node_id, &default_state) {
334                if let Some(color) = val.get_property() {
335                    result.tier2_cold[i].border_bottom_color = encode_color_u32(&color.inner);
336                }
337            }
338            if let Some(val) = self.get_border_left_color(nd, &node_id, &default_state) {
339                if let Some(color) = val.get_property() {
340                    result.tier2_cold[i].border_left_color = encode_color_u32(&color.inner);
341                }
342            }
343
344            // Border spacing (two PixelValue → i16 × 10 resolved px)
345            if let Some(val) = self.get_border_spacing(nd, &node_id, &default_state) {
346                if let Some(spacing) = val.get_property() {
347                    if spacing.horizontal.metric == SizeMetric::Px {
348                        result.tier2_cold[i].border_spacing_h = encode_resolved_px_i16(spacing.horizontal.number.get());
349                    }
350                    if spacing.vertical.metric == SizeMetric::Px {
351                        result.tier2_cold[i].border_spacing_v = encode_resolved_px_i16(spacing.vertical.number.get());
352                    }
353                }
354            }
355
356            // Tab size (PixelValue → i16 × 10 resolved px)
357            if let Some(val) = self.get_tab_size(nd, &node_id, &default_state) {
358                result.tier2_cold[i].tab_size = encode_css_pixel_as_i16(val);
359            }
360
361            // =====================================================================
362            // Tier 2b: Text properties
363            // =====================================================================
364
365            // Text color (ColorU → u32 as 0xRRGGBBAA)
366            if let Some(val) = self.get_text_color(nd, &node_id, &default_state) {
367                if let Some(color) = val.get_property() {
368                    let c = &color.inner;
369                    result.tier2b_text[i].text_color =
370                        (u32::from(c.r) << 24) | (u32::from(c.g) << 16) | (u32::from(c.b) << 8) | u32::from(c.a);
371                }
372            }
373
374            // Font-family (hash the whole StyleFontFamilyVec for fast comparison)
375            if let Some(val) = self.get_font_family(nd, &node_id, &default_state) {
376                if let Some(families) = val.get_property() {
377                    let mut hasher = DefaultHasher::new();
378                    families.hash(&mut hasher);
379                    let h = hasher.finish();
380                    let h = if h == 0 { 1 } else { h };
381                    result.tier2b_text[i].font_family_hash = h;
382                    result.font_hash_to_families.insert(h, families.clone());
383                }
384            }
385
386            // Line-height. Parser convention: a NEGATIVE normalized() is an
387            // ABSOLUTE pixel line-height, a positive one a unitless multiple
388            // (or percentage) of font-size. The two need different i16
389            // scales:
390            //  - positive: multiple × 1000 (120% -> 1200; range up to ~32x)
391            //  - negative: -px × 10 (line-height: 40px -> -400; ±3276.7px)
392            // The old single ×1000 scale overflowed i16 for any absolute
393            // line-height above 32.76px, stored the SENTINEL, and the getter
394            // decoded that as "line-height: normal" - `line-height: 40px`
395            // was silently dropped on every normal-state node.
396            if let Some(val) = self.get_line_height(nd, &node_id, &default_state) {
397                if let Some(lh) = val.get_property() {
398                    let n = lh.inner.normalized();
399                    let stored = if n < 0.0 {
400                        // Absolute px: clamp to the representable range
401                        // instead of falling to the sentinel ("normal").
402                        ((n * 10.0).round() as i32).max(-32768)
403                    } else {
404                        (n * 1000.0).round() as i32
405                    };
406                    if stored >= -32768 && stored < i32::from(I16_SENTINEL_THRESHOLD) {
407                        result.tier2b_text[i].line_height = stored as i16;
408                    } else {
409                        result.tier2b_text[i].line_height = I16_SENTINEL;
410                    }
411                }
412            }
413
414            // Letter-spacing (PixelValue wrapper → i16 × 10 resolved px)
415            if let Some(val) = self.get_letter_spacing(nd, &node_id, &default_state) {
416                result.tier2b_text[i].letter_spacing = encode_css_pixel_as_i16(val);
417            }
418
419            // Word-spacing (PixelValue wrapper → i16 × 10 resolved px)
420            if let Some(val) = self.get_word_spacing(nd, &node_id, &default_state) {
421                result.tier2b_text[i].word_spacing = encode_css_pixel_as_i16(val);
422            }
423
424            // Text-indent (PixelValue wrapper → i16 × 10 resolved px)
425            if let Some(val) = self.get_text_indent(nd, &node_id, &default_state) {
426                result.tier2b_text[i].text_indent = encode_css_pixel_as_i16(val);
427            }
428        }
429
430        // =====================================================================
431        // Per-node font dirty tracking (P4)
432        // Compare each node's font_family_hash against the previous frame's hash.
433        // Nodes whose hash changed are recorded in font_dirty_nodes for
434        // incremental font chain re-resolution instead of all-or-nothing.
435        // =====================================================================
436        result.font_dirty_nodes.clear();
437        for i in 0..node_count {
438            let new_hash = result.tier2b_text[i].font_family_hash;
439            let old_hash = prev_font_hashes.get(i).copied().unwrap_or(0);
440            if new_hash != old_hash {
441                result.font_dirty_nodes.push(i);
442            }
443        }
444        // Save current hashes as prev_font_hashes for next frame comparison
445        result.prev_font_hashes = result.tier2b_text.iter().map(|t| t.font_family_hash).collect();
446
447        result
448    }
449
450    /// Build compact cache with inheritance in a single pass.
451    ///
452    /// Replaces the separate `compute_inherited_values()` + `build_compact_cache()` calls.
453    /// For each node (in DOM index order, which is pre-order = parents before children):
454    ///   1. Copy parent's compact values for INHERITABLE properties
455    ///   2. Apply this node's CSS properties on top (from `css_props` + inline + UA)
456    ///   3. Write directly to compact arrays
457    ///
458    /// This eliminates 50K Vec clones from `compute_inherited_values` and
459    /// avoids re-reading properties from 5 separate data structures.
460    pub fn build_compact_cache_with_inheritance(
461        &self,
462        node_data: &[NodeData],
463        node_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
464        prev_font_hashes: &[u64],
465    ) -> CompactLayoutCache {
466        self.build_compact_cache_with_inheritance_debug(node_data, node_hierarchy, prev_font_hashes, &mut None)
467    }
468
469    /// Same as `build_compact_cache_with_inheritance` but with optional debug logging.
470    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
471    pub fn build_compact_cache_with_inheritance_debug(
472        &self,
473        node_data: &[NodeData],
474        node_hierarchy: &[crate::styled_dom::NodeHierarchyItem],
475        prev_font_hashes: &[u64],
476        debug_messages: &mut Option<Vec<azul_css::LayoutDebugMessage>>,
477    ) -> CompactLayoutCache {
478        // Inheritable tier1 CSS fields (font-weight/style, text-align, visibility,
479        // white-space, direction, border-collapse). Copied from parent in Step 1.
480        const INHERITABLE_TIER1_MASK: u64 =
481            (FONT_WEIGHT_MASK << FONT_WEIGHT_SHIFT)
482            | (FONT_STYLE_MASK << FONT_STYLE_SHIFT)
483            | (TEXT_ALIGN_MASK << TEXT_ALIGN_SHIFT)
484            | (VISIBILITY_MASK << VISIBILITY_SHIFT)
485            | (WHITE_SPACE_MASK << WHITE_SPACE_SHIFT)
486            | (DIRECTION_MASK << DIRECTION_SHIFT)
487            | (BORDER_COLLAPSE_MASK << BORDER_COLLAPSE_SHIFT);
488
489        let node_count = self.node_count;
490        let default_state = StyledNodeState::default();
491        let mut result = CompactLayoutCache::with_capacity(node_count);
492
493        // Pre-encode global CSS properties (from `*` rules) into compact form.
494        // These are applied as baseline for every node before inheritance.
495        let mut global_tier1: u64 = 0;
496        let mut global_dims = CompactNodeProps::default();
497        let mut global_cold = CompactNodePropsCold::default();
498        let mut global_text = CompactTextProps::default();
499        let has_global = !self.global_css_props.is_empty();
500
501        if has_global {
502            use azul_css::props::property::CssProperty;
503
504            for prop in &self.global_css_props {
505                result.uses_viewport_units |= css_property_uses_viewport_units(prop);
506                // Apply each global property to the pre-encoded compact values
507                macro_rules! global_tier1_enum {
508                    ($variant:ident, $shift:ident, $mask:ident, $encoder:ident) => {
509                        if let CssProperty::$variant(v) = prop {
510                            if let Some(exact) = v.get_property() {
511                                let encoded = u64::from($encoder(*exact));
512                                let shifted_mask = $mask << $shift;
513                                global_tier1 = (global_tier1 & !shifted_mask) | ((encoded & $mask) << $shift);
514                            }
515                        }
516                    };
517                }
518
519                global_tier1_enum!(Display, DISPLAY_SHIFT, DISPLAY_MASK, layout_display_to_u8);
520                global_tier1_enum!(Position, POSITION_SHIFT, POSITION_MASK, layout_position_to_u8);
521                global_tier1_enum!(Float, FLOAT_SHIFT, FLOAT_MASK, layout_float_to_u8);
522                global_tier1_enum!(OverflowX, OVERFLOW_X_SHIFT, OVERFLOW_MASK, layout_overflow_to_u8);
523                global_tier1_enum!(OverflowY, OVERFLOW_Y_SHIFT, OVERFLOW_MASK, layout_overflow_to_u8);
524                global_tier1_enum!(BoxSizing, BOX_SIZING_SHIFT, BOX_SIZING_MASK, layout_box_sizing_to_u8);
525                global_tier1_enum!(FlexDirection, FLEX_DIRECTION_SHIFT, FLEX_DIR_MASK, layout_flex_direction_to_u8);
526                global_tier1_enum!(FlexWrap, FLEX_WRAP_SHIFT, FLEX_WRAP_MASK, layout_flex_wrap_to_u8);
527                global_tier1_enum!(JustifyContent, JUSTIFY_CONTENT_SHIFT, JUSTIFY_MASK, layout_justify_content_to_u8);
528                global_tier1_enum!(AlignItems, ALIGN_ITEMS_SHIFT, ALIGN_MASK, layout_align_items_to_u8);
529                global_tier1_enum!(AlignContent, ALIGN_CONTENT_SHIFT, ALIGN_MASK, layout_align_content_to_u8);
530                global_tier1_enum!(Clear, CLEAR_SHIFT, CLEAR_MASK, layout_clear_to_u8);
531                global_tier1_enum!(Visibility, VISIBILITY_SHIFT, VISIBILITY_MASK, style_visibility_to_u8);
532                global_tier1_enum!(WritingMode, WRITING_MODE_SHIFT, WRITING_MODE_MASK, layout_writing_mode_to_u8);
533                global_tier1_enum!(FontWeight, FONT_WEIGHT_SHIFT, FONT_WEIGHT_MASK, style_font_weight_to_u8);
534                global_tier1_enum!(FontStyle, FONT_STYLE_SHIFT, FONT_STYLE_MASK, style_font_style_to_u8);
535                global_tier1_enum!(TextAlign, TEXT_ALIGN_SHIFT, TEXT_ALIGN_MASK, style_text_align_to_u8);
536                global_tier1_enum!(WhiteSpace, WHITE_SPACE_SHIFT, WHITE_SPACE_MASK, style_white_space_to_u8);
537                global_tier1_enum!(Direction, DIRECTION_SHIFT, DIRECTION_MASK, style_direction_to_u8);
538                global_tier1_enum!(VerticalAlign, VERTICAL_ALIGN_SHIFT, VERTICAL_ALIGN_MASK, style_vertical_align_to_u8);
539                global_tier1_enum!(BorderCollapse, BORDER_COLLAPSE_SHIFT, BORDER_COLLAPSE_MASK, border_collapse_to_u8);
540
541                // Tier 2 dims
542                match prop {
543                    CssProperty::PaddingTop(v) => { global_dims.padding_top = encode_css_pixel_as_i16(v); }
544                    CssProperty::PaddingRight(v) => { global_dims.padding_right = encode_css_pixel_as_i16(v); }
545                    CssProperty::PaddingBottom(v) => { global_dims.padding_bottom = encode_css_pixel_as_i16(v); }
546                    CssProperty::PaddingLeft(v) => { global_dims.padding_left = encode_css_pixel_as_i16(v); }
547                    CssProperty::MarginTop(v) => { global_dims.margin_top = encode_margin_i16(v); }
548                    CssProperty::MarginRight(v) => { global_dims.margin_right = encode_margin_i16(v); }
549                    CssProperty::MarginBottom(v) => { global_dims.margin_bottom = encode_margin_i16(v); }
550                    CssProperty::MarginLeft(v) => { global_dims.margin_left = encode_margin_i16(v); }
551                    CssProperty::Width(v) => { global_dims.width = encode_layout_width(v); }
552                    CssProperty::Height(v) => { global_dims.height = encode_layout_height(v); }
553                    CssProperty::FontSize(v) => { global_dims.font_size = encode_pixel_prop(v); }
554                    CssProperty::BorderTopWidth(v) => { global_dims.border_top_width = encode_css_pixel_as_i16(v); }
555                    CssProperty::BorderRightWidth(v) => { global_dims.border_right_width = encode_css_pixel_as_i16(v); }
556                    CssProperty::BorderBottomWidth(v) => { global_dims.border_bottom_width = encode_css_pixel_as_i16(v); }
557                    CssProperty::BorderLeftWidth(v) => { global_dims.border_left_width = encode_css_pixel_as_i16(v); }
558                    _ => {}
559                }
560            }
561
562            if global_tier1 != 0 {
563                global_tier1 |= TIER1_POPULATED_BIT;
564            }
565        }
566
567        // Helper: push debug message if debug_messages is Some
568        macro_rules! cascade_debug {
569            ($($arg:tt)*) => {
570                if let Some(ref mut msgs) = debug_messages {
571                    msgs.push(azul_css::LayoutDebugMessage::css_getter(format!($($arg)*)));
572                }
573            };
574        }
575
576        for i in 0..node_count {
577            let node_id = NodeId::new(i);
578            let nd = &node_data[i];
579
580            // Step 0: Apply UA CSS defaults first (lowest priority).
581            // Then global `*` rules override UA (higher priority).
582            // Then per-node CSS (Step 3) overrides both.
583            //
584            // CSS cascade priority: UA < author `*` < author specific < inline
585
586            // Step 1: Inherit from parent's COMPACT values (not computed_values)
587            // Parent index is always < i in pre-order arena, so already computed.
588            //
589            // Step 1: Inherit ONLY inheritable CSS properties from parent.
590            // Non-inheritable fields (display, position, float, overflow, box-sizing,
591            // flex-*, clear, vertical-align, writing-mode) stay at 0 (CSS initial value).
592            // They get set by UA CSS (Step 2) and author CSS (Step 3).
593            let parent_id = node_hierarchy[i].parent_id();
594            if let Some(pid) = parent_id {
595                let pi = pid.index();
596
597                // AUDIT: inheritance assumes a PRE-ORDER arena, i.e. a node's
598                // parent is always stored at a lower index (`pi < i`) and has
599                // therefore already been fully cascaded. A forward reference
600                // (`pi >= i`) would silently inherit that parent's still-default
601                // (all-zero) values, and an out-of-bounds `pi >= node_count`
602                // would panic. Guard against both: assert the pre-order
603                // invariant in debug builds, and skip inheritance (treat the
604                // node as a root) for any malformed reference in release builds.
605                debug_assert!(
606                    pi < i,
607                    "compact cascade: non-pre-order arena — node {i}'s parent {pi} \
608                     is not stored before it; inheritance would read default values",
609                );
610                if pi < i {
611                // Copy only inheritable tier1 fields from parent
612                result.tier1_enums[i] = result.tier1_enums[pi] & INHERITABLE_TIER1_MASK;
613
614                // Inheritable tier2: font_size
615                result.tier2_dims[i].font_size = result.tier2_dims[pi].font_size;
616
617                // Inheritable tier2_cold: border_spacing, tab_size
618                result.tier2_cold[i].border_spacing_h = result.tier2_cold[pi].border_spacing_h;
619                result.tier2_cold[i].border_spacing_v = result.tier2_cold[pi].border_spacing_v;
620                result.tier2_cold[i].tab_size = result.tier2_cold[pi].tab_size;
621
622                // Inheritable tier2b: all text properties
623                result.tier2b_text[i] = result.tier2b_text[pi];
624                }
625            }
626
627            {
628                let d = &result.tier2_dims[i];
629                cascade_debug!("node[{}] {:?} after-inherit: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={} w={} h={}",
630                    i, nd.node_type, d.padding_top, d.padding_bottom, d.padding_left, d.padding_right,
631                    d.margin_top, d.margin_bottom, d.margin_left, d.margin_right, d.width, d.height);
632            }
633
634            // Step 2: Apply UA CSS defaults for this node type directly to compact values.
635            // UA defaults have lowest cascade priority — overridden by author CSS below.
636            apply_ua_css_to_compact(
637                &nd.node_type,
638                &mut result.tier1_enums[i],
639                &mut result.tier2_dims[i],
640                &mut result.tier2_cold[i],
641                &mut result.tier2b_text[i],
642                &mut result.font_hash_to_families,
643            );
644
645            {
646                let d = &result.tier2_dims[i];
647                cascade_debug!("node[{}] {:?} after-UA: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={}",
648                    i, nd.node_type, d.padding_top, d.padding_bottom, d.padding_left, d.padding_right,
649                    d.margin_top, d.margin_bottom, d.margin_left, d.margin_right);
650            }
651
652            // Step 2.5: Apply global `*` author CSS (overrides UA, overridden by specific rules)
653            // Apply each `*` rule property individually (not bulk-assign) so we only
654            // override properties the `*` rule actually set, preserving UA CSS for others.
655            //
656            // Per CSS spec, `*` matches all ELEMENTS. Text nodes are not elements —
657            // they must only inherit from their parent. Without this check, `* { color: #666 }`
658            // would overwrite the inherited `color: red` on a Text child of `<p>`,
659            // even though `<p>` correctly got red from `p { color: red }`.
660            if !nd.is_text_node() {
661                for prop in &self.global_css_props {
662                    // (flag already accumulated in the has_global pre-pass)
663                    apply_css_property_to_compact(
664                        prop,
665                        &mut result.tier1_enums[i],
666                        &mut result.tier2_dims[i],
667                        &mut result.tier2_cold[i],
668                        &mut result.tier2b_text[i],
669                        &mut result.font_hash_to_families,
670                    );
671                    update_dom_declared_flags(prop, &mut result.dom_declared_flags);
672                }
673            }
674
675            {
676                let d = &result.tier2_dims[i];
677                cascade_debug!("node[{}] {:?} after-global-star: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={}",
678                    i, nd.node_type, d.padding_top, d.padding_bottom, d.padding_left, d.padding_right,
679                    d.margin_top, d.margin_bottom, d.margin_left, d.margin_right);
680                let n_props = self.css_props.get_slice(i).len();
681                let n_inline = nd.style.iter_inline_properties().count();
682                cascade_debug!("node[{}] css_props={} entries, inline={} entries", i, n_props, n_inline);
683                for prop in self.css_props.get_slice(i) {
684                    cascade_debug!("node[{}]   css_prop: state={:?} type={:?}", i, prop.state, prop.prop_type);
685                }
686            }
687
688            // Step 3: Apply this node's CSS properties directly to compact values.
689            // Per-node author CSS has higher specificity than global `*`.
690
691            // Scan css_props (stylesheet rules, sorted by (state, prop_type))
692            // Typically 5-15 entries per node. Only Normal state matters for layout.
693            for prop in self.css_props.get_slice(i) {
694                if prop.state != azul_css::dynamic_selector::PseudoStateType::Normal { continue; }
695                result.uses_viewport_units |= css_property_uses_viewport_units(&prop.property);
696                apply_css_property_to_compact(
697                    &prop.property,
698                    &mut result.tier1_enums[i],
699                    &mut result.tier2_dims[i],
700                    &mut result.tier2_cold[i],
701                    &mut result.tier2b_text[i],
702                    &mut result.font_hash_to_families,
703                );
704                update_dom_declared_flags(&prop.property, &mut result.dom_declared_flags);
705            }
706
707            {
708                let d = &result.tier2_dims[i];
709                cascade_debug!("node[{}] {:?} after-css-props: pt={} pb={} pl={} pr={} mt={} mb={} ml={} mr={}",
710                    i, nd.node_type, d.padding_top, d.padding_bottom, d.padding_left, d.padding_right,
711                    d.margin_top, d.margin_bottom, d.margin_left, d.margin_right);
712            }
713
714            // Scan inline CSS (node_data.style — typically 0-3 properties).
715            // Inline CSS has highest specificity — applied last to override stylesheet.
716            for (prop, conds) in nd.style.iter_inline_properties() {
717                // Apply when the conditions hold for the RESTING state:
718                // pseudo-state conditions must be Normal, and every other
719                // condition (viewport/@media, theme, OS...) is evaluated
720                // against the window's dynamic context — the same rule
721                // get_property_slow applies, so the fast path and the slow
722                // path cannot disagree about a conditional property. A
723                // non-pseudo condition also flags the cache, so the window
724                // knows a context change requires a rebuild.
725                let is_normal = conds.as_slice().is_empty()
726                    || conds.as_slice().iter().all(|c| match c {
727                        azul_css::dynamic_selector::DynamicSelector::PseudoState(s) => {
728                            *s == azul_css::dynamic_selector::PseudoStateType::Normal
729                        }
730                        non_pseudo => {
731                            result.has_dynamic_conditions = true;
732                            // Harvest the thresholds this condition can flip
733                            // at — the resize decision regenerates when the
734                            // window crosses one (dedup/sort happens once,
735                            // after the node loop).
736                            {
737                                let mut w = Vec::new();
738                                let mut h = Vec::new();
739                                azul_css::dynamic_selector::collect_viewport_thresholds(
740                                    core::slice::from_ref(non_pseudo),
741                                    &mut w,
742                                    &mut h,
743                                );
744                                result
745                                    .inline_viewport_w
746                                    .extend(w.into_iter().map(f32::to_bits));
747                                result
748                                    .inline_viewport_h
749                                    .extend(h.into_iter().map(f32::to_bits));
750                            }
751                            self.dynamic_context
752                                .as_deref()
753                                .is_some_and(|ctx| non_pseudo.matches(ctx))
754                        }
755                    });
756                if !is_normal { continue; }
757                result.uses_viewport_units |= css_property_uses_viewport_units(prop);
758                // Layout-critical props dispatched via single-variant `if let` (direct discriminant
759                // COMPARES, no indirect jump). apply_css_property_to_compact's ~100-arm `match` lowers
760                // to a jump table that remill mis-lifts (never reaches the right arm) — same class as the
761                // CssProperty::clone bug. With the conversion-clone fix the prop discriminant is now
762                // correct, so these compares match and apply the value; everything else falls back.
763                // (CssProperty is imported at module top.)
764                if let CssProperty::Width(v) = prop {
765                    result.tier2_dims[i].width = encode_layout_width(v);
766                } else if let CssProperty::Height(v) = prop {
767                    result.tier2_dims[i].height = encode_layout_height(v);
768                } else if let CssProperty::FlexGrow(v) = prop {
769                    if let Some(e) = v.get_property() {
770                        result.tier2_dims[i].flex_grow = encode_flex_u16(e.inner.get());
771                    }
772                } else if let CssProperty::Display(v) = prop {
773                    if let Some(e) = v.get_property() {
774                        let enc = u64::from(layout_display_to_u8(*e));
775                        let m = DISPLAY_MASK;
776                        let s = DISPLAY_SHIFT;
777                        result.tier1_enums[i] = (result.tier1_enums[i] & !(m << s)) | ((enc & m) << s);
778                    }
779                } else {
780                    apply_css_property_to_compact(
781                        prop,
782                        &mut result.tier1_enums[i],
783                        &mut result.tier2_dims[i],
784                        &mut result.tier2_cold[i],
785                        &mut result.tier2b_text[i],
786                        &mut result.font_hash_to_families,
787                    );
788                }
789                update_dom_declared_flags(prop, &mut result.dom_declared_flags);
790            }
791
792            // Step 4b: user-overridden properties (runtime patches via
793            // `set_css_property` / `restyle_user_property`). The resolver
794            // consults this layer FIRST, so the compact cache must apply it
795            // LAST — the cache is a projection of the same cascade and the
796            // two must agree. Without this step a rebuilt cache resurrected
797            // the pre-patch value: `restyle_user_property` rebuilds the cache
798            // right after recording the override, and the layout fast path
799            // then read the stale display/geometry the patch had just
800            // changed. Same dispatch shape as the inline loop above (the
801            // single-variant `if let`s exist for the remill lift, see there).
802            if let Some(user_props) = self.user_overridden_properties.get(i) {
803                for (_, prop) in user_props {
804                    result.uses_viewport_units |= css_property_uses_viewport_units(prop);
805                    if let CssProperty::Width(v) = prop {
806                        result.tier2_dims[i].width = encode_layout_width(v);
807                    } else if let CssProperty::Height(v) = prop {
808                        result.tier2_dims[i].height = encode_layout_height(v);
809                    } else if let CssProperty::FlexGrow(v) = prop {
810                        if let Some(e) = v.get_property() {
811                            result.tier2_dims[i].flex_grow = encode_flex_u16(e.inner.get());
812                        }
813                    } else if let CssProperty::Display(v) = prop {
814                        if let Some(e) = v.get_property() {
815                            let enc = u64::from(layout_display_to_u8(*e));
816                            let m = DISPLAY_MASK;
817                            let s = DISPLAY_SHIFT;
818                            result.tier1_enums[i] =
819                                (result.tier1_enums[i] & !(m << s)) | ((enc & m) << s);
820                        }
821                    } else {
822                        apply_css_property_to_compact(
823                            prop,
824                            &mut result.tier1_enums[i],
825                            &mut result.tier2_dims[i],
826                            &mut result.tier2_cold[i],
827                            &mut result.tier2b_text[i],
828                            &mut result.font_hash_to_families,
829                        );
830                    }
831                    update_dom_declared_flags(prop, &mut result.dom_declared_flags);
832                }
833            }
834
835            // Resolve font-size from em/percent/pt/etc. to px.
836            // CSS 2.1: inherited font-size is the COMPUTED (px) value, not the specified value.
837            // Pre-order traversal guarantees parent's font_size is already resolved.
838            resolve_font_size_to_px(
839                &mut result.tier2_dims,
840                i,
841                parent_id,
842            );
843
844            // Set populated bit
845            if result.tier1_enums[i] != 0 {
846                result.tier1_enums[i] |= TIER1_POPULATED_BIT;
847            }
848        }
849
850        // Font dirty tracking.
851        // When prev_font_hashes is empty (first build for this DOM), mark ALL
852        // text nodes dirty to force font resolution. Without this, a DOM with
853        // no explicit font-family (all hashes 0) would compare 0==0 and skip
854        // resolution, even though font-weight/font-style may differ from the
855        // cached chains of a previous DOM.
856        result.font_dirty_nodes.clear();
857        let first_build = prev_font_hashes.is_empty();
858        for i in 0..node_count {
859            let new_hash = result.tier2b_text[i].font_family_hash;
860            let old_hash = prev_font_hashes.get(i).copied().unwrap_or(0);
861            if first_build || new_hash != old_hash {
862                result.font_dirty_nodes.push(i);
863            }
864        }
865        result.prev_font_hashes = result.tier2b_text.iter().map(|t| t.font_family_hash).collect();
866
867        // Normalize the harvested viewport thresholds once (pushed raw per
868        // node above): sorted + deduped by bit pattern.
869        result.inline_viewport_w.sort_unstable();
870        result.inline_viewport_w.dedup();
871        result.inline_viewport_h.sort_unstable();
872        result.inline_viewport_h.dedup();
873
874        result
875    }
876}
877
878// =============================================================================
879// Helpers extracted from build_compact_cache_with_inheritance_debug
880// =============================================================================
881
882/// Apply UA CSS defaults for a node type directly to compact values.
883/// UA defaults have lowest cascade priority — overridden by author CSS.
884fn apply_ua_css_to_compact(
885    node_type: &crate::dom::NodeType,
886    tier1: &mut u64,
887    dims: &mut CompactNodeProps,
888    cold: &mut CompactNodePropsCold,
889    text: &mut CompactTextProps,
890    font_hash_map: &mut alloc::collections::BTreeMap<u64, azul_css::props::basic::font::StyleFontFamilyVec>,
891) {
892    use azul_css::props::property::CssPropertyType as PT2;
893    const UA_PROPERTY_TYPES: &[PT2] = &[
894        // Tier1 enum properties
895        PT2::Display, PT2::Position, PT2::Float, PT2::Clear,
896        PT2::OverflowX, PT2::OverflowY, PT2::BoxSizing,
897        PT2::FlexDirection, PT2::FlexWrap, PT2::JustifyContent,
898        PT2::AlignItems, PT2::AlignContent, PT2::WritingMode,
899        PT2::FontWeight, PT2::FontStyle, PT2::TextAlign,
900        PT2::Visibility, PT2::WhiteSpace, PT2::Direction,
901        PT2::VerticalAlign, PT2::BorderCollapse,
902        // Tier2 dimension properties
903        PT2::Width, PT2::Height, PT2::FontSize,
904        PT2::MarginTop, PT2::MarginBottom, PT2::MarginLeft, PT2::MarginRight,
905        PT2::PaddingTop, PT2::PaddingBottom, PT2::PaddingLeft, PT2::PaddingRight,
906        PT2::BorderTopWidth, PT2::BorderTopStyle, PT2::BorderTopColor,
907        PT2::BorderRightWidth, PT2::BorderRightStyle, PT2::BorderRightColor,
908        PT2::BorderBottomWidth, PT2::BorderBottomStyle, PT2::BorderBottomColor,
909        PT2::BorderLeftWidth, PT2::BorderLeftStyle, PT2::BorderLeftColor,
910        // Text properties
911        PT2::TextColor, PT2::LineHeight, PT2::LetterSpacing, PT2::WordSpacing,
912        PT2::TextDecoration, PT2::Cursor, PT2::ListStyleType,
913        // Counters: the UA sheet resets `list-item` on <ol>/<ul> so each list
914        // restarts numbering. Without these here the has_counter fast-path bit
915        // stays unset for list containers, compute_counters skips the reset, and
916        // the list-item counter runs globally (a <ul> then <ol> numbered 1,2 then
917        // 3,4 instead of restarting at 1).
918        PT2::CounterReset, PT2::CounterIncrement,
919    ];
920    for pt in UA_PROPERTY_TYPES {
921        if let Some(ua_prop) = crate::ua_css::get_ua_property(node_type, *pt) {
922            apply_css_property_to_compact(ua_prop, tier1, dims, cold, text, font_hash_map);
923        }
924    }
925}
926
927/// Resolve a node's font-size from relative units (em, %, rem, pt) to absolute px.
928/// CSS 2.1: inherited font-size is the COMPUTED (px) value, not the specified value.
929/// Pre-order traversal guarantees parent's `font_size` is already resolved.
930fn resolve_font_size_to_px(
931    tier2_dims: &mut [CompactNodeProps],
932    node_idx: usize,
933    parent_id: Option<NodeId>,
934) {
935    let raw_fs = tier2_dims[node_idx].font_size;
936    if raw_fs == U32_SENTINEL || raw_fs >= U32_SENTINEL_THRESHOLD {
937        return;
938    }
939    let pv = match decode_pixel_value_u32(raw_fs) {
940        Some(pv) if pv.metric != SizeMetric::Px => pv,
941        _ => return,
942    };
943
944    // AUDIT: pre-order arena assumed — the parent's font-size is already
945    // resolved to px only when `pid < node_idx`. Use checked `get` so an
946    // out-of-bounds parent ref cannot panic, and require `pid < node_idx` so a
947    // forward reference falls back to the 16px CSS initial value instead of
948    // reading an unresolved (still em/%) parent value.
949    let parent_font_size_px = parent_id
950        .map_or(16.0, |pid| {
951            let pi = pid.index();
952            debug_assert!(
953                pi < node_idx,
954                "compact font-size resolve: non-pre-order arena — node {node_idx}'s \
955                 parent {pi} font-size is not yet resolved",
956            );
957            if pi < node_idx {
958                tier2_dims
959                    .get(pi)
960                    .and_then(|p| decode_pixel_value_u32(p.font_size))
961                    .map_or(16.0, |ppv| ppv.number.get())
962            } else {
963                16.0
964            }
965        });
966
967    let resolved_px = match pv.metric {
968        SizeMetric::Em => pv.number.get() * parent_font_size_px,
969        SizeMetric::Percent => pv.number.get() / 100.0 * parent_font_size_px,
970        SizeMetric::Rem => {
971            // rem = the ROOT element's font size. For the root itself that is circular,
972            // so CSS resolves root rem against the 16px INITIAL value (Selectors/Values:
973            // "when specified on the root element, rem refers to the initial value").
974            // tier2_dims[0] IS the root's slot, but while resolving the root it still
975            // holds the root's own unresolved raw rem — so `html { font-size: 2rem }`
976            // computed 2*2 = 4px instead of 2*16 = 32px.
977            let rem_base = if parent_id.is_none() {
978                16.0
979            } else {
980                tier2_dims
981                    .first()
982                    .and_then(|r| decode_pixel_value_u32(r.font_size))
983                    .map_or(16.0, |rpv| rpv.number.get())
984            };
985            rem_base * pv.number.get()
986        }
987        SizeMetric::Pt => pv.number.get() * 96.0 / 72.0,
988        _ => pv.number.get(),
989    };
990    tier2_dims[node_idx].font_size =
991        encode_pixel_value_u32(&azul_css::props::basic::pixel::PixelValue::px(resolved_px));
992}
993
994/// Does this property's value use a viewport-relative unit (vw/vh/vmin/vmax)?
995///
996/// Feeds `CompactLayoutCache::uses_viewport_units` from the property loops of
997/// `build_compact_cache_with_inheritance` — one call per (node, property), on
998/// data the loops are already iterating. See that field's docs for what the
999/// flag buys (solver3 skips per-resize invalidation of every inline collection
1000/// for the overwhelming majority of documents that never mention a viewport
1001/// unit).
1002///
1003/// Coverage = the pixel-carrying properties the compact cache itself encodes,
1004/// which is a superset of what inline collection/measurement reads (the only
1005/// consumer). `calc()` widths/heights are flagged CONSERVATIVELY without
1006/// walking the AST — a false positive merely keeps the old always-invalidate
1007/// behaviour.
1008fn css_property_uses_viewport_units(prop: &CssProperty) -> bool {
1009    use azul_css::props::basic::length::SizeMetric;
1010    use azul_css::props::basic::pixel::PixelValue;
1011    const fn pv(p: &PixelValue) -> bool {
1012        matches!(p.metric, SizeMetric::Vw | SizeMetric::Vh | SizeMetric::Vmin | SizeMetric::Vmax)
1013    }
1014    fn inner<T: HasInnerPixelValue>(v: &CssPropertyValue<T>) -> bool {
1015        matches!(v, CssPropertyValue::Exact(x) if pv(&x.get_inner_pixel()))
1016    }
1017    use azul_css::props::layout::dimensions::{LayoutHeight, LayoutWidth};
1018    use azul_css::props::layout::flex::LayoutFlexBasis;
1019    match prop {
1020        CssProperty::Width(v) => matches!(v, CssPropertyValue::Exact(w) if match w {
1021            LayoutWidth::Px(p) | LayoutWidth::FitContent(p) => pv(p),
1022            LayoutWidth::Calc(_) => true,
1023            _ => false,
1024        }),
1025        CssProperty::Height(v) => matches!(v, CssPropertyValue::Exact(h) if match h {
1026            LayoutHeight::Px(p) | LayoutHeight::FitContent(p) => pv(p),
1027            LayoutHeight::Calc(_) => true,
1028            _ => false,
1029        }),
1030        CssProperty::FlexBasis(v) => matches!(v, CssPropertyValue::Exact(LayoutFlexBasis::Exact(p)) if pv(p)),
1031        CssProperty::MinWidth(v) => inner(v),
1032        CssProperty::MaxWidth(v) => inner(v),
1033        CssProperty::MinHeight(v) => inner(v),
1034        CssProperty::MaxHeight(v) => inner(v),
1035        CssProperty::FontSize(v) => inner(v),
1036        CssProperty::PaddingTop(v) => inner(v),
1037        CssProperty::PaddingRight(v) => inner(v),
1038        CssProperty::PaddingBottom(v) => inner(v),
1039        CssProperty::PaddingLeft(v) => inner(v),
1040        CssProperty::MarginTop(v) => inner(v),
1041        CssProperty::MarginRight(v) => inner(v),
1042        CssProperty::MarginBottom(v) => inner(v),
1043        CssProperty::MarginLeft(v) => inner(v),
1044        CssProperty::BorderTopWidth(v) => inner(v),
1045        CssProperty::BorderRightWidth(v) => inner(v),
1046        CssProperty::BorderBottomWidth(v) => inner(v),
1047        CssProperty::BorderLeftWidth(v) => inner(v),
1048        CssProperty::Top(v) => inner(v),
1049        CssProperty::Right(v) => inner(v),
1050        CssProperty::Bottom(v) => inner(v),
1051        CssProperty::Left(v) => inner(v),
1052        CssProperty::LetterSpacing(v) => inner(v),
1053        CssProperty::WordSpacing(v) => inner(v),
1054        CssProperty::TextIndent(v) => inner(v),
1055        CssProperty::TabSize(v) => inner(v),
1056        _ => false,
1057    }
1058}
1059
1060// =============================================================================
1061// Direct CssProperty → compact field writer
1062// =============================================================================
1063
1064/// Apply a single `CssProperty` directly to the compact representation.
1065/// Called once per property per node — replaces the old 56+ getter approach.
1066#[inline]
1067// The scrollbar-* and counter-* arms have identical bodies
1068// (`if v.get_property().is_some() { flags |= … }`) but each variant wraps a
1069// DIFFERENT value type (StyleBackgroundContentValue, LayoutScrollbarWidthValue,
1070// StyleScrollbarColorValue, CounterResetValue, CounterIncrementValue, …), so an
1071// or-pattern binding `v` cannot be expressed across them.
1072#[allow(clippy::match_same_arms)]
1073// fixed-point encoders: z-index / line-height are range-checked before the
1074// narrowing cast, and opacity is clamped to [0,1] then scaled to [0,254] (u8).
1075#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1076#[allow(clippy::too_many_lines, clippy::cognitive_complexity)] // large but cohesive: single-purpose parser/builder/dispatch (one branch per input variant)
1077fn apply_css_property_to_compact(
1078    prop: &CssProperty,
1079    tier1: &mut u64,
1080    dims: &mut CompactNodeProps,
1081    cold: &mut CompactNodePropsCold,
1082    text: &mut CompactTextProps,
1083    font_hash_map: &mut alloc::collections::BTreeMap<u64, azul_css::props::basic::font::StyleFontFamilyVec>,
1084) {
1085    macro_rules! set_tier1 {
1086        ($v:expr, $shift:expr, $mask:expr, $encoder:ident) => {
1087            if let Some(exact) = $v.get_property() {
1088                let encoded = u64::from($encoder(*exact));
1089                let shifted_mask = $mask << $shift;
1090                *tier1 = (*tier1 & !shifted_mask) | ((encoded & $mask) << $shift);
1091            }
1092        };
1093    }
1094
1095    match prop {
1096        // Tier 1 enums
1097        CssProperty::Display(v) => set_tier1!(v, DISPLAY_SHIFT, DISPLAY_MASK, layout_display_to_u8),
1098        CssProperty::Position(v) => set_tier1!(v, POSITION_SHIFT, POSITION_MASK, layout_position_to_u8),
1099        CssProperty::Float(v) => set_tier1!(v, FLOAT_SHIFT, FLOAT_MASK, layout_float_to_u8),
1100        CssProperty::OverflowX(v) => set_tier1!(v, OVERFLOW_X_SHIFT, OVERFLOW_MASK, layout_overflow_to_u8),
1101        CssProperty::OverflowY(v) => set_tier1!(v, OVERFLOW_Y_SHIFT, OVERFLOW_MASK, layout_overflow_to_u8),
1102        // +spec:overflow:17654b - overflow-block / overflow-inline resolve to
1103        // the physical axis through the writing mode. Application is in
1104        // declaration order (a later physical declaration overwrites the
1105        // same tier1 slot and vice versa), which is exactly CSS's
1106        // equal-specificity last-wins rule for logical/physical pairs. The
1107        // writing mode is read from tier1 AT THIS POINT: the inherited value
1108        // is already present (inheritance runs first), so only the exotic
1109        // "writing-mode declared AFTER a logical overflow on the SAME node"
1110        // ordering maps against the pre-declaration mode.
1111        CssProperty::OverflowBlock(v) => {
1112            if let Some(val) = v.get_property() {
1113                let wm_bits = ((*tier1 >> WRITING_MODE_SHIFT) & WRITING_MODE_MASK) as u8;
1114                let vertical = wm_bits == layout_writing_mode_to_u8(
1115                    azul_css::props::layout::wrapping::LayoutWritingMode::VerticalRl,
1116                ) || wm_bits == layout_writing_mode_to_u8(
1117                    azul_css::props::layout::wrapping::LayoutWritingMode::VerticalLr,
1118                );
1119                let shift = if vertical { OVERFLOW_X_SHIFT } else { OVERFLOW_Y_SHIFT };
1120                let enc = u64::from(layout_overflow_to_u8(*val));
1121                *tier1 = (*tier1 & !(OVERFLOW_MASK << shift)) | ((enc & OVERFLOW_MASK) << shift);
1122            }
1123        }
1124        CssProperty::OverflowInline(v) => {
1125            if let Some(val) = v.get_property() {
1126                let wm_bits = ((*tier1 >> WRITING_MODE_SHIFT) & WRITING_MODE_MASK) as u8;
1127                let vertical = wm_bits == layout_writing_mode_to_u8(
1128                    azul_css::props::layout::wrapping::LayoutWritingMode::VerticalRl,
1129                ) || wm_bits == layout_writing_mode_to_u8(
1130                    azul_css::props::layout::wrapping::LayoutWritingMode::VerticalLr,
1131                );
1132                let shift = if vertical { OVERFLOW_Y_SHIFT } else { OVERFLOW_X_SHIFT };
1133                let enc = u64::from(layout_overflow_to_u8(*val));
1134                *tier1 = (*tier1 & !(OVERFLOW_MASK << shift)) | ((enc & OVERFLOW_MASK) << shift);
1135            }
1136        }
1137        CssProperty::BoxSizing(v) => set_tier1!(v, BOX_SIZING_SHIFT, BOX_SIZING_MASK, layout_box_sizing_to_u8),
1138        CssProperty::FlexDirection(v) => set_tier1!(v, FLEX_DIRECTION_SHIFT, FLEX_DIR_MASK, layout_flex_direction_to_u8),
1139        CssProperty::FlexWrap(v) => set_tier1!(v, FLEX_WRAP_SHIFT, FLEX_WRAP_MASK, layout_flex_wrap_to_u8),
1140        CssProperty::JustifyContent(v) => set_tier1!(v, JUSTIFY_CONTENT_SHIFT, JUSTIFY_MASK, layout_justify_content_to_u8),
1141        CssProperty::AlignItems(v) => set_tier1!(v, ALIGN_ITEMS_SHIFT, ALIGN_MASK, layout_align_items_to_u8),
1142        CssProperty::AlignContent(v) => set_tier1!(v, ALIGN_CONTENT_SHIFT, ALIGN_MASK, layout_align_content_to_u8),
1143        CssProperty::WritingMode(v) => set_tier1!(v, WRITING_MODE_SHIFT, WRITING_MODE_MASK, layout_writing_mode_to_u8),
1144        CssProperty::Clear(v) => set_tier1!(v, CLEAR_SHIFT, CLEAR_MASK, layout_clear_to_u8),
1145        CssProperty::FontWeight(v) => set_tier1!(v, FONT_WEIGHT_SHIFT, FONT_WEIGHT_MASK, style_font_weight_to_u8),
1146        CssProperty::FontStyle(v) => set_tier1!(v, FONT_STYLE_SHIFT, FONT_STYLE_MASK, style_font_style_to_u8),
1147        CssProperty::TextAlign(v) => set_tier1!(v, TEXT_ALIGN_SHIFT, TEXT_ALIGN_MASK, style_text_align_to_u8),
1148        CssProperty::Visibility(v) => set_tier1!(v, VISIBILITY_SHIFT, VISIBILITY_MASK, style_visibility_to_u8),
1149        CssProperty::WhiteSpace(v) => set_tier1!(v, WHITE_SPACE_SHIFT, WHITE_SPACE_MASK, style_white_space_to_u8),
1150        CssProperty::Direction(v) => set_tier1!(v, DIRECTION_SHIFT, DIRECTION_MASK, style_direction_to_u8),
1151        CssProperty::VerticalAlign(v) => set_tier1!(v, VERTICAL_ALIGN_SHIFT, VERTICAL_ALIGN_MASK, style_vertical_align_to_u8),
1152        CssProperty::BorderCollapse(v) => set_tier1!(v, BORDER_COLLAPSE_SHIFT, BORDER_COLLAPSE_MASK, border_collapse_to_u8),
1153        CssProperty::AlignSelf(v) => set_tier1!(v, ALIGN_SELF_SHIFT, ALIGN_SELF_MASK, layout_align_self_to_u8),
1154        CssProperty::JustifySelf(v) => set_tier1!(v, JUSTIFY_SELF_SHIFT, JUSTIFY_SELF_MASK, layout_justify_self_to_u8),
1155        CssProperty::GridAutoFlow(v) => set_tier1!(v, GRID_AUTO_FLOW_SHIFT, GRID_AUTO_FLOW_MASK, layout_grid_auto_flow_to_u8),
1156        CssProperty::JustifyItems(v) => set_tier1!(v, JUSTIFY_ITEMS_SHIFT, JUSTIFY_ITEMS_MASK, layout_justify_items_to_u8),
1157
1158        // Tier 2 dimensions
1159        CssProperty::Width(v) => { dims.width = encode_layout_width(v); }
1160        CssProperty::Height(v) => { dims.height = encode_layout_height(v); }
1161        CssProperty::MinWidth(v) => { dims.min_width = encode_pixel_prop(v); }
1162        CssProperty::MaxWidth(v) => { dims.max_width = encode_pixel_prop(v); }
1163        CssProperty::MinHeight(v) => { dims.min_height = encode_pixel_prop(v); }
1164        CssProperty::MaxHeight(v) => { dims.max_height = encode_pixel_prop(v); }
1165        CssProperty::FlexBasis(v) => { dims.flex_basis = encode_flex_basis(v); }
1166        CssProperty::FontSize(v) => { dims.font_size = encode_pixel_prop(v); }
1167        CssProperty::PaddingTop(v) => { dims.padding_top = encode_css_pixel_as_i16(v); }
1168        CssProperty::PaddingRight(v) => { dims.padding_right = encode_css_pixel_as_i16(v); }
1169        CssProperty::PaddingBottom(v) => { dims.padding_bottom = encode_css_pixel_as_i16(v); }
1170        CssProperty::PaddingLeft(v) => { dims.padding_left = encode_css_pixel_as_i16(v); }
1171        CssProperty::MarginTop(v) => { dims.margin_top = encode_margin_i16(v); }
1172        CssProperty::MarginRight(v) => { dims.margin_right = encode_margin_i16(v); }
1173        CssProperty::MarginBottom(v) => { dims.margin_bottom = encode_margin_i16(v); }
1174        CssProperty::MarginLeft(v) => { dims.margin_left = encode_margin_i16(v); }
1175        CssProperty::BorderTopWidth(v) => { dims.border_top_width = encode_css_pixel_as_i16(v); }
1176        CssProperty::BorderRightWidth(v) => { dims.border_right_width = encode_css_pixel_as_i16(v); }
1177        CssProperty::BorderBottomWidth(v) => { dims.border_bottom_width = encode_css_pixel_as_i16(v); }
1178        CssProperty::BorderLeftWidth(v) => { dims.border_left_width = encode_css_pixel_as_i16(v); }
1179        CssProperty::Top(v) => { dims.top = encode_css_pixel_as_i16(v); }
1180        CssProperty::Right(v) => { dims.right = encode_css_pixel_as_i16(v); }
1181        CssProperty::Bottom(v) => { dims.bottom = encode_css_pixel_as_i16(v); }
1182        CssProperty::Left(v) => { dims.left = encode_css_pixel_as_i16(v); }
1183        CssProperty::FlexGrow(v) => {
1184            if let Some(exact) = v.get_property() {
1185                dims.flex_grow = encode_flex_u16(exact.inner.get());
1186            }
1187        }
1188        CssProperty::FlexShrink(v) => {
1189            if let Some(exact) = v.get_property() {
1190                dims.flex_shrink = encode_flex_u16(exact.inner.get());
1191            }
1192        }
1193
1194        CssProperty::RowGap(v) => {
1195            if let Some(g) = v.get_property() {
1196                if g.inner.metric == SizeMetric::Px {
1197                    dims.row_gap = encode_resolved_px_i16(g.inner.number.get());
1198                }
1199            }
1200        }
1201        CssProperty::ColumnGap(v) => {
1202            if let Some(g) = v.get_property() {
1203                if g.inner.metric == SizeMetric::Px {
1204                    dims.column_gap = encode_resolved_px_i16(g.inner.number.get());
1205                }
1206            }
1207        }
1208        CssProperty::Gap(v) => {
1209            if let Some(g) = v.get_property() {
1210                if g.inner.metric == SizeMetric::Px {
1211                    let enc = encode_resolved_px_i16(g.inner.number.get());
1212                    dims.row_gap = enc;
1213                    dims.column_gap = enc;
1214                }
1215            }
1216        }
1217
1218        // Grid placement (compact encoding for common Auto/Line cases)
1219        CssProperty::GridColumn(v) => {
1220            if let Some(gp) = v.get_property() {
1221                cold.grid_col_start = encode_grid_line(&gp.grid_start);
1222                cold.grid_col_end = encode_grid_line(&gp.grid_end);
1223            }
1224        }
1225        CssProperty::GridRow(v) => {
1226            if let Some(gp) = v.get_property() {
1227                cold.grid_row_start = encode_grid_line(&gp.grid_start);
1228                cold.grid_row_end = encode_grid_line(&gp.grid_end);
1229            }
1230        }
1231
1232        // Tier 2 cold
1233        CssProperty::ZIndex(v) => {
1234            if let Some(exact) = v.get_property() {
1235                match exact {
1236                    LayoutZIndex::Auto => cold.z_index = I16_AUTO,
1237                    LayoutZIndex::Integer(z) => {
1238                        // Two-sided (see the tier2_cold path above): a large negative z
1239                        // used to wrap positive via `*z as i16`. Escape both ends.
1240                        cold.z_index = if *z >= -32768 && *z < i32::from(I16_SENTINEL_THRESHOLD) {
1241                            *z as i16
1242                        } else {
1243                            I16_SENTINEL
1244                        };
1245                    }
1246                }
1247            }
1248        }
1249        CssProperty::BorderTopStyle(v) => {
1250            if let Some(exact) = v.get_property() {
1251                let bs = u16::from(border_style_to_u8(exact.inner));
1252                cold.border_styles_packed = (cold.border_styles_packed & !0x000F) | bs;
1253            }
1254        }
1255        CssProperty::BorderRightStyle(v) => {
1256            if let Some(exact) = v.get_property() {
1257                let bs = u16::from(border_style_to_u8(exact.inner));
1258                cold.border_styles_packed = (cold.border_styles_packed & !0x00F0) | (bs << 4);
1259            }
1260        }
1261        CssProperty::BorderBottomStyle(v) => {
1262            if let Some(exact) = v.get_property() {
1263                let bs = u16::from(border_style_to_u8(exact.inner));
1264                cold.border_styles_packed = (cold.border_styles_packed & !0x0F00) | (bs << 8);
1265            }
1266        }
1267        CssProperty::BorderLeftStyle(v) => {
1268            if let Some(exact) = v.get_property() {
1269                let bs = u16::from(border_style_to_u8(exact.inner));
1270                cold.border_styles_packed = (cold.border_styles_packed & !0xF000) | (bs << 12);
1271            }
1272        }
1273        CssProperty::BorderTopColor(v) => {
1274            if let Some(c) = v.get_property() { cold.border_top_color = encode_color_u32(&c.inner); }
1275        }
1276        CssProperty::BorderRightColor(v) => {
1277            if let Some(c) = v.get_property() { cold.border_right_color = encode_color_u32(&c.inner); }
1278        }
1279        CssProperty::BorderBottomColor(v) => {
1280            if let Some(c) = v.get_property() { cold.border_bottom_color = encode_color_u32(&c.inner); }
1281        }
1282        CssProperty::BorderLeftColor(v) => {
1283            if let Some(c) = v.get_property() { cold.border_left_color = encode_color_u32(&c.inner); }
1284        }
1285        CssProperty::BorderSpacing(v) => {
1286            if let Some(spacing) = v.get_property() {
1287                if spacing.horizontal.metric == SizeMetric::Px {
1288                    cold.border_spacing_h = encode_resolved_px_i16(spacing.horizontal.number.get());
1289                }
1290                if spacing.vertical.metric == SizeMetric::Px {
1291                    cold.border_spacing_v = encode_resolved_px_i16(spacing.vertical.number.get());
1292                }
1293            }
1294        }
1295        CssProperty::TabSize(v) => { cold.tab_size = encode_css_pixel_as_i16(v); }
1296
1297        // Tier 2b text
1298        CssProperty::TextColor(v) => {
1299            if let Some(color) = v.get_property() {
1300                let c = &color.inner;
1301                text.text_color = (u32::from(c.r) << 24) | (u32::from(c.g) << 16) | (u32::from(c.b) << 8) | u32::from(c.a);
1302            }
1303        }
1304        CssProperty::FontFamily(v) => {
1305            if let Some(families) = v.get_property() {
1306                let mut hasher = DefaultHasher::new();
1307                families.hash(&mut hasher);
1308                let h = hasher.finish();
1309                let h = if h == 0 { 1 } else { h };
1310                text.font_family_hash = h;
1311                font_hash_map.insert(h, families.clone());
1312            }
1313        }
1314        CssProperty::LineHeight(v) => {
1315            if let Some(lh) = v.get_property() {
1316                // Split scale by SIGN (see the builder's line-height pre-pass
1317                // and compact_cache.rs field doc): negative normalized =
1318                // absolute px, stored as -px x 10; positive = multiple,
1319                // stored x 1000. A single x1000 scale overflowed i16 for any
1320                // absolute line-height above 32.76px and silently became
1321                // "normal" via the sentinel.
1322                let n = lh.inner.normalized();
1323                let stored = if n < 0.0 {
1324                    ((n * 10.0).round() as i32).max(-32768)
1325                } else {
1326                    (n * 1000.0).round() as i32
1327                };
1328                if stored >= -32768 && stored < i32::from(I16_SENTINEL_THRESHOLD) {
1329                    text.line_height = stored as i16;
1330                } else {
1331                    text.line_height = I16_SENTINEL;
1332                }
1333            }
1334        }
1335        CssProperty::LetterSpacing(v) => { text.letter_spacing = encode_css_pixel_as_i16(v); }
1336        CssProperty::WordSpacing(v) => { text.word_spacing = encode_css_pixel_as_i16(v); }
1337        CssProperty::TextIndent(v) => { text.text_indent = encode_css_pixel_as_i16(v); }
1338
1339        // Border radii (cold): encode px × 10 into i16; sentinel stays = unset/0
1340        CssProperty::BorderTopLeftRadius(v) => {
1341            if let Some(exact) = v.get_property() {
1342                if exact.inner.metric == SizeMetric::Px {
1343                    cold.border_top_left_radius = encode_resolved_px_i16(exact.inner.number.get());
1344                }
1345            }
1346        }
1347        CssProperty::BorderTopRightRadius(v) => {
1348            if let Some(exact) = v.get_property() {
1349                if exact.inner.metric == SizeMetric::Px {
1350                    cold.border_top_right_radius = encode_resolved_px_i16(exact.inner.number.get());
1351                }
1352            }
1353        }
1354        CssProperty::BorderBottomLeftRadius(v) => {
1355            if let Some(exact) = v.get_property() {
1356                if exact.inner.metric == SizeMetric::Px {
1357                    cold.border_bottom_left_radius = encode_resolved_px_i16(exact.inner.number.get());
1358                }
1359            }
1360        }
1361        CssProperty::BorderBottomRightRadius(v) => {
1362            if let Some(exact) = v.get_property() {
1363                if exact.inner.metric == SizeMetric::Px {
1364                    cold.border_bottom_right_radius = encode_resolved_px_i16(exact.inner.number.get());
1365                }
1366            }
1367        }
1368
1369        // Opacity: encode as 0-254, 255 = sentinel (unset/default = 1.0)
1370        CssProperty::Opacity(v) => {
1371            if let Some(exact) = v.get_property() {
1372                let o = exact.inner.normalized().clamp(0.0, 1.0);
1373                let byte = (o * 254.0).round() as u8;
1374                // byte is in [0, 254], never collides with OPACITY_SENTINEL=255
1375                cold.opacity = byte;
1376            }
1377        }
1378
1379        // has-flags: set bit whenever property is set (regardless of value).
1380        // Getter uses this as a fast "is the default" bail-out.
1381        CssProperty::Transform(v) => {
1382            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_TRANSFORM; }
1383        }
1384        CssProperty::TransformOrigin(v) => {
1385            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_TRANSFORM_ORIGIN; }
1386        }
1387        // All four shadow sides wrap the same StyleBoxShadowValue and set the
1388        // single has-box-shadow bit.
1389        CssProperty::BoxShadowTop(v)
1390        | CssProperty::BoxShadowBottom(v)
1391        | CssProperty::BoxShadowLeft(v)
1392        | CssProperty::BoxShadowRight(v) => {
1393            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_BOX_SHADOW; }
1394        }
1395        CssProperty::TextDecoration(v) => {
1396            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_TEXT_DECORATION; }
1397        }
1398        CssProperty::ScrollbarGutter(v) => {
1399            if let Some(exact) = v.get_property() {
1400                use azul_css::props::layout::overflow::StyleScrollbarGutter;
1401                let bits: u8 = match exact {
1402                    StyleScrollbarGutter::Auto => SCROLLBAR_GUTTER_AUTO,
1403                    StyleScrollbarGutter::Stable => SCROLLBAR_GUTTER_STABLE,
1404                    StyleScrollbarGutter::StableBothEdges => SCROLLBAR_GUTTER_BOTH_EDGES,
1405                };
1406                cold.hot_flags = (cold.hot_flags & !HOT_FLAG_SCROLLBAR_GUTTER_MASK)
1407                    | ((bits << HOT_FLAG_SCROLLBAR_GUTTER_SHIFT) & HOT_FLAG_SCROLLBAR_GUTTER_MASK);
1408            }
1409        }
1410        CssProperty::BackgroundContent(v) => {
1411            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_BACKGROUND; }
1412        }
1413        CssProperty::ClipPath(v) => {
1414            if v.get_property().is_some() { cold.hot_flags |= HOT_FLAG_HAS_CLIP_PATH; }
1415        }
1416
1417        // Any scrollbar customisation sets the single `has_any_scrollbar_css`
1418        // bit. When unset, get_scrollbar_style can bail to UA defaults without
1419        // doing 8 cascade walks.
1420        CssProperty::ScrollbarTrack(v) => {
1421            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1422        }
1423        CssProperty::ScrollbarThumb(v) => {
1424            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1425        }
1426        CssProperty::ScrollbarButton(v) => {
1427            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1428        }
1429        CssProperty::ScrollbarCorner(v) => {
1430            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1431        }
1432        CssProperty::ScrollbarWidth(v) => {
1433            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1434        }
1435        CssProperty::ScrollbarColor(v) => {
1436            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1437        }
1438        CssProperty::ScrollbarVisibility(v) => {
1439            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1440        }
1441        CssProperty::ScrollbarFadeDelay(v) => {
1442            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1443        }
1444        CssProperty::ScrollbarFadeDuration(v) => {
1445            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_SCROLLBAR_CSS; }
1446        }
1447
1448        // Rare paint/layout props with dedicated fast-path bits.
1449        CssProperty::CounterReset(v) => {
1450            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_COUNTER; }
1451        }
1452        CssProperty::CounterIncrement(v) => {
1453            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_COUNTER; }
1454        }
1455        // Both break-before/after wrap PageBreakValue and set the has-break bit.
1456        CssProperty::BreakBefore(v) | CssProperty::BreakAfter(v) => {
1457            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_BREAK; }
1458        }
1459        CssProperty::TextOrientation(v) => {
1460            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_TEXT_ORIENTATION; }
1461        }
1462        CssProperty::TextShadow(v) => {
1463            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_TEXT_SHADOW; }
1464        }
1465        CssProperty::BackdropFilter(v) => {
1466            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_BACKDROP_FILTER; }
1467        }
1468        CssProperty::Filter(v) => {
1469            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_FILTER; }
1470        }
1471        CssProperty::MixBlendMode(v) => {
1472            if v.get_property().is_some() { cold.extra_flags |= EXTRA_FLAG_HAS_MIX_BLEND_MODE; }
1473        }
1474
1475        // Non-compact properties (background, etc.) — handled by get_property_slow fallback
1476        _ => {}
1477    }
1478}
1479
1480/// OR the DOM-level declared-flag for rarely-set text properties. Called once
1481/// per property per node so that when a flag bit is clear, callers
1482/// (e.g. `translate_to_text3_constraints`) can skip the cascade walk and use
1483/// the default value — the slow walk would never find a declaration anyway.
1484const fn update_dom_declared_flags(prop: &CssProperty, flags: &mut u32) {
1485    // Only mark if the property value is actually "set" (not Auto/Initial/etc.).
1486    // Using `get_property().is_some()` mirrors the pattern used elsewhere in
1487    // this builder for has-X bits.
1488    match prop {
1489        CssProperty::ShapeInside(v) => if v.get_property().is_some() { *flags |= DOM_HAS_SHAPE_INSIDE; }
1490        CssProperty::ShapeOutside(v) => if v.get_property().is_some() { *flags |= DOM_HAS_SHAPE_OUTSIDE; }
1491        CssProperty::TextJustify(v) => if v.get_property().is_some() { *flags |= DOM_HAS_TEXT_JUSTIFY; }
1492        CssProperty::TextIndent(v) => if v.get_property().is_some() { *flags |= DOM_HAS_TEXT_INDENT; }
1493        CssProperty::ColumnCount(v) => if v.get_property().is_some() { *flags |= DOM_HAS_COLUMN_COUNT; }
1494        CssProperty::ColumnGap(v) => if v.get_property().is_some() { *flags |= DOM_HAS_COLUMN_GAP; }
1495        CssProperty::ColumnWidth(v) => if v.get_property().is_some() { *flags |= DOM_HAS_COLUMN_WIDTH; }
1496        CssProperty::InitialLetter(v) => if v.get_property().is_some() { *flags |= DOM_HAS_INITIAL_LETTER; }
1497        CssProperty::InitialLetterAlign(v) => if v.get_property().is_some() { *flags |= DOM_HAS_INITIAL_LETTER_ALIGN; }
1498        CssProperty::LineClamp(v) => if v.get_property().is_some() { *flags |= DOM_HAS_LINE_CLAMP; }
1499        CssProperty::HangingPunctuation(v) => if v.get_property().is_some() { *flags |= DOM_HAS_HANGING_PUNCTUATION; }
1500        CssProperty::TextCombineUpright(v) => if v.get_property().is_some() { *flags |= DOM_HAS_TEXT_COMBINE_UPRIGHT; }
1501        CssProperty::ExclusionMargin(v) => if v.get_property().is_some() { *flags |= DOM_HAS_EXCLUSION_MARGIN; }
1502        CssProperty::ShapeMargin(v) => if v.get_property().is_some() { *flags |= DOM_HAS_SHAPE_MARGIN; }
1503        CssProperty::HyphenationLanguage(v) => if v.get_property().is_some() { *flags |= DOM_HAS_HYPHENATION_LANGUAGE; }
1504        CssProperty::UnicodeBidi(v) => if v.get_property().is_some() { *flags |= DOM_HAS_UNICODE_BIDI; }
1505        CssProperty::TextBoxTrim(v) => if v.get_property().is_some() { *flags |= DOM_HAS_TEXT_BOX_TRIM; }
1506        CssProperty::Hyphens(v) => if v.get_property().is_some() { *flags |= DOM_HAS_HYPHENS; }
1507        CssProperty::WordBreak(v) => if v.get_property().is_some() { *flags |= DOM_HAS_WORD_BREAK; }
1508        CssProperty::OverflowWrap(v) => if v.get_property().is_some() { *flags |= DOM_HAS_OVERFLOW_WRAP; }
1509        CssProperty::LineBreak(v) => if v.get_property().is_some() { *flags |= DOM_HAS_LINE_BREAK; }
1510        CssProperty::TextAlignLast(v) => if v.get_property().is_some() { *flags |= DOM_HAS_TEXT_ALIGN_LAST; }
1511        CssProperty::LineHeight(v) => if v.get_property().is_some() { *flags |= DOM_HAS_LINE_HEIGHT; }
1512        _ => {}
1513    }
1514}
1515
1516// =============================================================================
1517// Helper encoders for dimension properties
1518// =============================================================================
1519
1520/// Encode a `GridLine` into i16: `Auto=I16_AUTO`, Line(n)=n, Span(n)=-(n).
1521/// Named lines fall back to `I16_SENTINEL` (not compact-encodable).
1522// const fn: the `n as i16` casts are guarded by explicit +/-32000 range checks.
1523#[allow(clippy::cast_possible_truncation)]
1524const fn encode_grid_line(line: &azul_css::props::layout::grid::GridLine) -> i16 {
1525    use azul_css::props::layout::grid::GridLine;
1526    match line {
1527        GridLine::Auto => I16_AUTO,
1528        GridLine::Line(n) => {
1529            if *n >= -32000 && *n <= 32000 { *n as i16 } else { I16_SENTINEL }
1530        }
1531        GridLine::Span(n) => {
1532            if *n >= 1 && *n <= 32000 { -(*n as i16) } else { I16_SENTINEL }
1533        }
1534        GridLine::Named(_) => I16_SENTINEL,
1535    }
1536}
1537
1538/// Encode a `CssPropertyValue`<LayoutWidth> into u32 compact form.
1539fn encode_layout_width<T: LayoutWidthLike>(val: &CssPropertyValue<T>) -> u32 {
1540    match val {
1541        CssPropertyValue::Exact(w) => w.encode_compact_u32(),
1542        CssPropertyValue::Auto => U32_AUTO,
1543        CssPropertyValue::Initial => U32_INITIAL,
1544        CssPropertyValue::Inherit => U32_INHERIT,
1545        CssPropertyValue::None => U32_NONE,
1546        _ => U32_SENTINEL,
1547    }
1548}
1549
1550/// Encode a `CssPropertyValue`<LayoutHeight> into u32 compact form.
1551fn encode_layout_height<T: LayoutWidthLike>(val: &CssPropertyValue<T>) -> u32 {
1552    encode_layout_width(val)
1553}
1554
1555/// Trait for types that can be encoded as compact u32 dimension values.
1556/// Implemented for `LayoutWidth`, `LayoutHeight` (which are Auto|Px|MinContent|MaxContent|Calc enums).
1557trait LayoutWidthLike {
1558    fn encode_compact_u32(&self) -> u32;
1559}
1560
1561impl LayoutWidthLike for LayoutWidth {
1562    fn encode_compact_u32(&self) -> u32 {
1563        match self {
1564            Self::Auto => U32_AUTO,
1565            Self::Px(pv) => encode_pixel_value_u32(pv),
1566            Self::MinContent => U32_MIN_CONTENT,
1567            Self::MaxContent => U32_MAX_CONTENT,
1568            // FitContent/Calc are not compact-encodable → overflow to tier 3.
1569            Self::FitContent(_) | Self::Calc(_) => U32_SENTINEL,
1570        }
1571    }
1572}
1573
1574impl LayoutWidthLike for LayoutHeight {
1575    fn encode_compact_u32(&self) -> u32 {
1576        match self {
1577            Self::Auto => U32_AUTO,
1578            Self::Px(pv) => encode_pixel_value_u32(pv),
1579            Self::MinContent => U32_MIN_CONTENT,
1580            Self::MaxContent => U32_MAX_CONTENT,
1581            // FitContent/Calc are not compact-encodable → overflow to tier 3.
1582            Self::FitContent(_) | Self::Calc(_) => U32_SENTINEL,
1583        }
1584    }
1585}
1586
1587/// Encode a `CssPropertyValue` wrapping a simple `PixelValue` struct (`LayoutMinWidth`, etc.)
1588fn encode_pixel_prop<T: HasInnerPixelValue>(val: &CssPropertyValue<T>) -> u32 {
1589    match val {
1590        CssPropertyValue::Exact(inner) => encode_pixel_value_u32(&inner.get_inner_pixel()),
1591        CssPropertyValue::Auto => U32_AUTO,
1592        CssPropertyValue::Initial => U32_INITIAL,
1593        CssPropertyValue::Inherit => U32_INHERIT,
1594        CssPropertyValue::None => U32_NONE,
1595        _ => U32_SENTINEL,
1596    }
1597}
1598
1599/// Trait for dimension structs wrapping `inner: PixelValue`.
1600trait HasInnerPixelValue {
1601    fn get_inner_pixel(&self) -> azul_css::props::basic::pixel::PixelValue;
1602}
1603
1604macro_rules! impl_has_inner_pixel {
1605    ($($ty:ty),*) => {
1606        $(
1607            impl HasInnerPixelValue for $ty {
1608                fn get_inner_pixel(&self) -> azul_css::props::basic::pixel::PixelValue {
1609                    self.inner
1610                }
1611            }
1612        )*
1613    };
1614}
1615
1616impl_has_inner_pixel!(
1617    azul_css::props::layout::dimensions::LayoutMinWidth,
1618    azul_css::props::layout::dimensions::LayoutMaxWidth,
1619    azul_css::props::layout::dimensions::LayoutMinHeight,
1620    azul_css::props::layout::dimensions::LayoutMaxHeight,
1621    azul_css::props::basic::font::StyleFontSize,
1622    azul_css::props::layout::spacing::LayoutPaddingTop,
1623    azul_css::props::layout::spacing::LayoutPaddingRight,
1624    azul_css::props::layout::spacing::LayoutPaddingBottom,
1625    azul_css::props::layout::spacing::LayoutPaddingLeft,
1626    azul_css::props::layout::spacing::LayoutMarginTop,
1627    azul_css::props::layout::spacing::LayoutMarginRight,
1628    azul_css::props::layout::spacing::LayoutMarginBottom,
1629    azul_css::props::layout::spacing::LayoutMarginLeft,
1630    azul_css::props::style::border::LayoutBorderTopWidth,
1631    azul_css::props::style::border::LayoutBorderRightWidth,
1632    azul_css::props::style::border::LayoutBorderBottomWidth,
1633    azul_css::props::style::border::LayoutBorderLeftWidth,
1634    azul_css::props::layout::position::LayoutTop,
1635    azul_css::props::layout::position::LayoutRight,
1636    azul_css::props::layout::position::LayoutInsetBottom,
1637    azul_css::props::layout::position::LayoutLeft,
1638    azul_css::props::style::text::StyleLetterSpacing,
1639    azul_css::props::style::text::StyleWordSpacing,
1640    azul_css::props::style::text::StyleTextIndent,
1641    azul_css::props::style::text::StyleTabSize
1642);
1643
1644/// Encode a `CssPropertyValue`<T> where T wraps a `PixelValue`, as i16 (×10 resolved px).
1645/// Delegates to the canonical `azul_css::compact_cache::encode_css_pixel_as_i16`.
1646fn encode_css_pixel_as_i16<T: HasInnerPixelValue>(val: &CssPropertyValue<T>) -> i16 {
1647    let mapped = match val {
1648        CssPropertyValue::Exact(inner) => CssPropertyValue::Exact(inner.get_inner_pixel()),
1649        CssPropertyValue::Auto => CssPropertyValue::Auto,
1650        CssPropertyValue::Initial => CssPropertyValue::Initial,
1651        CssPropertyValue::Inherit => CssPropertyValue::Inherit,
1652        CssPropertyValue::None => CssPropertyValue::None,
1653        _ => return I16_SENTINEL,
1654    };
1655    azul_css::compact_cache::encode_css_pixel_as_i16(&mapped)
1656}
1657
1658/// Encode margin: same as `encode_css_pixel_as_i16` but Auto is a distinct value.
1659fn encode_margin_i16<T: HasInnerPixelValue>(val: &CssPropertyValue<T>) -> i16 {
1660    encode_css_pixel_as_i16(val)
1661}
1662
1663/// Encode `CssPropertyValue`<LayoutFlexBasis> — `LayoutFlexBasis` is Auto | Exact(PixelValue).
1664fn encode_flex_basis(val: &CssPropertyValue<LayoutFlexBasis>) -> u32 {
1665    match val {
1666        CssPropertyValue::Exact(fb) => match fb {
1667            LayoutFlexBasis::Auto => U32_AUTO,
1668            LayoutFlexBasis::Exact(pv) => encode_pixel_value_u32(pv),
1669        },
1670        CssPropertyValue::Auto => U32_AUTO,
1671        CssPropertyValue::Initial => U32_INITIAL,
1672        CssPropertyValue::Inherit => U32_INHERIT,
1673        CssPropertyValue::None => U32_NONE,
1674        _ => U32_SENTINEL,
1675    }
1676}
1677
1678#[cfg(test)]
1679mod audit_tests {
1680    use super::resolve_font_size_to_px;
1681    use crate::dom::NodeId;
1682    use azul_css::compact_cache::{
1683        decode_pixel_value_u32, encode_pixel_value_u32, CompactNodeProps,
1684    };
1685    use azul_css::props::basic::pixel::PixelValue;
1686
1687    // Happy path: an `em` font-size resolves against a valid (pre-order) parent.
1688    #[test]
1689    fn resolve_font_size_em_from_parent() {
1690        let mut dims = vec![CompactNodeProps::default(); 2];
1691        dims[0].font_size = encode_pixel_value_u32(&PixelValue::px(20.0));
1692        dims[1].font_size = encode_pixel_value_u32(&PixelValue::em(2.0));
1693        resolve_font_size_to_px(&mut dims, 1, Some(NodeId::new(0)));
1694        let pv = decode_pixel_value_u32(dims[1].font_size).unwrap();
1695        assert!((pv.number.get() - 40.0).abs() < 0.01, "got {}", pv.number.get());
1696    }
1697
1698    // Root `em` (no parent) uses the 16px CSS initial value.
1699    #[test]
1700    fn resolve_font_size_root_em_uses_default() {
1701        let mut dims = vec![CompactNodeProps::default()];
1702        dims[0].font_size = encode_pixel_value_u32(&PixelValue::em(2.0));
1703        resolve_font_size_to_px(&mut dims, 0, None);
1704        let pv = decode_pixel_value_u32(dims[0].font_size).unwrap();
1705        assert!((pv.number.get() - 32.0).abs() < 0.01, "got {}", pv.number.get());
1706    }
1707
1708    // A `rem` value reads the root (index 0) via the `.first()` guard without
1709    // panicking (previously indexed `tier2_dims[0]` directly).
1710    #[test]
1711    fn resolve_font_size_rem_reads_root() {
1712        let mut dims = vec![CompactNodeProps::default(); 2];
1713        dims[0].font_size = encode_pixel_value_u32(&PixelValue::px(10.0)); // root
1714        dims[1].font_size = encode_pixel_value_u32(&PixelValue::rem(3.0)); // child rem
1715        resolve_font_size_to_px(&mut dims, 1, Some(NodeId::new(0)));
1716        let pv = decode_pixel_value_u32(dims[1].font_size).unwrap();
1717        assert!((pv.number.get() - 30.0).abs() < 0.01, "got {}", pv.number.get());
1718    }
1719}
1720
1721// =============================================================================
1722// Adversarial unit tests (autotest)
1723//
1724// Inline module: the encoders below (`encode_grid_line`, `encode_layout_width`,
1725// `encode_pixel_prop`, `encode_css_pixel_as_i16`, `encode_margin_i16`,
1726// `encode_flex_basis`, `apply_css_property_to_compact`, `apply_ua_css_to_compact`,
1727// `update_dom_declared_flags`, `resolve_font_size_to_px`) are all private, so they
1728// can only be exercised from inside this module.
1729//
1730// Focus: overflow / saturation / sentinel-aliasing / round-trip fidelity, i.e. the
1731// places where a fixed-point codec silently turns one CSS value into a different
1732// one instead of panicking.
1733// =============================================================================
1734#[cfg(test)]
1735#[allow(
1736    clippy::float_cmp,
1737    clippy::unreadable_literal,
1738    clippy::too_many_lines,
1739    clippy::cast_lossless
1740)]
1741mod autotest_generated {
1742    use super::*;
1743
1744    use alloc::collections::BTreeMap;
1745
1746    use crate::dom::NodeType;
1747    use crate::styled_dom::NodeHierarchyItem;
1748    use azul_css::props::basic::color::ColorU;
1749    use azul_css::props::basic::font::{StyleFontFamily, StyleFontFamilyVec};
1750    use azul_css::props::basic::length::{FloatValue, PercentageValue};
1751    use azul_css::props::basic::pixel::PixelValue;
1752    use azul_css::props::layout::dimensions::LayoutMinWidth;
1753    use azul_css::props::layout::display::LayoutDisplay;
1754    use azul_css::props::layout::flex::{LayoutFlexGrow, LayoutFlexShrink};
1755    use azul_css::props::layout::grid::{GridLine, GridPlacement, LayoutGap, NamedGridLine};
1756    use azul_css::props::layout::overflow::StyleScrollbarGutter;
1757    use azul_css::props::layout::position::LayoutPosition;
1758    use azul_css::props::layout::spacing::{LayoutMarginTop, LayoutPaddingTop};
1759    use azul_css::props::layout::table::StyleBorderCollapse;
1760    use azul_css::props::style::border::{BorderStyle, StyleBorderTopStyle};
1761    use azul_css::props::style::effects::StyleOpacity;
1762    use azul_css::props::style::text::{
1763        StyleLineHeight, StyleTextColor, StyleTextDecoration, StyleTextIndent,
1764    };
1765
1766    // -------------------------------------------------------------------------
1767    // Fixtures
1768    // -------------------------------------------------------------------------
1769
1770    /// The four compact output slots + the font reverse-map, as one value, so a
1771    /// test can snapshot "everything the writer could have touched".
1772    struct Sink {
1773        tier1: u64,
1774        dims: CompactNodeProps,
1775        cold: CompactNodePropsCold,
1776        text: CompactTextProps,
1777        fonts: BTreeMap<u64, StyleFontFamilyVec>,
1778    }
1779
1780    impl Sink {
1781        fn new() -> Self {
1782            Self {
1783                tier1: 0,
1784                dims: CompactNodeProps::default(),
1785                cold: CompactNodePropsCold::default(),
1786                text: CompactTextProps::default(),
1787                fonts: BTreeMap::new(),
1788            }
1789        }
1790
1791        fn apply(&mut self, prop: &CssProperty) {
1792            apply_css_property_to_compact(
1793                prop,
1794                &mut self.tier1,
1795                &mut self.dims,
1796                &mut self.cold,
1797                &mut self.text,
1798                &mut self.fonts,
1799            );
1800        }
1801
1802        fn ua(&mut self, node_type: &NodeType) {
1803            apply_ua_css_to_compact(
1804                node_type,
1805                &mut self.tier1,
1806                &mut self.dims,
1807                &mut self.cold,
1808                &mut self.text,
1809                &mut self.fonts,
1810            );
1811        }
1812
1813        fn snapshot(&self) -> (u64, CompactNodeProps, CompactNodePropsCold, CompactTextProps) {
1814            (self.tier1, self.dims, self.cold, self.text)
1815        }
1816    }
1817
1818    fn div_nodes(n: usize) -> Vec<NodeData> {
1819        (0..n).map(|_| NodeData::create_node(NodeType::Div)).collect()
1820    }
1821
1822    /// Pre-order chain: node 0 is the root, node `i` is the child of node `i-1`.
1823    /// `NodeHierarchyItem` uses 1-based encoding (0 = None, n = `NodeId(n-1)`).
1824    fn linear_hierarchy(n: usize) -> Vec<NodeHierarchyItem> {
1825        (0..n)
1826            .map(|i| NodeHierarchyItem {
1827                parent: i, // i == 0 -> None; i > 0 -> NodeId(i-1)
1828                previous_sibling: 0,
1829                next_sibling: 0,
1830                last_child: if i + 1 < n { i + 2 } else { 0 },
1831            })
1832            .collect()
1833    }
1834
1835    fn padding(px: f32) -> CssPropertyValue<LayoutPaddingTop> {
1836        CssPropertyValue::Exact(LayoutPaddingTop { inner: PixelValue::px(px) })
1837    }
1838
1839    // -------------------------------------------------------------------------
1840    // encode_grid_line
1841    // -------------------------------------------------------------------------
1842
1843    #[test]
1844    fn grid_line_auto_and_named_map_to_their_sentinels() {
1845        assert_eq!(encode_grid_line(&GridLine::Auto), I16_AUTO);
1846        let named = GridLine::Named(NamedGridLine {
1847            grid_line_name: "sidebar".into(),
1848            span_count: 0,
1849        });
1850        assert_eq!(encode_grid_line(&named), I16_SENTINEL);
1851    }
1852
1853    #[test]
1854    fn grid_line_number_boundaries_saturate_instead_of_truncating() {
1855        assert_eq!(encode_grid_line(&GridLine::Line(0)), 0);
1856        assert_eq!(encode_grid_line(&GridLine::Line(1)), 1);
1857        assert_eq!(encode_grid_line(&GridLine::Line(-1)), -1);
1858        assert_eq!(encode_grid_line(&GridLine::Line(32_000)), 32_000);
1859        assert_eq!(encode_grid_line(&GridLine::Line(-32_000)), -32_000);
1860        // One past the guarded range: must become the sentinel, never a wrapped i16.
1861        assert_eq!(encode_grid_line(&GridLine::Line(32_001)), I16_SENTINEL);
1862        assert_eq!(encode_grid_line(&GridLine::Line(-32_001)), I16_SENTINEL);
1863        assert_eq!(encode_grid_line(&GridLine::Line(i32::MAX)), I16_SENTINEL);
1864        assert_eq!(encode_grid_line(&GridLine::Line(i32::MIN)), I16_SENTINEL);
1865    }
1866
1867    #[test]
1868    fn grid_line_span_boundaries_and_nonsense_spans() {
1869        assert_eq!(encode_grid_line(&GridLine::Span(1)), -1);
1870        assert_eq!(encode_grid_line(&GridLine::Span(32_000)), -32_000);
1871        // `span 0` / negative spans are not representable -> sentinel, NOT 0 (which
1872        // would silently mean "grid line 0").
1873        assert_eq!(encode_grid_line(&GridLine::Span(0)), I16_SENTINEL);
1874        assert_eq!(encode_grid_line(&GridLine::Span(-1)), I16_SENTINEL);
1875        assert_eq!(encode_grid_line(&GridLine::Span(32_001)), I16_SENTINEL);
1876        assert_eq!(encode_grid_line(&GridLine::Span(i32::MAX)), I16_SENTINEL);
1877        assert_eq!(encode_grid_line(&GridLine::Span(i32::MIN)), I16_SENTINEL);
1878    }
1879
1880    #[test]
1881    fn grid_line_in_range_values_never_alias_the_sentinel_band() {
1882        // A real line number that lands on >= I16_SENTINEL_THRESHOLD would decode
1883        // as "auto" / "overflow" and move the item to a different grid cell.
1884        for n in [-32_000i32, -1_000, -1, 0, 1, 1_000, 32_000] {
1885            let e = encode_grid_line(&GridLine::Line(n));
1886            assert!(
1887                e < I16_SENTINEL_THRESHOLD,
1888                "Line({n}) encoded into the sentinel band as {e}"
1889            );
1890        }
1891        for n in [1i32, 2, 1_000, 32_000] {
1892            let e = encode_grid_line(&GridLine::Span(n));
1893            assert!(e < 0, "Span({n}) must encode as a negative value, got {e}");
1894            assert!(
1895                e < I16_SENTINEL_THRESHOLD,
1896                "Span({n}) encoded into the sentinel band as {e}"
1897            );
1898        }
1899    }
1900
1901    // -------------------------------------------------------------------------
1902    // encode_layout_width / encode_layout_height
1903    // -------------------------------------------------------------------------
1904
1905    #[test]
1906    fn layout_width_keywords_map_to_distinct_sentinels() {
1907        let auto: CssPropertyValue<LayoutWidth> = CssPropertyValue::Auto;
1908        let none: CssPropertyValue<LayoutWidth> = CssPropertyValue::None;
1909        let initial: CssPropertyValue<LayoutWidth> = CssPropertyValue::Initial;
1910        let inherit: CssPropertyValue<LayoutWidth> = CssPropertyValue::Inherit;
1911        assert_eq!(encode_layout_width(&auto), U32_AUTO);
1912        assert_eq!(encode_layout_width(&none), U32_NONE);
1913        assert_eq!(encode_layout_width(&initial), U32_INITIAL);
1914        assert_eq!(encode_layout_width(&inherit), U32_INHERIT);
1915    }
1916
1917    #[test]
1918    fn layout_width_revert_and_unset_fall_back_to_the_overflow_sentinel() {
1919        // `revert` / `unset` have no compact slot. They must land on U32_SENTINEL
1920        // (= "ask the slow path"), never on a *semantic* sentinel like AUTO.
1921        let revert: CssPropertyValue<LayoutWidth> = CssPropertyValue::Revert;
1922        let unset: CssPropertyValue<LayoutWidth> = CssPropertyValue::Unset;
1923        assert_eq!(encode_layout_width(&revert), U32_SENTINEL);
1924        assert_eq!(encode_layout_width(&unset), U32_SENTINEL);
1925        assert_eq!(encode_layout_height(&revert), U32_SENTINEL);
1926        assert_eq!(encode_layout_height(&unset), U32_SENTINEL);
1927    }
1928
1929    #[test]
1930    fn layout_width_exact_keyword_variants() {
1931        assert_eq!(
1932            encode_layout_width(&CssPropertyValue::Exact(LayoutWidth::Auto)),
1933            U32_AUTO
1934        );
1935        assert_eq!(
1936            encode_layout_width(&CssPropertyValue::Exact(LayoutWidth::MinContent)),
1937            U32_MIN_CONTENT
1938        );
1939        assert_eq!(
1940            encode_layout_width(&CssPropertyValue::Exact(LayoutWidth::MaxContent)),
1941            U32_MAX_CONTENT
1942        );
1943        // fit-content() is not compact-encodable -> tier 3
1944        assert_eq!(
1945            encode_layout_width(&CssPropertyValue::Exact(LayoutWidth::FitContent(
1946                PixelValue::px(10.0)
1947            ))),
1948            U32_SENTINEL
1949        );
1950    }
1951
1952    #[test]
1953    fn layout_width_px_round_trips() {
1954        for px in [0.0f32, 0.5, 1.0, 100.0, 1234.567, -50.0] {
1955            let enc =
1956                encode_layout_width(&CssPropertyValue::Exact(LayoutWidth::Px(PixelValue::px(px))));
1957            let dec = decode_pixel_value_u32(enc)
1958                .expect("an in-range px value must not encode to a sentinel");
1959            assert_eq!(dec.metric, SizeMetric::Px);
1960            assert!(
1961                (dec.number.get() - px).abs() < 0.002,
1962                "round-trip of {px}px produced {}px",
1963                dec.number.get()
1964            );
1965        }
1966    }
1967
1968    #[test]
1969    fn layout_width_extreme_values_saturate_to_the_overflow_sentinel() {
1970        // Past the 28-bit fixed-point range the encoder must bail to tier 3 rather
1971        // than wrapping the low bits into a small (and plausible-looking) width.
1972        for px in [
1973            1.0e9f32,
1974            -1.0e9,
1975            f32::MAX,
1976            f32::MIN,
1977            f32::INFINITY,
1978            f32::NEG_INFINITY,
1979        ] {
1980            let enc =
1981                encode_layout_width(&CssPropertyValue::Exact(LayoutWidth::Px(PixelValue::px(px))));
1982            assert_eq!(
1983                enc, U32_SENTINEL,
1984                "width {px}px should overflow to U32_SENTINEL, got {enc:#x}"
1985            );
1986        }
1987    }
1988
1989    #[test]
1990    fn layout_width_nan_degrades_to_zero_without_panicking() {
1991        // `NaN as isize` saturates to 0, so a NaN width becomes 0px — deterministic
1992        // and finite, which is what the layout solver needs.
1993        let enc = encode_layout_width(&CssPropertyValue::Exact(LayoutWidth::Px(PixelValue::px(
1994            f32::NAN,
1995        ))));
1996        let dec = decode_pixel_value_u32(enc).expect("NaN must degrade to a value, not a sentinel");
1997        assert!(dec.number.get().is_finite());
1998        assert_eq!(dec.number.get(), 0.0);
1999    }
2000
2001    #[test]
2002    fn layout_height_never_diverges_from_layout_width() {
2003        let vals = [
2004            CssPropertyValue::Exact(LayoutWidth::Auto),
2005            CssPropertyValue::Exact(LayoutWidth::MinContent),
2006            CssPropertyValue::Exact(LayoutWidth::MaxContent),
2007            CssPropertyValue::Exact(LayoutWidth::Px(PixelValue::px(42.0))),
2008            CssPropertyValue::Exact(LayoutWidth::Px(PixelValue::px(1.0e9))),
2009            CssPropertyValue::Unset,
2010        ];
2011        for v in &vals {
2012            assert_eq!(encode_layout_width(v), encode_layout_height(v));
2013        }
2014    }
2015
2016    // -------------------------------------------------------------------------
2017    // encode_pixel_prop
2018    // -------------------------------------------------------------------------
2019
2020    #[test]
2021    fn pixel_prop_keywords_map_to_distinct_sentinels() {
2022        let auto: CssPropertyValue<LayoutMinWidth> = CssPropertyValue::Auto;
2023        let none: CssPropertyValue<LayoutMinWidth> = CssPropertyValue::None;
2024        let initial: CssPropertyValue<LayoutMinWidth> = CssPropertyValue::Initial;
2025        let inherit: CssPropertyValue<LayoutMinWidth> = CssPropertyValue::Inherit;
2026        let revert: CssPropertyValue<LayoutMinWidth> = CssPropertyValue::Revert;
2027        let unset: CssPropertyValue<LayoutMinWidth> = CssPropertyValue::Unset;
2028        assert_eq!(encode_pixel_prop(&auto), U32_AUTO);
2029        assert_eq!(encode_pixel_prop(&none), U32_NONE);
2030        assert_eq!(encode_pixel_prop(&initial), U32_INITIAL);
2031        assert_eq!(encode_pixel_prop(&inherit), U32_INHERIT);
2032        assert_eq!(encode_pixel_prop(&revert), U32_SENTINEL);
2033        assert_eq!(encode_pixel_prop(&unset), U32_SENTINEL);
2034    }
2035
2036    #[test]
2037    fn pixel_prop_round_trips_value_and_metric() {
2038        for pv in [
2039            PixelValue::px(50.0),
2040            PixelValue::em(1.5),
2041            PixelValue::percent(80.0),
2042            PixelValue::pt(12.0),
2043            PixelValue::rem(2.0),
2044        ] {
2045            let enc = encode_pixel_prop(&CssPropertyValue::Exact(LayoutMinWidth { inner: pv }));
2046            let dec = decode_pixel_value_u32(enc).expect("must round-trip");
2047            assert_eq!(dec.metric, pv.metric, "metric lost in the round-trip");
2048            assert!(
2049                (dec.number.get() - pv.number.get()).abs() < 0.002,
2050                "value lost in the round-trip: {} -> {}",
2051                pv.number.get(),
2052                dec.number.get()
2053            );
2054        }
2055    }
2056
2057    #[test]
2058    fn pixel_prop_overflow_saturates() {
2059        let enc = encode_pixel_prop(&CssPropertyValue::Exact(LayoutMinWidth {
2060            inner: PixelValue::px(1.0e9),
2061        }));
2062        assert_eq!(enc, U32_SENTINEL);
2063    }
2064
2065    #[test]
2066    fn pixel_prop_exact_value_never_aliases_a_semantic_sentinel() {
2067        // INVARIANT: an `Exact` length may overflow to U32_SENTINEL (= "slow path"),
2068        // but must never collide with a sentinel that means something *else*
2069        // (auto / none / inherit / initial / min-content / max-content) — that turns
2070        // a length into a different keyword with no way to tell.
2071        //
2072        // `encode_pixel_value_u32` packs `value << 4 | metric`. For the raw
2073        // fixed-point value -1 (i.e. -0.001) the value bits are 0xFFFF_FFF0, so any
2074        // metric whose code is >= 9 (vh = 9, vmin = 10, vmax = 11) ORs straight into
2075        // the sentinel band:
2076        //     -0.001vh   -> 0xFFFF_FFF9 == U32_MAX_CONTENT
2077        //     -0.001vmin -> 0xFFFF_FFFA == U32_MIN_CONTENT
2078        //     -0.001vmax -> 0xFFFF_FFFB == U32_INITIAL
2079        for metric in [SizeMetric::Vh, SizeMetric::Vmin, SizeMetric::Vmax] {
2080            let pv = PixelValue::from_metric(metric, -0.001);
2081            let enc = encode_pixel_prop(&CssPropertyValue::Exact(LayoutMinWidth { inner: pv }));
2082            assert!(
2083                enc == U32_SENTINEL || enc < U32_SENTINEL_THRESHOLD,
2084                "an Exact viewport length encoded to {enc:#x}, which aliases a semantic sentinel",
2085            );
2086        }
2087    }
2088
2089    // -------------------------------------------------------------------------
2090    // encode_css_pixel_as_i16 / encode_margin_i16
2091    // -------------------------------------------------------------------------
2092
2093    #[test]
2094    fn css_pixel_i16_scales_by_ten() {
2095        assert_eq!(encode_css_pixel_as_i16(&padding(0.0)), 0);
2096        assert_eq!(encode_css_pixel_as_i16(&padding(10.5)), 105);
2097        assert_eq!(encode_css_pixel_as_i16(&padding(-10.5)), -105);
2098    }
2099
2100    #[test]
2101    fn css_pixel_i16_boundaries() {
2102        // 3276.3px is the largest representable value (one below the sentinel band)
2103        assert_eq!(encode_css_pixel_as_i16(&padding(3276.3)), 32_763);
2104        // one tick further must saturate, NOT alias I16_INITIAL (32764)
2105        assert_eq!(encode_css_pixel_as_i16(&padding(3276.4)), I16_SENTINEL);
2106        // and the negative end
2107        assert_eq!(encode_css_pixel_as_i16(&padding(-3276.8)), -32_768);
2108        assert_eq!(encode_css_pixel_as_i16(&padding(-3276.9)), I16_SENTINEL);
2109    }
2110
2111    #[test]
2112    fn css_pixel_i16_non_px_units_need_the_slow_path() {
2113        let em = CssPropertyValue::Exact(LayoutPaddingTop { inner: PixelValue::em(2.0) });
2114        let pct = CssPropertyValue::Exact(LayoutPaddingTop {
2115            inner: PixelValue::percent(50.0),
2116        });
2117        assert_eq!(encode_css_pixel_as_i16(&em), I16_SENTINEL);
2118        assert_eq!(encode_css_pixel_as_i16(&pct), I16_SENTINEL);
2119    }
2120
2121    #[test]
2122    fn css_pixel_i16_keywords_are_distinguishable() {
2123        let auto: CssPropertyValue<LayoutPaddingTop> = CssPropertyValue::Auto;
2124        let initial: CssPropertyValue<LayoutPaddingTop> = CssPropertyValue::Initial;
2125        let inherit: CssPropertyValue<LayoutPaddingTop> = CssPropertyValue::Inherit;
2126        let none: CssPropertyValue<LayoutPaddingTop> = CssPropertyValue::None;
2127        let revert: CssPropertyValue<LayoutPaddingTop> = CssPropertyValue::Revert;
2128        let unset: CssPropertyValue<LayoutPaddingTop> = CssPropertyValue::Unset;
2129        assert_eq!(encode_css_pixel_as_i16(&auto), I16_AUTO);
2130        assert_eq!(encode_css_pixel_as_i16(&initial), I16_INITIAL);
2131        assert_eq!(encode_css_pixel_as_i16(&inherit), I16_INHERIT);
2132        // none / revert / unset have no dedicated slot -> generic sentinel
2133        assert_eq!(encode_css_pixel_as_i16(&none), I16_SENTINEL);
2134        assert_eq!(encode_css_pixel_as_i16(&revert), I16_SENTINEL);
2135        assert_eq!(encode_css_pixel_as_i16(&unset), I16_SENTINEL);
2136    }
2137
2138    #[test]
2139    fn css_pixel_i16_nan_and_infinity_are_safe() {
2140        assert_eq!(encode_css_pixel_as_i16(&padding(f32::NAN)), 0);
2141        assert_eq!(encode_css_pixel_as_i16(&padding(f32::INFINITY)), I16_SENTINEL);
2142        assert_eq!(
2143            encode_css_pixel_as_i16(&padding(f32::NEG_INFINITY)),
2144            I16_SENTINEL
2145        );
2146        assert_eq!(encode_css_pixel_as_i16(&padding(f32::MAX)), I16_SENTINEL);
2147        assert_eq!(encode_css_pixel_as_i16(&padding(f32::MIN)), I16_SENTINEL);
2148    }
2149
2150    #[test]
2151    fn css_pixel_i16_exact_value_never_aliases_a_keyword_sentinel() {
2152        // The i16 encoder range-checks *both* ends before narrowing, so — unlike the
2153        // u32 path — an Exact px value can never be mistaken for auto/inherit/initial.
2154        for px in [
2155            -3276.8f32, -100.0, -0.1, 0.0, 0.1, 100.0, 3276.3, 1.0e9, -1.0e9,
2156        ] {
2157            let e = encode_css_pixel_as_i16(&padding(px));
2158            assert!(
2159                e != I16_AUTO && e != I16_INHERIT && e != I16_INITIAL,
2160                "{px}px aliased a keyword sentinel ({e})"
2161            );
2162        }
2163    }
2164
2165    #[test]
2166    fn margin_i16_keeps_auto_and_otherwise_matches_the_pixel_encoder() {
2167        let auto: CssPropertyValue<LayoutMarginTop> = CssPropertyValue::Auto;
2168        assert_eq!(encode_margin_i16(&auto), I16_AUTO);
2169        for px in [-50.0f32, 0.0, 12.5, 3276.3, 5.0e9, f32::NAN] {
2170            let m = CssPropertyValue::Exact(LayoutMarginTop { inner: PixelValue::px(px) });
2171            assert_eq!(encode_margin_i16(&m), encode_css_pixel_as_i16(&padding(px)));
2172        }
2173    }
2174
2175    // -------------------------------------------------------------------------
2176    // encode_flex_basis
2177    // -------------------------------------------------------------------------
2178
2179    #[test]
2180    fn flex_basis_all_variants() {
2181        assert_eq!(
2182            encode_flex_basis(&CssPropertyValue::Exact(LayoutFlexBasis::Auto)),
2183            U32_AUTO
2184        );
2185        let enc = encode_flex_basis(&CssPropertyValue::Exact(LayoutFlexBasis::Exact(
2186            PixelValue::px(120.0),
2187        )));
2188        let dec = decode_pixel_value_u32(enc).expect("px flex-basis must round-trip");
2189        assert!((dec.number.get() - 120.0).abs() < 0.002);
2190
2191        assert_eq!(encode_flex_basis(&CssPropertyValue::Auto), U32_AUTO);
2192        assert_eq!(encode_flex_basis(&CssPropertyValue::None), U32_NONE);
2193        assert_eq!(encode_flex_basis(&CssPropertyValue::Initial), U32_INITIAL);
2194        assert_eq!(encode_flex_basis(&CssPropertyValue::Inherit), U32_INHERIT);
2195        assert_eq!(encode_flex_basis(&CssPropertyValue::Revert), U32_SENTINEL);
2196        assert_eq!(encode_flex_basis(&CssPropertyValue::Unset), U32_SENTINEL);
2197    }
2198
2199    #[test]
2200    fn flex_basis_overflow_saturates() {
2201        assert_eq!(
2202            encode_flex_basis(&CssPropertyValue::Exact(LayoutFlexBasis::Exact(
2203                PixelValue::px(1.0e9)
2204            ))),
2205            U32_SENTINEL
2206        );
2207        assert_eq!(
2208            encode_flex_basis(&CssPropertyValue::Exact(LayoutFlexBasis::Exact(
2209                PixelValue::px(f32::INFINITY)
2210            ))),
2211            U32_SENTINEL
2212        );
2213    }
2214
2215    // -------------------------------------------------------------------------
2216    // update_dom_declared_flags
2217    // -------------------------------------------------------------------------
2218
2219    fn text_indent_prop() -> CssProperty {
2220        CssProperty::TextIndent(CssPropertyValue::Exact(StyleTextIndent::default()))
2221    }
2222
2223    fn line_height_prop(pct: f32) -> CssProperty {
2224        CssProperty::LineHeight(CssPropertyValue::Exact(StyleLineHeight {
2225            inner: PercentageValue::new(pct),
2226        }))
2227    }
2228
2229    #[test]
2230    fn dom_flags_set_the_right_bit_from_zero() {
2231        let mut flags = 0u32;
2232        update_dom_declared_flags(&text_indent_prop(), &mut flags);
2233        assert_eq!(flags, DOM_HAS_TEXT_INDENT);
2234
2235        let mut flags2 = 0u32;
2236        update_dom_declared_flags(&line_height_prop(150.0), &mut flags2);
2237        assert_eq!(flags2, DOM_HAS_LINE_HEIGHT);
2238    }
2239
2240    #[test]
2241    fn dom_flags_only_ever_or_never_clear() {
2242        // Starting from all-ones, the function must not clear a single bit.
2243        let mut flags = u32::MAX;
2244        update_dom_declared_flags(&text_indent_prop(), &mut flags);
2245        update_dom_declared_flags(&line_height_prop(150.0), &mut flags);
2246        update_dom_declared_flags(
2247            &CssProperty::Width(CssPropertyValue::Exact(LayoutWidth::px(10.0))),
2248            &mut flags,
2249        );
2250        assert_eq!(flags, u32::MAX);
2251    }
2252
2253    #[test]
2254    fn dom_flags_accumulate_and_are_idempotent() {
2255        let mut flags = 0u32;
2256        update_dom_declared_flags(&text_indent_prop(), &mut flags);
2257        update_dom_declared_flags(&line_height_prop(150.0), &mut flags);
2258        let after_two = flags;
2259        assert_eq!(after_two, DOM_HAS_TEXT_INDENT | DOM_HAS_LINE_HEIGHT);
2260        // re-applying the same properties must be a no-op
2261        update_dom_declared_flags(&text_indent_prop(), &mut flags);
2262        update_dom_declared_flags(&line_height_prop(150.0), &mut flags);
2263        assert_eq!(flags, after_two);
2264    }
2265
2266    #[test]
2267    fn dom_flags_are_not_set_for_a_valueless_property() {
2268        // `line-height: initial` / `text-indent: auto` carry no Exact payload, so the
2269        // "declared" fast-path bit must stay clear (the slow walk would find nothing).
2270        let mut flags = 0u32;
2271        update_dom_declared_flags(&CssProperty::LineHeight(CssPropertyValue::Initial), &mut flags);
2272        update_dom_declared_flags(&CssProperty::TextIndent(CssPropertyValue::Auto), &mut flags);
2273        update_dom_declared_flags(&CssProperty::TextIndent(CssPropertyValue::Unset), &mut flags);
2274        assert_eq!(flags, 0);
2275    }
2276
2277    #[test]
2278    fn dom_flags_ignore_unrelated_properties() {
2279        let mut flags = 0u32;
2280        update_dom_declared_flags(
2281            &CssProperty::Width(CssPropertyValue::Exact(LayoutWidth::px(10.0))),
2282            &mut flags,
2283        );
2284        update_dom_declared_flags(
2285            &CssProperty::ZIndex(CssPropertyValue::Exact(LayoutZIndex::Integer(3))),
2286            &mut flags,
2287        );
2288        assert_eq!(flags, 0);
2289    }
2290
2291    // -------------------------------------------------------------------------
2292    // apply_css_property_to_compact — tier 1 bitfield
2293    // -------------------------------------------------------------------------
2294
2295    #[test]
2296    fn apply_tier1_fields_do_not_bleed_into_each_other() {
2297        let mut s = Sink::new();
2298        s.apply(&CssProperty::Display(CssPropertyValue::Exact(
2299            LayoutDisplay::InlineBlock,
2300        )));
2301        s.apply(&CssProperty::Position(CssPropertyValue::Exact(
2302            LayoutPosition::Absolute,
2303        )));
2304        // border-collapse lives at bit 52, i.e. at the far end of the bitfield
2305        s.apply(&CssProperty::BorderCollapse(CssPropertyValue::Exact(
2306            StyleBorderCollapse::Collapse,
2307        )));
2308
2309        assert_eq!(
2310            (s.tier1 >> DISPLAY_SHIFT) & DISPLAY_MASK,
2311            u64::from(layout_display_to_u8(LayoutDisplay::InlineBlock))
2312        );
2313        assert_eq!(
2314            (s.tier1 >> POSITION_SHIFT) & POSITION_MASK,
2315            u64::from(layout_position_to_u8(LayoutPosition::Absolute))
2316        );
2317        assert_eq!(
2318            (s.tier1 >> BORDER_COLLAPSE_SHIFT) & BORDER_COLLAPSE_MASK,
2319            u64::from(border_collapse_to_u8(StyleBorderCollapse::Collapse))
2320        );
2321
2322        let known = (DISPLAY_MASK << DISPLAY_SHIFT)
2323            | (POSITION_MASK << POSITION_SHIFT)
2324            | (BORDER_COLLAPSE_MASK << BORDER_COLLAPSE_SHIFT);
2325        assert_eq!(
2326            s.tier1 & !known,
2327            0,
2328            "tier1 = {:#x} has bits set outside the three fields that were written",
2329            s.tier1
2330        );
2331    }
2332
2333    #[test]
2334    fn apply_tier1_overwrite_clears_only_its_own_field() {
2335        // Hostile starting state: every bit set. The clear-then-set in `set_tier1!`
2336        // must wipe exactly the display field and leave every neighbour intact.
2337        let mut s = Sink::new();
2338        s.tier1 = u64::MAX;
2339        s.apply(&CssProperty::Display(CssPropertyValue::Exact(
2340            LayoutDisplay::Block,
2341        )));
2342        assert_eq!(
2343            (s.tier1 >> DISPLAY_SHIFT) & DISPLAY_MASK,
2344            u64::from(layout_display_to_u8(LayoutDisplay::Block))
2345        );
2346        let others = !(DISPLAY_MASK << DISPLAY_SHIFT);
2347        assert_eq!(
2348            s.tier1 & others,
2349            u64::MAX & others,
2350            "neighbouring tier-1 fields were clobbered"
2351        );
2352    }
2353
2354    #[test]
2355    fn apply_tier1_ignores_a_valueless_property() {
2356        let mut s = Sink::new();
2357        s.apply(&CssProperty::Display(CssPropertyValue::Inherit));
2358        assert_eq!(s.tier1, 0, "`display: inherit` has no Exact payload to encode");
2359    }
2360
2361    // -------------------------------------------------------------------------
2362    // apply_css_property_to_compact — tier 2 dims
2363    // -------------------------------------------------------------------------
2364
2365    #[test]
2366    fn apply_width_round_trips_and_touches_nothing_else() {
2367        let mut s = Sink::new();
2368        let before_cold = s.cold;
2369        let before_text = s.text;
2370        s.apply(&CssProperty::Width(CssPropertyValue::Exact(LayoutWidth::Px(
2371            PixelValue::px(320.0),
2372        ))));
2373        let dec = decode_pixel_value_u32(s.dims.width).expect("width must round-trip");
2374        assert!((dec.number.get() - 320.0).abs() < 0.002);
2375        assert_eq!(s.tier1, 0, "a tier-2 property must not touch the tier-1 bitfield");
2376        assert_eq!(s.cold, before_cold, "a tier-2 property must not touch tier-2 cold");
2377        assert_eq!(s.text, before_text, "a tier-2 property must not touch tier-2b text");
2378        assert!(s.fonts.is_empty());
2379    }
2380
2381    #[test]
2382    fn apply_flex_grow_saturates_and_rejects_negatives() {
2383        let mut s = Sink::new();
2384        s.apply(&CssProperty::FlexGrow(CssPropertyValue::Exact(LayoutFlexGrow {
2385            inner: FloatValue::new(2.5),
2386        })));
2387        assert_eq!(s.dims.flex_grow, 250);
2388
2389        // A negative flex-grow must not wrap around into a huge positive u16.
2390        let mut neg = Sink::new();
2391        neg.apply(&CssProperty::FlexGrow(CssPropertyValue::Exact(LayoutFlexGrow {
2392            inner: FloatValue::new(-1.0),
2393        })));
2394        assert_eq!(neg.dims.flex_grow, U16_SENTINEL);
2395
2396        // ...and neither must an absurdly large one.
2397        let mut big = Sink::new();
2398        big.apply(&CssProperty::FlexGrow(CssPropertyValue::Exact(LayoutFlexGrow {
2399            inner: FloatValue::new(1.0e9),
2400        })));
2401        assert_eq!(big.dims.flex_grow, U16_SENTINEL);
2402
2403        // NaN degrades to 0 rather than to a wrapped value.
2404        let mut nan = Sink::new();
2405        nan.apply(&CssProperty::FlexShrink(CssPropertyValue::Exact(
2406            LayoutFlexShrink { inner: FloatValue::new(f32::NAN) },
2407        )));
2408        assert_eq!(nan.dims.flex_shrink, 0);
2409    }
2410
2411    #[test]
2412    fn apply_gap_px_sets_both_axes_and_ignores_unresolvable_units() {
2413        let mut s = Sink::new();
2414        s.apply(&CssProperty::Gap(CssPropertyValue::Exact(LayoutGap {
2415            inner: PixelValue::px(8.0),
2416        })));
2417        assert_eq!(s.dims.row_gap, 80);
2418        assert_eq!(s.dims.column_gap, 80);
2419
2420        // An `em` gap cannot be resolved without a font context — it must be left
2421        // untouched (so the slow path can handle it), not silently encoded as 2px.
2422        let mut em = Sink::new();
2423        em.apply(&CssProperty::Gap(CssPropertyValue::Exact(LayoutGap {
2424            inner: PixelValue::em(2.0),
2425        })));
2426        assert_eq!(em.dims.row_gap, 0);
2427        assert_eq!(em.dims.column_gap, 0);
2428    }
2429
2430    // -------------------------------------------------------------------------
2431    // apply_css_property_to_compact — tier 2 cold
2432    // -------------------------------------------------------------------------
2433
2434    #[test]
2435    fn apply_z_index_auto_and_in_range_values() {
2436        let mut s = Sink::new();
2437        s.apply(&CssProperty::ZIndex(CssPropertyValue::Exact(LayoutZIndex::Auto)));
2438        assert_eq!(s.cold.z_index, I16_AUTO);
2439        s.apply(&CssProperty::ZIndex(CssPropertyValue::Exact(
2440            LayoutZIndex::Integer(100),
2441        )));
2442        assert_eq!(s.cold.z_index, 100);
2443        // last value below the sentinel band
2444        s.apply(&CssProperty::ZIndex(CssPropertyValue::Exact(
2445            LayoutZIndex::Integer(32_763),
2446        )));
2447        assert_eq!(s.cold.z_index, 32_763);
2448    }
2449
2450    #[test]
2451    fn apply_z_index_large_positive_saturates() {
2452        for z in [32_764i32, 100_000, i32::MAX] {
2453            let mut s = Sink::new();
2454            s.apply(&CssProperty::ZIndex(CssPropertyValue::Exact(
2455                LayoutZIndex::Integer(z),
2456            )));
2457            assert_eq!(s.cold.z_index, I16_SENTINEL, "z-index {z} should saturate");
2458        }
2459    }
2460
2461    #[test]
2462    fn apply_z_index_large_negative_must_not_wrap_positive() {
2463        // The encoder range-checks only the UPPER bound:
2464        //     if *z >= I16_SENTINEL_THRESHOLD { I16_SENTINEL } else { *z as i16 }
2465        // so a large negative z-index truncates instead of saturating, e.g.
2466        //     z-index: -40000  ->  -40000 as i16  ==  +25536
2467        // which flips the node from the very back of the stacking context to the
2468        // front. Compare with the line-height encoder, which *does* check
2469        // `pct_x10 >= -32768` before narrowing.
2470        for z in [-32_769i32, -40_000, -99_999, i32::MIN] {
2471            let mut s = Sink::new();
2472            s.apply(&CssProperty::ZIndex(CssPropertyValue::Exact(
2473                LayoutZIndex::Integer(z),
2474            )));
2475            assert!(
2476                s.cold.z_index < 0 || s.cold.z_index == I16_SENTINEL,
2477                "z-index {z} encoded to {}: a negative z-index must stay negative (or \
2478                 saturate to the sentinel), it must never wrap to a positive value",
2479                s.cold.z_index,
2480            );
2481        }
2482    }
2483
2484    #[test]
2485    fn apply_border_styles_pack_into_independent_nibbles() {
2486        let mut s = Sink::new();
2487        s.apply(&CssProperty::BorderTopStyle(CssPropertyValue::Exact(
2488            StyleBorderTopStyle { inner: BorderStyle::Solid },
2489        )));
2490        assert_eq!(
2491            s.cold.border_styles_packed & 0x000F,
2492            u16::from(border_style_to_u8(BorderStyle::Solid))
2493        );
2494        assert_eq!(
2495            s.cold.border_styles_packed & 0xFFF0,
2496            0,
2497            "the top-style nibble leaked into the other three sides"
2498        );
2499
2500        // Re-applying must REPLACE the nibble, not OR into it: Solid(1) | Double(2)
2501        // would be Dotted(3), a different border style entirely.
2502        s.apply(&CssProperty::BorderTopStyle(CssPropertyValue::Exact(
2503            StyleBorderTopStyle { inner: BorderStyle::Double },
2504        )));
2505        assert_eq!(
2506            s.cold.border_styles_packed & 0x000F,
2507            u16::from(border_style_to_u8(BorderStyle::Double))
2508        );
2509    }
2510
2511    #[test]
2512    fn apply_opacity_clamps_into_the_0_254_range() {
2513        for (pct, expected) in [
2514            (-1.0e9f32, 0u8),
2515            (-100.0, 0),
2516            (0.0, 0),
2517            (50.0, 127),
2518            (100.0, 254),
2519            (500.0, 254),
2520            (1.0e9, 254),
2521        ] {
2522            let mut s = Sink::new();
2523            s.apply(&CssProperty::Opacity(CssPropertyValue::Exact(StyleOpacity {
2524                inner: PercentageValue::new(pct),
2525            })));
2526            assert_eq!(s.cold.opacity, expected, "opacity: {pct}%");
2527            assert_ne!(
2528                s.cold.opacity, OPACITY_SENTINEL,
2529                "an explicitly set opacity must never encode as the 'unset' sentinel"
2530            );
2531        }
2532    }
2533
2534    #[test]
2535    fn apply_grid_column_encodes_both_lines() {
2536        let mut s = Sink::new();
2537        s.apply(&CssProperty::GridColumn(CssPropertyValue::Exact(GridPlacement {
2538            grid_start: GridLine::Line(2),
2539            grid_end: GridLine::Span(3),
2540        })));
2541        assert_eq!(s.cold.grid_col_start, 2);
2542        assert_eq!(s.cold.grid_col_end, -3);
2543        // grid-row must be untouched by a grid-column declaration
2544        assert_eq!(s.cold.grid_row_start, I16_AUTO);
2545        assert_eq!(s.cold.grid_row_end, I16_AUTO);
2546    }
2547
2548    #[test]
2549    fn apply_hot_flags_or_in_without_clobbering_each_other() {
2550        let mut s = Sink::new();
2551        s.apply(&CssProperty::TextDecoration(CssPropertyValue::Exact(
2552            StyleTextDecoration::Underline,
2553        )));
2554        assert_eq!(
2555            s.cold.hot_flags & HOT_FLAG_HAS_TEXT_DECORATION,
2556            HOT_FLAG_HAS_TEXT_DECORATION
2557        );
2558
2559        // scrollbar-gutter writes a 2-bit *field* into the same byte; it must not
2560        // wipe the has-* bits around it.
2561        s.apply(&CssProperty::ScrollbarGutter(CssPropertyValue::Exact(
2562            StyleScrollbarGutter::Stable,
2563        )));
2564        assert_eq!(
2565            (s.cold.hot_flags & HOT_FLAG_SCROLLBAR_GUTTER_MASK) >> HOT_FLAG_SCROLLBAR_GUTTER_SHIFT,
2566            SCROLLBAR_GUTTER_STABLE
2567        );
2568        assert_eq!(
2569            s.cold.hot_flags & HOT_FLAG_HAS_TEXT_DECORATION,
2570            HOT_FLAG_HAS_TEXT_DECORATION,
2571            "scrollbar-gutter cleared the has-text-decoration bit"
2572        );
2573
2574        // ...and replacing the gutter value must clear the old bits, not OR into them
2575        s.apply(&CssProperty::ScrollbarGutter(CssPropertyValue::Exact(
2576            StyleScrollbarGutter::Auto,
2577        )));
2578        assert_eq!(
2579            (s.cold.hot_flags & HOT_FLAG_SCROLLBAR_GUTTER_MASK) >> HOT_FLAG_SCROLLBAR_GUTTER_SHIFT,
2580            SCROLLBAR_GUTTER_AUTO
2581        );
2582        assert_eq!(
2583            s.cold.hot_flags & HOT_FLAG_HAS_TEXT_DECORATION,
2584            HOT_FLAG_HAS_TEXT_DECORATION
2585        );
2586    }
2587
2588    #[test]
2589    fn apply_valueless_property_does_not_set_a_has_flag() {
2590        // The has-* bits exist so the getter can skip the cascade walk. A property
2591        // with no Exact payload must leave them clear, or every node pays for a walk
2592        // that would find nothing.
2593        let mut s = Sink::new();
2594        s.apply(&CssProperty::TextDecoration(CssPropertyValue::Initial));
2595        s.apply(&CssProperty::ScrollbarGutter(CssPropertyValue::Unset));
2596        assert_eq!(s.cold.hot_flags, 0);
2597    }
2598
2599    // -------------------------------------------------------------------------
2600    // apply_css_property_to_compact — tier 2b text
2601    // -------------------------------------------------------------------------
2602
2603    #[test]
2604    fn apply_text_color_packs_rgba_big_endian() {
2605        let mut s = Sink::new();
2606        s.apply(&CssProperty::TextColor(CssPropertyValue::Exact(StyleTextColor {
2607            inner: ColorU { r: 0x12, g: 0x34, b: 0x56, a: 0x78 },
2608        })));
2609        assert_eq!(s.text.text_color, 0x1234_5678);
2610
2611        // Documented limitation: rgba(0,0,0,0) is indistinguishable from "unset".
2612        let mut transparent = Sink::new();
2613        transparent.apply(&CssProperty::TextColor(CssPropertyValue::Exact(
2614            StyleTextColor { inner: ColorU { r: 0, g: 0, b: 0, a: 0 } },
2615        )));
2616        assert_eq!(transparent.text.text_color, 0);
2617    }
2618
2619    #[test]
2620    fn apply_line_height_round_trips_and_saturates_at_both_ends() {
2621        let mut s = Sink::new();
2622        s.apply(&line_height_prop(120.0));
2623        assert_eq!(s.text.line_height, 1200, "120% must encode as % x 10");
2624
2625        // Absurd values must saturate - no wrap-around. The two signs land
2626        // differently by design: a huge POSITIVE (unitless multiple) falls to
2627        // the sentinel ("normal" - a 10^7x multiple is meaningless), while a
2628        // huge NEGATIVE (= absolute px per the parser convention) CLAMPS to
2629        // the largest representable px (-32768 = 3276.8px) instead of being
2630        // silently reinterpreted as "normal".
2631        let mut big = Sink::new();
2632        big.apply(&line_height_prop(1.0e9f32));
2633        assert_eq!(
2634            big.text.line_height, I16_SENTINEL,
2635            "a huge unitless multiple saturates to the sentinel"
2636        );
2637        let mut neg = Sink::new();
2638        neg.apply(&line_height_prop(-1.0e9f32));
2639        assert_eq!(
2640            neg.text.line_height, -32768,
2641            "a huge absolute px line-height clamps instead of dropping to normal"
2642        );
2643
2644        // The split scale itself: 48px (normalized -48) stores as -480 and
2645        // decodes back to 48px - the old x1000 scale overflowed at 32.76px.
2646        let mut px48 = Sink::new();
2647        px48.apply(&line_height_prop(-4800.0));
2648        assert_eq!(px48.text.line_height, -480, "line-height: 48px stores as -px x 10");
2649    }
2650
2651    #[test]
2652    fn apply_font_family_hash_is_nonzero_stable_and_registered() {
2653        let arial = StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("Arial".into())]);
2654
2655        let mut s = Sink::new();
2656        s.apply(&CssProperty::FontFamily(CssPropertyValue::Exact(arial.clone())));
2657        let h = s.text.font_family_hash;
2658        assert_ne!(
2659            h, 0,
2660            "0 is the 'unset' sentinel — a set font-family must never hash to it"
2661        );
2662        assert!(
2663            s.fonts.contains_key(&h),
2664            "the hash must be registered in the reverse map, or consumers cannot resolve it"
2665        );
2666
2667        // Same input -> same hash (the whole dirty-tracking scheme depends on this).
2668        let mut same = Sink::new();
2669        same.apply(&CssProperty::FontFamily(CssPropertyValue::Exact(arial)));
2670        assert_eq!(same.text.font_family_hash, h);
2671
2672        // Different input -> different hash.
2673        let mut other = Sink::new();
2674        other.apply(&CssProperty::FontFamily(CssPropertyValue::Exact(
2675            StyleFontFamilyVec::from_vec(vec![StyleFontFamily::System("Times".into())]),
2676        )));
2677        assert_ne!(other.text.font_family_hash, h);
2678    }
2679
2680    // -------------------------------------------------------------------------
2681    // apply_ua_css_to_compact
2682    // -------------------------------------------------------------------------
2683
2684    #[test]
2685    fn ua_css_is_idempotent_for_every_representative_node_type() {
2686        let nodes = [
2687            NodeData::create_node(NodeType::Html),
2688            NodeData::create_node(NodeType::Body),
2689            NodeData::create_node(NodeType::Div),
2690            NodeData::create_node(NodeType::P),
2691            NodeData::create_node(NodeType::Br),
2692            NodeData::create_text("hello"),
2693        ];
2694        for nd in &nodes {
2695            let mut s = Sink::new();
2696            s.ua(&nd.node_type);
2697            let once = s.snapshot();
2698            s.ua(&nd.node_type);
2699            assert_eq!(
2700                s.snapshot(),
2701                once,
2702                "applying UA CSS twice must be a no-op the second time"
2703            );
2704        }
2705    }
2706
2707    #[test]
2708    fn ua_css_never_touches_the_tier1_populated_bit() {
2709        // Bit 63 is owned by the builder, not by the UA stylesheet.
2710        for nt in [NodeType::Html, NodeType::Body, NodeType::Div, NodeType::P] {
2711            let mut s = Sink::new();
2712            s.ua(&nt);
2713            assert_eq!(s.tier1 & TIER1_POPULATED_BIT, 0);
2714        }
2715    }
2716
2717    #[test]
2718    fn ua_css_survives_a_hostile_pre_filled_sink() {
2719        // Every bit set / every numeric field at an extreme: the writer must still
2720        // only touch its own fields and must not panic on the sentinel inputs.
2721        let mut s = Sink::new();
2722        s.tier1 = u64::MAX;
2723        s.dims.width = U32_SENTINEL;
2724        s.dims.font_size = U32_SENTINEL;
2725        s.dims.flex_grow = U16_SENTINEL;
2726        s.cold.z_index = i16::MIN;
2727        s.cold.opacity = OPACITY_SENTINEL;
2728        s.text.line_height = i16::MIN;
2729        s.ua(&NodeType::Div);
2730        assert_eq!(
2731            s.tier1 & TIER1_POPULATED_BIT,
2732            TIER1_POPULATED_BIT,
2733            "UA CSS must not clear bits it does not own"
2734        );
2735    }
2736
2737    // -------------------------------------------------------------------------
2738    // build_compact_cache
2739    // -------------------------------------------------------------------------
2740
2741    #[test]
2742    fn build_compact_cache_handles_zero_nodes() {
2743        let cache = CssPropertyCache::empty(0);
2744        let r = cache.build_compact_cache(&[], &[]);
2745        assert_eq!(r.node_count(), 0);
2746        assert!(r.tier2_dims.is_empty());
2747        assert!(r.font_dirty_nodes.is_empty());
2748        assert!(r.prev_font_hashes.is_empty());
2749    }
2750
2751    #[test]
2752    fn build_compact_cache_tolerates_a_mismatched_prev_font_hash_slice() {
2753        let cache = CssPropertyCache::empty(3);
2754        let nodes = div_nodes(3);
2755        // longer than node_count, shorter than node_count, and empty — none may panic
2756        for prev in [vec![1u64, 2, 3, 4, 5, 6], vec![7u64], Vec::new()] {
2757            let r = cache.build_compact_cache(&nodes, &prev);
2758            assert_eq!(r.prev_font_hashes.len(), 3);
2759            assert_eq!(r.node_count(), 3);
2760        }
2761    }
2762
2763    #[test]
2764    fn build_compact_cache_tolerates_short_node_data() {
2765        // node_count claims 4 but only 2 NodeDatas are supplied: the trailing nodes
2766        // must keep their defaults instead of indexing out of bounds.
2767        let cache = CssPropertyCache::empty(4);
2768        let r = cache.build_compact_cache(&div_nodes(2), &[]);
2769        assert_eq!(r.node_count(), 4);
2770        assert_eq!(r.tier2_dims.len(), 4);
2771        assert_eq!(r.tier2_cold.len(), 4);
2772        assert_eq!(r.tier2b_text.len(), 4);
2773        assert_eq!(r.prev_font_hashes.len(), 4);
2774    }
2775
2776    #[test]
2777    fn build_compact_cache_honours_node_count_over_node_data_len() {
2778        let cache = CssPropertyCache::empty(2);
2779        let r = cache.build_compact_cache(&div_nodes(5), &[]);
2780        assert_eq!(r.node_count(), 2);
2781    }
2782
2783    #[test]
2784    fn build_compact_cache_rebuild_with_unchanged_fonts_is_not_dirty() {
2785        let cache = CssPropertyCache::empty(3);
2786        let nodes = div_nodes(3);
2787        let first = cache.build_compact_cache(&nodes, &[]);
2788        let second = cache.build_compact_cache(&nodes, &first.prev_font_hashes);
2789        assert!(
2790            second.font_dirty_nodes.is_empty(),
2791            "a rebuild with identical font hashes must not re-resolve any font chain"
2792        );
2793    }
2794
2795    // -------------------------------------------------------------------------
2796    // build_compact_cache_with_inheritance{,_debug}
2797    // -------------------------------------------------------------------------
2798
2799    #[test]
2800    fn build_with_inheritance_handles_zero_nodes() {
2801        let cache = CssPropertyCache::empty(0);
2802        let r = cache.build_compact_cache_with_inheritance(&[], &[], &[]);
2803        assert_eq!(r.node_count(), 0);
2804
2805        let mut msgs = None;
2806        let r2 = cache.build_compact_cache_with_inheritance_debug(&[], &[], &[], &mut msgs);
2807        assert_eq!(r2.node_count(), 0);
2808        assert!(msgs.is_none());
2809    }
2810
2811    #[test]
2812    fn build_with_inheritance_propagates_font_size_down_the_chain() {
2813        let n = 3;
2814        let cache = CssPropertyCache::empty(n);
2815        let r = cache.build_compact_cache_with_inheritance(
2816            &div_nodes(n),
2817            &linear_hierarchy(n),
2818            &[],
2819        );
2820        assert_eq!(r.node_count(), n);
2821        // font-size is inheritable: property-less children must match the root exactly.
2822        assert_eq!(r.tier2_dims[1].font_size, r.tier2_dims[0].font_size);
2823        assert_eq!(r.tier2_dims[2].font_size, r.tier2_dims[0].font_size);
2824    }
2825
2826    #[test]
2827    fn build_with_inheritance_marks_all_nodes_dirty_on_the_first_build() {
2828        let n = 3;
2829        let cache = CssPropertyCache::empty(n);
2830        let nodes = div_nodes(n);
2831        let hierarchy = linear_hierarchy(n);
2832
2833        // Empty prev_font_hashes == first build for this DOM -> force ALL nodes dirty.
2834        let first = cache.build_compact_cache_with_inheritance(&nodes, &hierarchy, &[]);
2835        assert_eq!(first.font_dirty_nodes, vec![0, 1, 2]);
2836
2837        // Second build with the previous hashes -> nothing changed, nothing dirty.
2838        let second =
2839            cache.build_compact_cache_with_inheritance(&nodes, &hierarchy, &first.prev_font_hashes);
2840        assert!(second.font_dirty_nodes.is_empty());
2841    }
2842
2843    #[test]
2844    fn build_with_inheritance_global_star_rules_skip_text_nodes() {
2845        // Per CSS, `*` matches ELEMENTS. A text node is not an element — it may only
2846        // inherit from its parent, otherwise `* { padding: 5px }` would overwrite the
2847        // value a text node inherited from `<p>`.
2848        let mut cache = CssPropertyCache::empty(2);
2849        cache
2850            .global_css_props
2851            .push(CssProperty::PaddingTop(padding(5.0)));
2852
2853        let nodes = vec![
2854            NodeData::create_node(NodeType::Div),
2855            NodeData::create_text("hi"),
2856        ];
2857        let r = cache.build_compact_cache_with_inheritance(&nodes, &linear_hierarchy(2), &[]);
2858
2859        assert_eq!(
2860            r.tier2_dims[0].padding_top, 50,
2861            "the `*` rule must apply to the element"
2862        );
2863        assert_ne!(
2864            r.tier2_dims[1].padding_top, 50,
2865            "the `*` rule must NOT apply to a text node"
2866        );
2867    }
2868
2869    #[test]
2870    fn build_with_inheritance_debug_messages_are_opt_in() {
2871        let n = 2;
2872        let cache = CssPropertyCache::empty(n);
2873        let nodes = div_nodes(n);
2874        let hierarchy = linear_hierarchy(n);
2875
2876        let mut on = Some(Vec::new());
2877        let _ = cache.build_compact_cache_with_inheritance_debug(&nodes, &hierarchy, &[], &mut on);
2878        assert!(
2879            !on.expect("still Some").is_empty(),
2880            "debug logging must emit at least one cascade message"
2881        );
2882
2883        let mut off = None;
2884        let _ = cache.build_compact_cache_with_inheritance_debug(&nodes, &hierarchy, &[], &mut off);
2885        assert!(off.is_none(), "a None sink must stay None");
2886    }
2887
2888    // -------------------------------------------------------------------------
2889    // resolve_font_size_to_px
2890    // -------------------------------------------------------------------------
2891
2892    #[test]
2893    fn resolve_font_size_percent_uses_the_parent() {
2894        let mut dims = vec![CompactNodeProps::default(); 2];
2895        dims[0].font_size = encode_pixel_value_u32(&PixelValue::px(20.0));
2896        dims[1].font_size = encode_pixel_value_u32(&PixelValue::percent(50.0));
2897        resolve_font_size_to_px(&mut dims, 1, Some(NodeId::new(0)));
2898        let pv = decode_pixel_value_u32(dims[1].font_size).expect("must resolve to px");
2899        assert_eq!(pv.metric, SizeMetric::Px);
2900        assert!((pv.number.get() - 10.0).abs() < 0.01, "got {}", pv.number.get());
2901    }
2902
2903    #[test]
2904    fn resolve_font_size_pt_converts_to_px() {
2905        let mut dims = vec![CompactNodeProps::default()];
2906        dims[0].font_size = encode_pixel_value_u32(&PixelValue::pt(12.0));
2907        resolve_font_size_to_px(&mut dims, 0, None);
2908        let pv = decode_pixel_value_u32(dims[0].font_size).expect("must resolve to px");
2909        assert!(
2910            (pv.number.get() - 16.0).abs() < 0.01,
2911            "12pt should be 16px, got {}",
2912            pv.number.get()
2913        );
2914    }
2915
2916    #[test]
2917    fn resolve_font_size_leaves_absolute_and_sentinel_values_alone() {
2918        // an already-px value must not be re-scaled by the parent
2919        let mut dims = vec![CompactNodeProps::default(); 2];
2920        dims[0].font_size = encode_pixel_value_u32(&PixelValue::px(20.0));
2921        dims[1].font_size = encode_pixel_value_u32(&PixelValue::px(13.0));
2922        let before = dims[1].font_size;
2923        resolve_font_size_to_px(&mut dims, 1, Some(NodeId::new(0)));
2924        assert_eq!(dims[1].font_size, before);
2925
2926        // an explicit sentinel must survive untouched
2927        let mut sent = vec![CompactNodeProps::default(); 2];
2928        sent[0].font_size = encode_pixel_value_u32(&PixelValue::px(20.0));
2929        sent[1].font_size = U32_SENTINEL;
2930        resolve_font_size_to_px(&mut sent, 1, Some(NodeId::new(0)));
2931        assert_eq!(sent[1].font_size, U32_SENTINEL);
2932
2933        // ...as must the CSS-initial default (which also sits above the threshold)
2934        let mut def = vec![CompactNodeProps::default(); 2];
2935        assert_eq!(def[1].font_size, U32_INITIAL);
2936        resolve_font_size_to_px(&mut def, 1, Some(NodeId::new(0)));
2937        assert_eq!(def[1].font_size, U32_INITIAL);
2938    }
2939
2940    #[test]
2941    fn resolve_font_size_negative_em_is_deterministic() {
2942        let mut dims = vec![CompactNodeProps::default(); 2];
2943        dims[0].font_size = encode_pixel_value_u32(&PixelValue::px(20.0));
2944        dims[1].font_size = encode_pixel_value_u32(&PixelValue::em(-2.0));
2945        resolve_font_size_to_px(&mut dims, 1, Some(NodeId::new(0)));
2946        let pv = decode_pixel_value_u32(dims[1].font_size).expect("must stay decodable");
2947        assert!(
2948            (pv.number.get() + 40.0).abs() < 0.01,
2949            "-2em of 20px should be -40px, got {}",
2950            pv.number.get()
2951        );
2952    }
2953
2954    #[test]
2955    fn resolve_font_size_overflow_saturates_instead_of_wrapping() {
2956        let mut dims = vec![CompactNodeProps::default(); 2];
2957        dims[0].font_size = encode_pixel_value_u32(&PixelValue::px(20.0));
2958        // 100_000em x 20px = 2_000_000px, past the 28-bit fixed-point range
2959        dims[1].font_size = encode_pixel_value_u32(&PixelValue::em(100_000.0));
2960        resolve_font_size_to_px(&mut dims, 1, Some(NodeId::new(0)));
2961        assert_eq!(
2962            dims[1].font_size, U32_SENTINEL,
2963            "an overflowing font-size must land on the tier-3 sentinel, not wrap"
2964        );
2965    }
2966
2967    #[test]
2968    fn resolve_font_size_nan_em_degrades_to_zero() {
2969        let mut dims = vec![CompactNodeProps::default(); 2];
2970        dims[0].font_size = encode_pixel_value_u32(&PixelValue::px(20.0));
2971        dims[1].font_size = encode_pixel_value_u32(&PixelValue::em(f32::NAN));
2972        resolve_font_size_to_px(&mut dims, 1, Some(NodeId::new(0)));
2973        let pv = decode_pixel_value_u32(dims[1].font_size).expect("must stay decodable");
2974        assert!(pv.number.get().is_finite(), "a NaN font-size must not propagate");
2975        assert_eq!(pv.number.get(), 0.0);
2976    }
2977
2978    #[test]
2979    fn resolve_font_size_root_rem_uses_the_16px_initial_value() {
2980        // For the ROOT node, `tier2_dims.first()` IS the node itself — and at this
2981        // point its font-size is still the *unresolved* rem value. The Rem arm then
2982        // multiplies the rem factor by itself:
2983        //     html { font-size: 2rem }  ->  2 * 2 = 4px   (should be 2 * 16 = 32px)
2984        // Every other unit handles the no-parent case correctly via `map_or(16.0, ..)`.
2985        let mut dims = vec![CompactNodeProps::default()];
2986        dims[0].font_size = encode_pixel_value_u32(&PixelValue::rem(2.0));
2987        resolve_font_size_to_px(&mut dims, 0, None);
2988        let pv = decode_pixel_value_u32(dims[0].font_size).expect("must resolve to px");
2989        assert!(
2990            (pv.number.get() - 32.0).abs() < 0.01,
2991            "root `font-size: 2rem` should resolve against the 16px initial value (= 32px), \
2992             got {}px",
2993            pv.number.get()
2994        );
2995    }
2996}