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