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